diff --git a/.changeset/fair-zebras-heal.md b/.changeset/fair-zebras-heal.md new file mode 100644 index 00000000..97daa384 --- /dev/null +++ b/.changeset/fair-zebras-heal.md @@ -0,0 +1,5 @@ +--- +'@callstack/react-native-brownfield': minor +--- + +feat: added postMessage API diff --git a/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt b/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt index 2d9aba0b..da2e6d93 100644 --- a/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt +++ b/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt @@ -1,5 +1,6 @@ package com.callstack.brownfield.android.example object ReactNativeConstants { - const val MAIN_MODULE_NAME = "RNApp" + const val MAIN_MODULE_NAME = "main" + const val APP_NAME = "Android (Expo)" } diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt index 4143acf2..1d265bc0 100644 --- a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt @@ -9,28 +9,22 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.compose.AndroidFragment +import com.callstack.brownfield.android.example.components.GreetingCard +import com.callstack.brownfield.android.example.components.PostMessageCard import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme import com.callstack.reactnativebrownfield.ReactNativeFragment import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames @@ -81,57 +75,22 @@ private fun MainScreen(modifier: Modifier = Modifier) { horizontalAlignment = Alignment.CenterHorizontally // center top bar content ) { GreetingCard( - name = "Android", - modifier = Modifier.fillMaxWidth() + name = ReactNativeConstants.APP_NAME, ) + PostMessageCard() + + Spacer(modifier = Modifier.height(1.dp)) + ReactNativeView( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .clip(RoundedCornerShape(16.dp)) .background(MaterialTheme.colorScheme.surface) ) } } -@Composable -fun GreetingCard( - name: String, - modifier: Modifier = Modifier -) { - var counter by rememberSaveable { mutableStateOf(0) } - - Card( - modifier = modifier, - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "Hello native $name 👋", - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center - ) - - Text( - text = "You clicked the button $counter time${if (counter == 1) "" else "s"}", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium - ) - - Button(onClick = { counter++ }) { - Text("Increment counter") - } - } - } -} - @Composable fun ReactNativeView( modifier: Modifier = Modifier diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/GreetingCard.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/GreetingCard.kt new file mode 100644 index 00000000..06f4b81d --- /dev/null +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/GreetingCard.kt @@ -0,0 +1,51 @@ +package com.callstack.brownfield.android.example.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun GreetingCard( + name: String, +) { + var counter by rememberSaveable { mutableIntStateOf(0) } + + MaterialCard { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Hello native $name 👋", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center + ) + + Text( + text = "You clicked the button $counter time${if (counter == 1) "" else "s"}", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + + Button(onClick = { counter++ }) { + Text("Increment counter") + } + } + } +} \ No newline at end of file diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/MaterialCard.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/MaterialCard.kt new file mode 100644 index 00000000..2dc660e7 --- /dev/null +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/MaterialCard.kt @@ -0,0 +1,22 @@ +package com.callstack.brownfield.android.example.components + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun MaterialCard( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + content() + } +} diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/PostMessageCard.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/PostMessageCard.kt new file mode 100644 index 00000000..fe405e5d --- /dev/null +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/PostMessageCard.kt @@ -0,0 +1,96 @@ +package com.callstack.brownfield.android.example.components + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.callstack.reactnativebrownfield.OnMessageListener +import com.callstack.reactnativebrownfield.ReactNativeBrownfield +import org.json.JSONObject + +@Composable +fun PostMessageCard() { + var nextId by remember { mutableIntStateOf(0) } + var draft by remember { mutableStateOf("") } + val lastToast = remember { mutableStateOf(null) } + + val context = LocalContext.current + + DisposableEffect(Unit) { + val listener = OnMessageListener { raw -> + val text = try { + JSONObject(raw).optString("text", raw) + } catch (_: Exception) { + raw + } + val toast = Toast.makeText( + context, + "Received message from React Native: $text", + Toast.LENGTH_LONG + ) + lastToast.value?.cancel() // cancel previous toast if still visible + toast.show() + lastToast.value = toast + } + ReactNativeBrownfield.shared.addMessageListener(listener) + onDispose { ReactNativeBrownfield.shared.removeMessageListener(listener) } + } + + MaterialCard { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + "postMessage", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(bottom = 2.dp) + .align(Alignment.CenterHorizontally) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + + OutlinedTextField( + value = draft, + onValueChange = { draft = it }, + modifier = Modifier.weight(1f), + placeholder = { Text("Type a message...") }, + singleLine = true, + ) + Button(onClick = { + val text = draft.ifBlank { "Hello from Android! (#${nextId++})" } + val json = JSONObject().put("text", text).toString() + ReactNativeBrownfield.shared.postMessage(json) + draft = "" + }) { + Text("Send") + } + } + } + } +} diff --git a/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt b/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt index 2d9aba0b..a353f186 100644 --- a/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt +++ b/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt @@ -2,4 +2,5 @@ package com.callstack.brownfield.android.example object ReactNativeConstants { const val MAIN_MODULE_NAME = "RNApp" + const val APP_NAME = "Android" } diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj index 1667efc2..94258f6d 100644 --- a/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj +++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj @@ -7,14 +7,14 @@ objects = { /* Begin PBXBuildFile section */ - 798B42862F3E9A8B00DAD195 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CA2F3E93D90099076A /* BrownfieldLib.xcframework */; }; - 798B42872F3E9A8B00DAD195 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CA2F3E93D90099076A /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 798B42882F3E9A8D00DAD195 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CB2F3E93D90099076A /* Brownie.xcframework */; }; - 798B42892F3E9A8D00DAD195 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CB2F3E93D90099076A /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 798B428A2F3E9A8E00DAD195 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CC2F3E93D90099076A /* hermesvm.xcframework */; }; - 798B428B2F3E9A8E00DAD195 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CC2F3E93D90099076A /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 798B428C2F3E9A8F00DAD195 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CD2F3E93D90099076A /* ReactBrownfield.xcframework */; }; - 798B428D2F3E9A8F00DAD195 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79A7F5CD2F3E93D90099076A /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; }; + 7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; }; + 7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; }; + 7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; }; + 7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -24,10 +24,10 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 798B428B2F3E9A8E00DAD195 /* hermesvm.xcframework in Embed Frameworks */, - 798B428D2F3E9A8F00DAD195 /* ReactBrownfield.xcframework in Embed Frameworks */, - 798B42892F3E9A8D00DAD195 /* Brownie.xcframework in Embed Frameworks */, - 798B42872F3E9A8B00DAD195 /* BrownfieldLib.xcframework in Embed Frameworks */, + 7926B0E92F4E5A6800694E68 /* Brownie.xcframework in Embed Frameworks */, + 7926B0E52F4E5A6600694E68 /* hermesvm.xcframework in Embed Frameworks */, + 7926B0E72F4E5A6700694E68 /* ReactBrownfield.xcframework in Embed Frameworks */, + 7926B0E32F4E5A6400694E68 /* BrownfieldLib.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -35,11 +35,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = ""; }; + 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = ""; }; + 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = ""; }; + 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = ""; }; 793C76A72EEBF938008A2A34 /* Brownfield Apple App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Brownfield Apple App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 79A7F5CA2F3E93D90099076A /* BrownfieldLib.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BrownfieldLib.xcframework; path = package/BrownfieldLib.xcframework; sourceTree = ""; }; - 79A7F5CB2F3E93D90099076A /* Brownie.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Brownie.xcframework; path = package/Brownie.xcframework; sourceTree = ""; }; - 79A7F5CC2F3E93D90099076A /* hermesvm.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = hermesvm.xcframework; path = package/hermesvm.xcframework; sourceTree = ""; }; - 79A7F5CD2F3E93D90099076A /* ReactBrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ReactBrownfield.xcframework; path = package/ReactBrownfield.xcframework; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -55,10 +55,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 798B42882F3E9A8D00DAD195 /* Brownie.xcframework in Frameworks */, - 798B428C2F3E9A8F00DAD195 /* ReactBrownfield.xcframework in Frameworks */, - 798B42862F3E9A8B00DAD195 /* BrownfieldLib.xcframework in Frameworks */, - 798B428A2F3E9A8E00DAD195 /* hermesvm.xcframework in Frameworks */, + 7926B0E82F4E5A6800694E68 /* Brownie.xcframework in Frameworks */, + 7926B0E62F4E5A6700694E68 /* ReactBrownfield.xcframework in Frameworks */, + 7926B0E42F4E5A6600694E68 /* hermesvm.xcframework in Frameworks */, + 7926B0E22F4E5A6400694E68 /* BrownfieldLib.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -70,10 +70,10 @@ children = ( 793C76A92EEBF938008A2A34 /* Brownfield Apple App */, 793C76A82EEBF938008A2A34 /* Products */, - 79A7F5CA2F3E93D90099076A /* BrownfieldLib.xcframework */, - 79A7F5CB2F3E93D90099076A /* Brownie.xcframework */, - 79A7F5CC2F3E93D90099076A /* hermesvm.xcframework */, - 79A7F5CD2F3E93D90099076A /* ReactBrownfield.xcframework */, + 7926B0DA2F4E5A6100694E68 /* BrownfieldLib.xcframework */, + 7926B0DB2F4E5A6100694E68 /* Brownie.xcframework */, + 7926B0DC2F4E5A6100694E68 /* hermesvm.xcframework */, + 7926B0DD2F4E5A6100694E68 /* ReactBrownfield.xcframework */, ); sourceTree = ""; }; @@ -166,7 +166,7 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - 793C76B02EEBF939008A2A34 /* Debug */ = { + 793C76B02EEBF939008A2A34 /* Debug Expo */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -227,9 +227,9 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; - name = Debug; + name = "Debug Expo"; }; - 793C76B12EEBF939008A2A34 /* Release */ = { + 793C76B12EEBF939008A2A34 /* Release Expo */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -282,9 +282,9 @@ STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; }; - name = Release; + name = "Release Expo"; }; - 793C76B32EEBF939008A2A34 /* Debug */ = { + 793C76B32EEBF939008A2A34 /* Debug Expo */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -329,9 +329,9 @@ USE_EXPO_HOST_STATUS_BAR_APPEARANCE = NO; XROS_DEPLOYMENT_TARGET = 2.0; }; - name = Debug; + name = "Debug Expo"; }; - 793C76B42EEBF939008A2A34 /* Release */ = { + 793C76B42EEBF939008A2A34 /* Release Expo */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -376,7 +376,7 @@ USE_EXPO_HOST_STATUS_BAR_APPEARANCE = NO; XROS_DEPLOYMENT_TARGET = 2.0; }; - name = Release; + name = "Release Expo"; }; 7A1B2C3D4E5F60718293A401 /* Debug Vanilla */ = { isa = XCBuildConfiguration; @@ -596,24 +596,24 @@ 793C76A22EEBF938008A2A34 /* Build configuration list for PBXProject "Brownfield Apple App" */ = { isa = XCConfigurationList; buildConfigurations = ( - 793C76B02EEBF939008A2A34 /* Debug */, - 793C76B12EEBF939008A2A34 /* Release */, + 793C76B02EEBF939008A2A34 /* Debug Expo */, + 793C76B12EEBF939008A2A34 /* Release Expo */, 7A1B2C3D4E5F60718293A401 /* Debug Vanilla */, 7A1B2C3D4E5F60718293A402 /* Release Vanilla */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = "Release Expo"; }; 793C76B22EEBF939008A2A34 /* Build configuration list for PBXNativeTarget "Brownfield Apple App" */ = { isa = XCConfigurationList; buildConfigurations = ( - 793C76B32EEBF939008A2A34 /* Debug */, - 793C76B42EEBF939008A2A34 /* Release */, + 793C76B32EEBF939008A2A34 /* Debug Expo */, + 793C76B42EEBF939008A2A34 /* Release Expo */, 7A1B2C3D4E5F60718293A403 /* Debug Vanilla */, 7A1B2C3D4E5F60718293A404 /* Release Vanilla */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = "Release Expo"; }; /* End XCConfigurationList section */ }; diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App.xcscheme b/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Expo.xcscheme similarity index 100% rename from apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App.xcscheme rename to apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Expo.xcscheme diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist b/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist new file mode 100644 index 00000000..f49cfffc --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + Brownfield Apple App Expo.xcscheme + + orderHint + 1 + + Brownfield Apple App Vanilla.xcscheme_^#shared#^_ + + orderHint + 2 + + Brownfield iOS App.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + 793C76A62EEBF938008A2A34 + + primary + + + + + diff --git a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift index f4d35f78..f4eab6a8 100644 --- a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift +++ b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift @@ -1,36 +1,40 @@ import BrownfieldLib import Brownie -import SwiftUI import ReactBrownfield +import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { var window: UIWindow? - + func application( _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + didFinishLaunchingWithOptions launchOptions: [UIApplication + .LaunchOptionsKey: Any]? = nil ) -> Bool { - return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions) + return ReactNativeBrownfield.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) } } @main struct BrownfieldAppleApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - + init() { ReactNativeBrownfield.shared.bundle = ReactNativeBundle ReactNativeBrownfield.shared.startReactNative { print("React Native has been loaded") } -#if USE_EXPO_HOST - ReactNativeBrownfield.shared.ensureExpoModulesProvider() -#endif + #if USE_EXPO_HOST + ReactNativeBrownfield.shared.ensureExpoModulesProvider() + #endif BrownfieldStore.register(initialState) } - + var body: some Scene { WindowGroup { ContentView() diff --git a/apps/AppleApp/Brownfield Apple App/ContentView.swift b/apps/AppleApp/Brownfield Apple App/ContentView.swift deleted file mode 100644 index 4be7c44c..00000000 --- a/apps/AppleApp/Brownfield Apple App/ContentView.swift +++ /dev/null @@ -1,65 +0,0 @@ -import ReactBrownfield -import Brownie -import SwiftUI - -let initialState = BrownfieldStore( - counter: 0, - user: User(name: "Username") -) - -struct ContentView: View { - var body: some View { - NavigationView { - MainScreen() - .padding(16) - } - } -} - -struct MainScreen: View { - var body: some View { - VStack(spacing: 16) { - GreetingCard(name: "iOS") - - ReactNativeView(moduleName: "RNApp") - .navigationBarHidden(true) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .background(Color(UIColor.systemBackground)) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding() - } -} - -struct GreetingCard: View { - let name: String - @UseStore(\BrownfieldStore.counter) var counter - - var body: some View { - VStack(spacing: 12) { - Text("Hello native \(name) 👋") - .font(.title3) - .multilineTextAlignment(.center) - - Text( - "You clicked the button \(Int(counter)) time\(counter == 1 ? "" : "s")" - ) - .multilineTextAlignment(.center) - .font(.body) - - Button("Increment counter") { - $counter.set { $0 + 1 } - } - .buttonStyle(.borderedProminent) - } - .padding(16) - .frame(maxWidth: .infinity) - .background(Color(UIColor.secondarySystemBackground)) - .cornerRadius(16) - .shadow(radius: 4) - } -} - -#Preview { - ContentView() -} diff --git a/apps/AppleApp/Brownfield Apple App/components/ContentView.swift b/apps/AppleApp/Brownfield Apple App/components/ContentView.swift new file mode 100644 index 00000000..7f08507a --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App/components/ContentView.swift @@ -0,0 +1,39 @@ +import Brownie +import ReactBrownfield +import SwiftUI +import UIKit + +struct ChatMessage: Identifiable { + let id: Int + let text: String + let fromRN: Bool +} + +let initialState = BrownfieldStore( + counter: 0, + user: User(name: "Username") +) + +struct ContentView: View { + var body: some View { + NavigationView { + + VStack(spacing: 16) { + GreetingCard(name: "iOS Expo") + + MessagesView() + + ReactNativeView(moduleName: "main") + .navigationBarHidden(true) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .background(Color(UIColor.systemBackground)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(16) + } + } +} + +#Preview { + ContentView() +} diff --git a/apps/AppleApp/Brownfield Apple App/components/GreetingCard.swift b/apps/AppleApp/Brownfield Apple App/components/GreetingCard.swift new file mode 100644 index 00000000..8829bf7f --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App/components/GreetingCard.swift @@ -0,0 +1,26 @@ +import SwiftUI +import Brownie + +struct GreetingCard: View { + let name: String + @UseStore(\BrownfieldStore.counter) var counter + + var body: some View { + MaterialCard { + Text("Hello native \(name) 👋") + .font(.title3) + .multilineTextAlignment(.center) + + Text( + "You clicked the button \(Int(counter)) time\(counter == 1 ? "" : "s")" + ) + .multilineTextAlignment(.center) + .font(.body) + + Button("Increment counter") { + $counter.set { $0 + 1 } + } + .buttonStyle(.borderedProminent) + } + } +} diff --git a/apps/AppleApp/Brownfield Apple App/components/MaterialCard.swift b/apps/AppleApp/Brownfield Apple App/components/MaterialCard.swift new file mode 100644 index 00000000..d71cf28f --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App/components/MaterialCard.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct MaterialCard: View { + let children: Content + + init(@ViewBuilder children: () -> Content) { + self.children = children() + } + + var body: some View { + VStack(spacing: 12) { + children + } + .padding(16) + .frame(maxWidth: .infinity) + .background(Color(UIColor.secondarySystemBackground)) + .cornerRadius(16) + .shadow(radius: 4) + } +} diff --git a/apps/AppleApp/Brownfield Apple App/components/MessagesView.swift b/apps/AppleApp/Brownfield Apple App/components/MessagesView.swift new file mode 100644 index 00000000..dc94639c --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App/components/MessagesView.swift @@ -0,0 +1,62 @@ +import ReactBrownfield +import SwiftUI + +struct MessagesView: View { + @State private var draft: String = "" + @State private var nextId: Int = 0 + @State private var observer: NSObjectProtocol? + @State private var showToast = false + @State private var toastText = "" + + var body: some View { + MaterialCard { + Text("postMessage") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .center) + + HStack { + TextField("Type a message...", text: $draft) + .textFieldStyle(.roundedBorder) + + Button("Send") { + let text = + draft.isEmpty ? "Hello from iOS! (#\(nextId))" : draft + let json = "{\"text\":\"\(text)\"}" + ReactNativeBrownfield.shared.postMessage(json) + withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) + { + nextId += 1 + } + draft = "" + } + .buttonStyle(.borderedProminent) + } + } + .padding() + .onAppear { + observer = ReactNativeBrownfield.shared.onMessage { raw in + var text = raw + if let data = raw.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) + as? [String: Any], + let t = json["text"] as? String + { + text = t + } + toastText = text + showToast = true + } + } + .onDisappear { + if let obs = observer { + NotificationCenter.default.removeObserver(obs) + observer = nil + } + } + .overlay( + showToast + ? Toast(message: toastText, isShowing: $showToast) + .padding(.bottom, 50) : nil + ) + } +} diff --git a/apps/AppleApp/Brownfield Apple App/components/Toast.swift b/apps/AppleApp/Brownfield Apple App/components/Toast.swift new file mode 100644 index 00000000..974b1a9f --- /dev/null +++ b/apps/AppleApp/Brownfield Apple App/components/Toast.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct Toast: View { + let message: String + @Binding var isShowing: Bool + + @State private var scale: CGFloat = 0.5 + @State private var opacity: Double = 0.0 + + var body: some View { + if isShowing { + Text(message) + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background(Color.black.opacity(0.8)) + .cornerRadius(25) + .multilineTextAlignment(.center) + .scaleEffect(scale) + .opacity(opacity) + .onAppear { + // Scale-in bounce + withAnimation(.interpolatingSpring(stiffness: 300, damping: 15)) { + scale = 1.0 + opacity = 1.0 + } + + // Hide after 2 seconds with scale out + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation(.easeInOut(duration: 0.3)) { + scale = 0.5 + opacity = 0.0 + } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + isShowing = false + } + } + } + .transition(.scale.combined(with: .opacity)) + .padding(.bottom, 50) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + } + } +} diff --git a/apps/AppleApp/package.json b/apps/AppleApp/package.json index 6f135c69..a6d3aab9 100644 --- a/apps/AppleApp/package.json +++ b/apps/AppleApp/package.json @@ -4,9 +4,9 @@ "private": true, "type": "module", "scripts": { - "internal::build::common": "xcodebuild -project \"Brownfield Apple App.xcodeproj\" -scheme \"Brownfield Apple App\" -sdk iphonesimulator build CODE_SIGNING_ALLOWED=NO -derivedDataPath ./build", - "build:example:ios-consumer:expo": "node prepareXCFrameworks.js --appName ExpoApp && yarn internal::build::common -configuration Release", - "build:example:ios-consumer:vanilla": "node prepareXCFrameworks.js --appName RNApp && yarn internal::build::common -configuration \"Release Vanilla\"" + "internal::build::common": "xcodebuild -project \"Brownfield Apple App.xcodeproj\" -sdk iphonesimulator build CODE_SIGNING_ALLOWED=NO -derivedDataPath ./build", + "build:example:ios-consumer:expo": "node prepareXCFrameworks.js --appName ExpoApp && yarn internal::build::common -scheme \"Brownfield Apple App Expo\" -configuration \"Release Expo\"", + "build:example:ios-consumer:vanilla": "node prepareXCFrameworks.js --appName RNApp && yarn internal::build::common -scheme \"Brownfield Apple App Vanilla\" -configuration \"Release Vanilla\"" }, "devDependencies": { "@rock-js/tools": "^0.12.8" diff --git a/apps/AppleApp/prepareXCFrameworks.js b/apps/AppleApp/prepareXCFrameworks.js index ab8a6354..9c6a2be5 100644 --- a/apps/AppleApp/prepareXCFrameworks.js +++ b/apps/AppleApp/prepareXCFrameworks.js @@ -89,4 +89,44 @@ for (const file of fs.readdirSync(targetPackagePath)) { logger.success(`${file} prepared`); } +logger.info('Patching entrypoint name in ContentView.swift'); +const filePath = path.join( + __dirname, + 'Brownfield Apple App', + 'components', + 'ContentView.swift' +); +const contentViewFileContents = fs.readFileSync(filePath, 'utf8'); +const moduleNameRegex = /moduleName: ".*"/g; + +if (!contentViewFileContents.match(moduleNameRegex)) { + throw new Error('moduleName not found in ContentView.swift'); +} + +const isVanillaApp = appName === 'RNApp'; + +let updatedContentViewFileContents = contentViewFileContents.replace( + moduleNameRegex, + `moduleName: "${ + isVanillaApp ? 'RNApp' : 'main' // default to main for Expo apps + }"` +); + +logger.success(`Entrypoint name patched in ${filePath}`); + +logger.info('Patching GreetingCard name in ContentView.swift'); + +// replace GreetingCard(name: "...") with GreetingCard(name: "${appName}") +const greetingCardNameRegex = /GreetingCard\(name: ".*"/g; +if (!updatedContentViewFileContents.match(greetingCardNameRegex)) { + throw new Error('GreetingCard name not found in ContentView.swift'); +} + +updatedContentViewFileContents = updatedContentViewFileContents.replace( + greetingCardNameRegex, + `GreetingCard(name: "iOS ${isVanillaApp ? 'Vanilla' : 'Expo'}"` +); + +fs.writeFileSync(filePath, updatedContentViewFileContents); + outro(`Done!`); diff --git a/apps/ExpoApp/app/(tabs)/_layout.tsx b/apps/ExpoApp/app/(tabs)/_layout.tsx index cd8f4050..ce946ec7 100644 --- a/apps/ExpoApp/app/(tabs)/_layout.tsx +++ b/apps/ExpoApp/app/(tabs)/_layout.tsx @@ -35,6 +35,15 @@ export default function TabLayout() { ), }} /> + ( + + ), + }} + /> ); } diff --git a/apps/ExpoApp/app/(tabs)/index.tsx b/apps/ExpoApp/app/(tabs)/index.tsx index 7242543e..ba27c8fd 100644 --- a/apps/ExpoApp/app/(tabs)/index.tsx +++ b/apps/ExpoApp/app/(tabs)/index.tsx @@ -1,6 +1,6 @@ import { Image } from 'expo-image'; import { Link } from 'expo-router'; -import { Platform, StyleSheet } from 'react-native'; +import { Platform, StyleSheet, View } from 'react-native'; import { HelloWave } from '@/components/hello-wave'; import ParallaxScrollView from '@/components/parallax-scroll-view'; @@ -9,77 +9,79 @@ import { ThemedView } from '@/components/themed-view'; export default function HomeScreen() { return ( - - } - > - - Welcome! - - - - Step 1: Try it - - Edit{' '} - app/(tabs)/index.tsx{' '} - to see changes. Press{' '} - - {Platform.select({ - ios: 'cmd + d', - android: 'cmd + m', - web: 'F12', - })} - {' '} - to open developer tools. - - - - - - Step 2: Explore - - - - alert('Action pressed')} - /> - alert('Share pressed')} - /> - + + + } + > + + Welcome! + + + + Step 1: Try it + + Edit{' '} + app/(tabs)/index.tsx{' '} + to see changes. Press{' '} + + {Platform.select({ + ios: 'cmd + d', + android: 'cmd + m', + web: 'F12', + })} + {' '} + to open developer tools. + + + + + + Step 2: Explore + + + alert('Delete pressed')} + title="Action" + icon="cube" + onPress={() => alert('Action pressed')} /> + alert('Share pressed')} + /> + + alert('Delete pressed')} + /> + - - - - - Step 3: Get a fresh start - - {`When you're ready, run `} - - npm run reset-project - {' '} - to get a fresh app{' '} - directory. This will move the current{' '} - app to{' '} - app-example. - - - + + + + Step 3: Get a fresh start + + {`When you're ready, run `} + + npm run reset-project + {' '} + to get a fresh app{' '} + directory. This will move the current{' '} + app to{' '} + app-example. + + + + ); } diff --git a/apps/ExpoApp/app/(tabs)/postMessage.tsx b/apps/ExpoApp/app/(tabs)/postMessage.tsx new file mode 100644 index 00000000..4910c934 --- /dev/null +++ b/apps/ExpoApp/app/(tabs)/postMessage.tsx @@ -0,0 +1,105 @@ +import { StyleSheet, FlatList, TouchableOpacity } from 'react-native'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import ReactNativeBrownfield from '@callstack/react-native-brownfield'; +import type { MessageEvent } from '@callstack/react-native-brownfield'; + +import { ThemedView } from '@/components/themed-view'; +import { ThemedText } from '@/components/themed-text'; +import type { Message } from '@/components/postMessage/Message'; +import { MessageBubble } from '@/components/postMessage/MessageBubble'; + +export default function HomeScreen() { + const [messages, setMessages] = useState([]); + const flatListRef = useRef>(null); + + const messageCounterRef = useRef(0); + + useEffect(() => { + const sub = ReactNativeBrownfield.onMessage((event: MessageEvent) => { + const data = event.data as { text?: string }; + setMessages((prev) => [ + ...prev, + { + id: String(++messageCounterRef.current), + text: data?.text ?? JSON.stringify(event.data), + from: 'native', + timestamp: Date.now(), + }, + ]); + }); + return () => sub.remove(); + }, []); + + const sendMessage = useCallback(() => { + const msg = { + text: `Hello from Expo! (#${++messageCounterRef.current})`, + timestamp: Date.now(), + }; + ReactNativeBrownfield.postMessage(msg); + setMessages((prev) => [ + ...prev, + { + id: String(messageCounterRef.current), + text: msg.text, + from: 'rn', + timestamp: msg.timestamp, + }, + ]); + }, []); + + return ( + + + + Send message to Native + + + + `message-${item.id}`} + renderItem={({ item }) => } + style={styles.messageList} + contentContainerStyle={styles.messageListContent} + inverted={true} // ensure newest messages are at the top + onContentSizeChange={() => { + flatListRef.current?.scrollToEnd({ animated: true }); + }} + ref={flatListRef} + /> + + ); +} + +const styles = StyleSheet.create({ + messageSection: { + flex: 1, + width: '100%', + padding: 12, + paddingHorizontal: 20, + }, + sendButton: { + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 10, + alignItems: 'center', + marginBottom: 10, + backgroundColor: '#4F8EF7', + }, + sendButtonText: { + fontWeight: '700', + fontSize: 15, + color: '#fff', + }, + messageList: { + flex: 1, + }, + messageListContent: { + paddingBottom: 8, + }, +}); diff --git a/apps/ExpoApp/components/postMessage/Message.ts b/apps/ExpoApp/components/postMessage/Message.ts new file mode 100644 index 00000000..35e5f1d1 --- /dev/null +++ b/apps/ExpoApp/components/postMessage/Message.ts @@ -0,0 +1,6 @@ +export interface Message { + id: string; + text: string; + from: 'native' | 'rn'; + timestamp: number; +} diff --git a/apps/ExpoApp/components/postMessage/MessageBubble.tsx b/apps/ExpoApp/components/postMessage/MessageBubble.tsx new file mode 100644 index 00000000..b9b86102 --- /dev/null +++ b/apps/ExpoApp/components/postMessage/MessageBubble.tsx @@ -0,0 +1,74 @@ +import { Animated, StyleSheet } from 'react-native'; +import { Message } from './Message'; +import { useEffect, useRef } from 'react'; +import { ThemedText } from '@/components/themed-text'; + +export function MessageBubble({ item }: { item: Message }) { + const opacity = useRef(new Animated.Value(0)).current; + const translateY = useRef(new Animated.Value(20)).current; + + useEffect(() => { + Animated.parallel([ + Animated.timing(opacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.spring(translateY, { + toValue: 0, + tension: 80, + friction: 10, + useNativeDriver: true, + }), + ]).start(); + }, [opacity, translateY]); + + const isFromNative = item.from === 'native'; + + return ( + + + {isFromNative ? 'From Native' : 'From RN'} + + {item.text} + + ); +} + +const styles = StyleSheet.create({ + bubble: { + padding: 10, + borderRadius: 10, + borderWidth: 1, + marginBottom: 6, + }, + bubbleNative: { + alignSelf: 'flex-start', + backgroundColor: 'rgba(79, 142, 247, 0.1)', + maxWidth: '80%', + }, + bubbleRN: { + alignSelf: 'flex-end', + backgroundColor: 'rgba(218, 165, 32, 0.1)', + maxWidth: '80%', + }, + bubbleLabel: { + fontSize: 10, + fontWeight: '600', + marginBottom: 2, + textTransform: 'uppercase', + }, + bubbleText: { + fontSize: 14, + }, +}); diff --git a/apps/ExpoApp/components/ui/icon-symbol.tsx b/apps/ExpoApp/components/ui/icon-symbol.tsx index 5aa32001..440fab60 100644 --- a/apps/ExpoApp/components/ui/icon-symbol.tsx +++ b/apps/ExpoApp/components/ui/icon-symbol.tsx @@ -22,6 +22,7 @@ const MAPPING = { 'chevron.left.forwardslash.chevron.right': 'code', 'chevron.right': 'chevron-right', plus: 'add', + 'message.fill': 'message', } as IconMapping; /** diff --git a/apps/ExpoApp/package.json b/apps/ExpoApp/package.json index 4b2a56e8..9244f052 100644 --- a/apps/ExpoApp/package.json +++ b/apps/ExpoApp/package.json @@ -19,13 +19,13 @@ "@callstack/brownie": "workspace:^", "@callstack/react-native-brownfield": "workspace:^", "@expo/vector-icons": "^15.0.3", - "expo": "~54.0.31", + "expo": "~54.0.33", "expo-constants": "~18.0.13", - "expo-font": "~14.0.10", + "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linking": "~8.0.11", - "expo-router": "~6.0.21", + "expo-router": "~6.0.23", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-symbols": "~1.0.8", @@ -40,7 +40,7 @@ "react-native-worklets": "0.5.1" }, "devDependencies": { - "@types/react": "~19.1.0", + "@types/react": "~19.1.10", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", "typescript": "~5.9.2" diff --git a/apps/RNApp/ios/Podfile.lock b/apps/RNApp/ios/Podfile.lock index dd5593aa..73debef9 100644 --- a/apps/RNApp/ios/Podfile.lock +++ b/apps/RNApp/ios/Podfile.lock @@ -2772,7 +2772,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - Brownie: 981350e32e072e5b55b624eb8810ba9bbc9683d9 + Brownie: 914f0a636f17d04fa6a1cd1d057d1439d868918f DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa @@ -2788,68 +2788,68 @@ SPEC CHECKSUMS: React-Core: 956ac86b4d9b0c0fd9a14b9cc533aa297bb501c0 React-CoreModules: 3a8d39778cf9eeca40e419814e875da1a8e29855 React-cxxreact: db275765e1eb08f038599fb44114cf57ee0d18cd - React-debug: c8356d908286b1dc4cf90cd0977227dd61b7b1eb - React-defaultsnativemodule: df1a41d072194c96d0077dd30ee8d5d452397f26 - React-domnativemodule: 8abd63d26685a5c1c88c8ccc902876dc9c0e2d6f - React-Fabric: 1dea7e164181d7d688cfbd70a6e5f026e2df6bf5 - React-FabricComponents: 2a6f81481fa240a9239536402d72823f9d642925 - React-FabricImage: 513940cfd43193d3befb45dba9911f936bd74df7 - React-featureflags: bc1d980ff8356b931cd87c16700a39aaede1ed5a - React-featureflagsnativemodule: 4f7beedf0c241c44dcffc51e52a6178b5e0d541d - React-graphics: 69311413b44b6d228dbc705d8ce57ad0a4d55daf + React-debug: 1dfa1d1cbd93bdaffa3b140190829f9fd9e27985 + React-defaultsnativemodule: 35f353ba06901fb5e374bc56e750fde05cbb05b9 + React-domnativemodule: cf9e1b1b520ce0e66396c2744b3eb6d419711c13 + React-Fabric: c0b0c1ad70476d354b3da9fef96094f7b37804da + React-FabricComponents: 8c6861c5233cf0d5685cee301a979313090e2f57 + React-FabricImage: cef8883d2fb6c892003fefcad261d2898adbe926 + React-featureflags: 0e2b969019c2b118de64a6d4c55ef7c05f2b0f1d + React-featureflagsnativemodule: e1ef619d14fe0a68d4783b32293309dbb13ef2a5 + React-graphics: 0fc6b7acaff7161bda05bf8bffceacc2b0b4e38d React-hermes: b454b9352bc26e638704d103009f659a125b86d3 - React-idlecallbacksnativemodule: d15d469a152b7677d184a9538fae0744692e4575 - React-ImageManager: cce591e16cc6fa63ad5d45de012b4ddf31fd21e9 - React-jserrorhandler: 05fb248a535148a7eec94c786bd0e9e1413c6b3a + React-idlecallbacksnativemodule: 35ab292f8404c469744db5a5dd5f0f27f95e5ebf + React-ImageManager: 3312c550ebcf6b7d911d9993082adcb3e1407ce8 + React-jserrorhandler: 2a7f2d94566f05f8cb82288afd46bc0fd8b2ffc7 React-jsi: 7aa265cf8372d8385ccc7935729e76d27e694dfe React-jsiexecutor: 8dd53bebfb3bc12f0541282aa4c858a433914e37 - React-jsinspector: 0f62d1ffa7242033a1106f0af9f83ec12a381401 - React-jsinspectorcdp: 5ae22d48dcf03812cd4f8c4a6fd7c7204cd8789d - React-jsinspectornetwork: 9052eb6bbd876bfdafa1605874dd848511236844 - React-jsinspectortracing: 6d89a5caab7b86947607cf654fc94cf1c31f8330 - React-jsitooling: ecbd81f751b79ba748d4d0d54445da1b53e363fd - React-jsitracing: 8068734240da604902fead29287dc21b820bc7d3 + React-jsinspector: f89b9ae62a4e2f6035b452442ef20a7f98f9cb27 + React-jsinspectorcdp: 44e46c1473a8deecf7b188389ed409be83fb3cc7 + React-jsinspectornetwork: dc9524f6e3d7694b1b6f4bd22dedad8ccc2c0a80 + React-jsinspectortracing: 0166ebbdfb125936a5d231895de3c11a19521dfc + React-jsitooling: 34692514ec8d8735938eda3677808a58f41c925b + React-jsitracing: a598dae84a87f8013635d09c5e7884023bda8501 React-logger: 500f2fa5697d224e63c33d913c8a4765319e19bf - React-Mapbuffer: 4c50cf6af44286015a20a5995d5321f625c93459 - React-microtasksnativemodule: a84b9331106616ab1fa36de9ae555718d4bbdcf5 - react-native-safe-area-context: 0a3b034bb63a5b684dd2f5fffd3c90ef6ed41ee8 - React-NativeModulesApple: efd0906463c79d9b86197dbcf0d58358dff8c5ed + React-Mapbuffer: 06d59c448da7e34eb05b3fb2189e12f6a30fec57 + React-microtasksnativemodule: d1ee999dc9052e23f6488b730fa2d383a4ea40e5 + react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 + React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0 React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb React-perflogger: 2e229bf33e42c094fd64516d89ec1187a2b79b5b - React-performancecdpmetrics: fd9bbc52960c6aa008fdae263849eb14411ae13e - React-performancetimeline: 16eaea3f8be5d42eb3bf8a261d87df2fe7e6e111 + React-performancecdpmetrics: 05ba4bd83f36acf192071bb5d9c8f45faf04d140 + React-performancetimeline: bfc96fcd2b79f7489dd54e3df4cba186dd8dd141 React-RCTActionSheet: 2399bb6cc8adaef2e5850878102fea2ad1788a0e React-RCTAnimation: d1deb6946e83e22a795a7d0148b94faad8851644 React-RCTAppDelegate: 10b35d5cec3f8653f6de843ae800b3ba8050b801 React-RCTBlob: 85150378edc42862d7c13ff2502693f32b174f91 - React-RCTFabric: f57a14a48756480a7c96670d633cb39692eed453 - React-RCTFBReactNativeSpec: 725c3bb08b2f86741df136455960f2b58dd8f6e4 + React-RCTFabric: 736f9da3ad57e2cef5fa4c132999933a89bb8378 + React-RCTFBReactNativeSpec: 705ec584758966950a31fa235539b57523059837 React-RCTImage: bb6cbdc22698b3afc8eb8d81ef03ee840d24c6f6 React-RCTLinking: e8b006d101c45651925de3e82189f03449eedfe7 React-RCTNetwork: 7999731af05ec8f591cbc6ad4e29d79e209c581a - React-RCTRuntime: cdbbadadafcad5836fb0616073d7011c39c30ffd + React-RCTRuntime: 99d8a2a17747866fb972561cdb205afe9b26d369 React-RCTSettings: 839f334abb92e917bc24322036081ffe15c84086 React-RCTText: 272f60e9a5dbfd14c348c85881ee7d5c7749a67c React-RCTVibration: 1ffa30a21e2227be3afe28d657ac8e6616c91bae - React-rendererconsistency: a51dcbe4b3c1159413cfdb85abace6a5c871a4b3 - React-renderercss: 5fdc31a529021337e7eac6f1e9bf4410947b877e - React-rendererdebug: 8427d2e5d1b7e39971c9c59e55bbfcb7884a942f - React-RuntimeApple: 9ba3723a539ed1701b8ba08dc317f1255c269a37 - React-RuntimeCore: 61b10d50472e29cd1ec98aba797d0d8d4f325283 - React-runtimeexecutor: 8e5135a09dcb012a15a025dc514361c927ea5db9 - React-RuntimeHermes: f06c7288967d0209fc075e5eabd5e851580047e9 - React-runtimescheduler: bd92275b3a847c71d10210ae89a8e04dba076630 - React-timing: 91f11a6537770b698eb8152e4669012992710b27 - React-utils: f06ff240e06e2bd4b34e48f1b34cac00866e8979 - React-webperformancenativemodule: b3398f8175fa96d992c071b1fa59bd6f9646b840 + React-rendererconsistency: 3c3e198aba0255524ed7126aa812d22ce750d217 + React-renderercss: 6b3ce3dfadf991937ae3229112be843ef1438c32 + React-rendererdebug: baf9e1daa07ac7f9aca379555126d29f963ba38b + React-RuntimeApple: 4136aee89257894955ef09e9f9ef74f0c27596be + React-RuntimeCore: e9a743d7de4bbd741b16e10b26078d815d6513ab + React-runtimeexecutor: 781e292362066af82fa2478d95c6b0e374421844 + React-RuntimeHermes: 6ab3c2847516769fc860d711814f1735859cad74 + React-runtimescheduler: 824c83a5fd68b35396de6d4f2f9ae995daac861b + React-timing: 1ebc7102dd52a3edcc63534686bb156e12648411 + React-utils: abf37b162f560cd0e3e5d037af30bb796512246d + React-webperformancenativemodule: 50a57c713a90d27ae3ab947a6c9c8859bcb49709 ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176 - ReactBrownfield: 03a2fd2f61109c00810b8d82af6f8907095191ed - ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb - ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654 - RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13 + ReactBrownfield: 3137236180ff93a812c6303974607eddb58b183d + ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9 + ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424 + RNScreens: ffbb0296608eb3560de641a711bbdb663ed1f6b4 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 46ff53afcbeda2bae19c85b65e17487c3e3984dd + Yoga: 8e01cef9947ca77f0477a098f0b32848a8e448c6 PODFILE CHECKSUM: 7c116a16dd0744063c8c6293dbfc638c9d447c19 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj b/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj index 36a5015f..2d019141 100644 --- a/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj +++ b/apps/RNApp/ios/RNApp.xcodeproj/project.pbxproj @@ -8,14 +8,14 @@ /* Begin PBXBuildFile section */ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 4BDFB2DEC3FA2A466B4399CC /* libPods-RNApp-BrownfieldLib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 74384ED30C65F0559B7E3845 /* libPods-RNApp-BrownfieldLib.a */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 79BD1EE92EEBFB76003AA29F /* BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */; }; 79BD1EEA2EEBFB76003AA29F /* BrownfieldLib.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 79F35E8C2EEC1D4500E64860 /* BrownfieldLib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; - A19F59C8902E0F7A03F2A8C6 /* Pods_RNApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */; }; - BFB326E698C6BD239F8AEAE9 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */; }; C66C2A65406C527E9529D08F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; }; + EA23C7C5CE3406CF601EEF2A /* libPods-RNApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 368B138CEEDA7E1E2280A031 /* libPods-RNApp.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,15 +47,15 @@ 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = RNApp/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = RNApp/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = RNApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 368B138CEEDA7E1E2280A031 /* libPods-RNApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNApp.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B4392A12AC88292D35C810B /* Pods-RNApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.debug.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.debug.xcconfig"; sourceTree = ""; }; 5709B34CF0A7D63546082F79 /* Pods-RNApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp.release.xcconfig"; path = "Target Support Files/Pods-RNApp/Pods-RNApp.release.xcconfig"; sourceTree = ""; }; - 64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp_BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 74384ED30C65F0559B7E3845 /* libPods-RNApp-BrownfieldLib.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RNApp-BrownfieldLib.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = RNApp/AppDelegate.swift; sourceTree = ""; }; 79BD1EE32EEBFB76003AA29F /* BrownfieldLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BrownfieldLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79F35E8A2EEC1D4500E64860 /* BrownfieldLib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrownfieldLib.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = RNApp/LaunchScreen.storyboard; sourceTree = ""; }; 8A02E03D9F74B585B0A8F7F7 /* Pods-RNApp-BrownfieldLib.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.debug.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.debug.xcconfig"; sourceTree = ""; }; - B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RNApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D8C030F60E402FD6CFBB3904 /* Pods-RNApp-BrownfieldLib.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNApp-BrownfieldLib.release.xcconfig"; path = "Target Support Files/Pods-RNApp-BrownfieldLib/Pods-RNApp-BrownfieldLib.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -66,7 +66,7 @@ buildActionMask = 2147483647; files = ( 79BD1EE92EEBFB76003AA29F /* BrownfieldLib.framework in Frameworks */, - A19F59C8902E0F7A03F2A8C6 /* Pods_RNApp.framework in Frameworks */, + EA23C7C5CE3406CF601EEF2A /* libPods-RNApp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -74,7 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - BFB326E698C6BD239F8AEAE9 /* Pods_RNApp_BrownfieldLib.framework in Frameworks */, + 4BDFB2DEC3FA2A466B4399CC /* libPods-RNApp-BrownfieldLib.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -97,8 +97,8 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - B6CBC8DE61C44A9AB569A3CA /* Pods_RNApp.framework */, - 64481842743F90E340AD414A /* Pods_RNApp_BrownfieldLib.framework */, + 368B138CEEDA7E1E2280A031 /* libPods-RNApp.a */, + 74384ED30C65F0559B7E3845 /* libPods-RNApp-BrownfieldLib.a */, ); name = Frameworks; sourceTree = ""; diff --git a/apps/RNApp/src/HomeScreen.tsx b/apps/RNApp/src/HomeScreen.tsx index 99131782..75638c26 100644 --- a/apps/RNApp/src/HomeScreen.tsx +++ b/apps/RNApp/src/HomeScreen.tsx @@ -1,17 +1,79 @@ -import { useEffect } from 'react'; -import { StyleSheet, Text, View, Button } from 'react-native'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { + Animated, + Button, + FlatList, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import ReactNativeBrownfield from '@callstack/react-native-brownfield'; +import type { MessageEvent } from '@callstack/react-native-brownfield'; import { getRandomTheme } from './utils'; import type { RootStackParamList } from './navigation/RootStack'; import Counter from './components/counter'; +interface Message { + id: string; + text: string; + from: 'native' | 'rn'; + timestamp: number; +} + +function MessageBubble({ item, color }: { item: Message; color: string }) { + const opacity = useRef(new Animated.Value(0)).current; + const translateY = useRef(new Animated.Value(20)).current; + + useEffect(() => { + Animated.parallel([ + Animated.timing(opacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.spring(translateY, { + toValue: 0, + tension: 80, + friction: 10, + useNativeDriver: true, + }), + ]).start(); + }, [opacity, translateY]); + + const isFromNative = item.from === 'native'; + + return ( + + + {isFromNative ? 'From Native' : 'From RN'} + + {item.text} + + ); +} + +let messageCounter = 0; + export function HomeScreen({ navigation, route, }: NativeStackScreenProps) { const colors = route.params?.theme ? route.params.theme : getRandomTheme(); + const [messages, setMessages] = useState([]); + const flatListRef = useRef>(null); useEffect(() => { const unsubscribe = navigation.addListener('focus', () => { @@ -21,6 +83,39 @@ export function HomeScreen({ return unsubscribe; }, [navigation]); + useEffect(() => { + const sub = ReactNativeBrownfield.onMessage((event: MessageEvent) => { + const data = event.data as { text?: string }; + setMessages((prev) => [ + ...prev, + { + id: String(++messageCounter), + text: data?.text ?? JSON.stringify(event.data), + from: 'native', + timestamp: Date.now(), + }, + ]); + }); + return () => sub.remove(); + }, []); + + const sendMessage = useCallback(() => { + const msg = { + text: `Hello from React Native! (#${++messageCounter})`, + timestamp: Date.now(), + }; + ReactNativeBrownfield.postMessage(msg); + setMessages((prev) => [ + ...prev, + { + id: String(messageCounter), + text: msg.text, + from: 'rn', + timestamp: msg.timestamp, + }, + ]); + }, []); + return ( @@ -29,27 +124,53 @@ export function HomeScreen({ -