diff --git a/.changeset/bright-android-performance.md b/.changeset/bright-android-performance.md
new file mode 100644
index 000000000..ee3b3f652
--- /dev/null
+++ b/.changeset/bright-android-performance.md
@@ -0,0 +1,6 @@
+---
+'@callstack/react-native-brownfield': major
+---
+
+Add Android startup, TTID/TTFD, and JS/UI responsiveness metrics. Android's
+`OnJSBundleLoaded` callback now receives `JSBundleTimings` instead of a Boolean.
diff --git a/apps/AndroidApp/app/src/expo57/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt b/apps/AndroidApp/app/src/expo57/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
index 45f2d46d1..41351b71c 100644
--- a/apps/AndroidApp/app/src/expo57/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
+++ b/apps/AndroidApp/app/src/expo57/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
@@ -3,4 +3,5 @@ package com.callstack.brownfield.android.example
object ReactNativeConstants {
const val MAIN_MODULE_NAME = "main"
const val APP_NAME = "Android (Expo 57)"
+ const val WAIT_FOR_FULL_DISPLAY = true
}
diff --git a/apps/AndroidApp/app/src/expo58/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt b/apps/AndroidApp/app/src/expo58/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
index 184b3982e..c25e1529d 100644
--- a/apps/AndroidApp/app/src/expo58/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
+++ b/apps/AndroidApp/app/src/expo58/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
@@ -3,4 +3,5 @@ package com.callstack.brownfield.android.example
object ReactNativeConstants {
const val MAIN_MODULE_NAME = "main"
const val APP_NAME = "Android (Expo 58)"
+ const val WAIT_FOR_FULL_DISPLAY = true
}
diff --git a/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
index 61214dd58..96cff50e1 100644
--- a/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
+++ b/apps/AndroidApp/app/src/expopreview/java/com/callstack/brownfield/android/expo/ReactNativeConstants.kt
@@ -3,4 +3,5 @@ package com.callstack.brownfield.android.example
object ReactNativeConstants {
const val MAIN_MODULE_NAME = "main"
const val APP_NAME = "Android (Expo Preview)"
+ const val WAIT_FOR_FULL_DISPLAY = true
}
diff --git a/apps/AndroidApp/app/src/main/AndroidManifest.xml b/apps/AndroidApp/app/src/main/AndroidManifest.xml
index b7c0735e8..c6f1665df 100644
--- a/apps/AndroidApp/app/src/main/AndroidManifest.xml
+++ b/apps/AndroidApp/app/src/main/AndroidManifest.xml
@@ -18,6 +18,10 @@
android:name=".ReferralsActivity"
android:exported="false"
android:theme="@style/Theme.AndroidBrownfieldApp" />
+
"unavailable"
+ is Double -> format(value)
+ is Float -> format(value.toDouble())
+ is Long -> format(value.toDouble())
+ is Int -> format(value.toDouble())
+ is Number -> format(value.toDouble())
+ else -> value.toString()
+ }
+
+ private fun format(value: Double): String = "%.2f".format(value)
+}
diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/E2eTestIds.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/E2eTestIds.kt
index aa71ee4be..d9846cd7b 100644
--- a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/E2eTestIds.kt
+++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/E2eTestIds.kt
@@ -3,6 +3,7 @@ package com.callstack.brownfield.android.example
/** Keep in sync with `@callstack/brownfield-example-shared-tests` `e2eTestIds`. */
object E2eTestIds {
const val nativeAppGreeting = "brownfield-e2e-appleapp-greeting"
+ const val nativeAppOpenReactNative = "brownfield-e2e-appleapp-open-react-native"
const val nativeAppPostMessageSend = "brownfield-e2e-appleapp-post-message-send"
const val nativeAppPostMessageToast = "brownfield-e2e-appleapp-post-message-toast"
const val nativeAppNativeSettings = "brownfield-e2e-appleapp-native-settings"
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 fca8dd710..912856921 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
@@ -3,27 +3,23 @@ package com.callstack.brownfield.android.example
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
-import android.os.Build
import android.os.Bundle
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Button
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
@@ -31,26 +27,18 @@ import androidx.compose.runtime.remember
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.platform.LocalContext
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
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.EspressoTagAnchor
import com.callstack.brownfield.android.example.components.GreetingCard
import com.callstack.brownfield.android.example.components.PostMessageCard
import com.callstack.brownfield.android.example.components.PostMessageToast
import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
-import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationDelegate
-import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationManager
-import com.callstack.nativebrownfieldnavigation.UserType
-import com.callstack.reactnativebrownfield.ReactNativeFragment
-import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames
-import com.facebook.react.bridge.Callback
-import com.facebook.react.bridge.Promise
-
-class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
+
+class MainActivity : AppCompatActivity() {
private val isDetoxE2E: Boolean
get() = intent?.getStringExtra("DetoxE2E") == "YES"
@@ -62,19 +50,11 @@ class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
override fun onResume() {
super.onResume()
- // Own Brownfield navigation only while this activity is foregrounded.
- BrownfieldNavigationManager.setDelegate(this)
if (isDetoxE2E) {
window.decorView.post { window.decorView.requestFocus() }
}
}
- override fun onPause() {
- // Release ownership before another host can become the active delegate.
- BrownfieldNavigationManager.clearDelegate()
- super.onPause()
- }
-
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (!hasFocus && isDetoxE2E) {
@@ -87,7 +67,7 @@ class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
enableEdgeToEdge()
if (savedInstanceState == null) {
- ReactNativeHostManager.initialize(application) {
+ ReactNativeHostManager.initialize(application) { _ ->
if (!isDetoxE2E) {
Toast.makeText(
this,
@@ -115,40 +95,6 @@ class MainActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
}
}
}
-
- override fun navigateToSettings(user: UserType) {
- startActivity(Intent(this, SettingsActivity::class.java))
- }
-
- override fun navigateToReferrals(userId: String) {
- startActivity(
- Intent(this, ReferralsActivity::class.java).putExtra(
- ReferralsActivity.EXTRA_USER_ID,
- userId
- )
- )
- }
-
- override fun requestNativeConfirmation(title: String, promise: Promise) {
- runOnUiThread {
- AlertDialog.Builder(this)
- .setTitle(title)
- .setPositiveButton("OK") { _, _ -> promise.resolve(true) }
- .setNegativeButton("Cancel") { _, _ -> promise.resolve(false) }
- .setCancelable(false)
- .show()
- }
- }
-
- override fun showNativeBanner(message: String, onDismiss: Callback) {
- runOnUiThread {
- AlertDialog.Builder(this)
- .setMessage(message)
- .setPositiveButton("Dismiss") { _, _ -> onDismiss.invoke() }
- .setCancelable(false)
- .show()
- }
- }
}
@Composable
@@ -173,20 +119,28 @@ private fun MainScreen(modifier: Modifier = Modifier) {
name = ReactNativeConstants.APP_NAME,
)
+ EspressoTagAnchor(tag = E2eTestIds.nativeAppOpenReactNative) {
+ Button(
+ onClick = {
+ activity?.let {
+ it.startActivity(
+ Intent(it, ReactNativeActivity::class.java)
+ // Forward the Detox flag so the RN host keeps E2E behaviour.
+ .putExtra("DetoxE2E", it.intent?.getStringExtra("DetoxE2E"))
+ )
+ }
+ }
+ ) {
+ Text("Open React Native")
+ }
+ }
+
PostMessageCard(
onMessageReceived = { message -> postMessageToastText = message },
textInputEnabled = !isDetoxE2E,
)
- Spacer(modifier = Modifier.height(1.dp))
-
- ReactNativeView(
- modifier = Modifier
- .fillMaxWidth()
- .height(520.dp)
- .clip(RoundedCornerShape(16.dp))
- .background(MaterialTheme.colorScheme.surface)
- )
+ Spacer(modifier = Modifier.height(16.dp))
}
postMessageToastText?.let { message ->
@@ -198,36 +152,6 @@ private fun MainScreen(modifier: Modifier = Modifier) {
}
}
-@Composable
-fun ReactNativeView(
- modifier: Modifier = Modifier
-) {
- val activity = LocalContext.current as? Activity
- val brownfieldE2E = remember(activity) {
- activity?.intent?.getStringExtra("DetoxE2E") == "YES"
- }
-
- AndroidFragment(
- modifier = modifier,
- arguments = Bundle().apply {
- putString(
- ReactNativeFragmentArgNames.ARG_MODULE_NAME,
- ReactNativeConstants.MAIN_MODULE_NAME
- )
- putBundle(
- ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS,
- Bundle().apply {
- putString(
- "nativeOsVersionLabel",
- "Android ${Build.VERSION.RELEASE}"
- )
- putBoolean("brownfieldE2E", brownfieldE2E)
- }
- )
- }
- )
-}
-
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/ReactNativeActivity.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/ReactNativeActivity.kt
new file mode 100644
index 000000000..d942ef86b
--- /dev/null
+++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/ReactNativeActivity.kt
@@ -0,0 +1,171 @@
+package com.callstack.brownfield.android.example
+
+import android.app.Activity
+import android.content.Intent
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentFactory
+import androidx.fragment.compose.AndroidFragment
+import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme
+import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationDelegate
+import com.callstack.nativebrownfieldnavigation.BrownfieldNavigationManager
+import com.callstack.nativebrownfieldnavigation.UserType
+import com.callstack.reactnativebrownfield.OnDisplayMetrics
+import com.callstack.reactnativebrownfield.ReactNativeFragment
+import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames
+import com.facebook.react.bridge.Callback
+import com.facebook.react.bridge.Promise
+
+/**
+ * Full-screen React Native host, pushed from [MainActivity] (mirrors RNAppScreen on iOS).
+ * Each launch creates a new fragment, so every presentation gets its own display metrics.
+ */
+class ReactNativeActivity : AppCompatActivity(), BrownfieldNavigationDelegate {
+ private val isDetoxE2E: Boolean
+ get() = intent?.getStringExtra("DetoxE2E") == "YES"
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+
+ ReactNativeHostManager.onConfigurationChanged(application, newConfig)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ // Own Brownfield navigation only while this activity is foregrounded.
+ BrownfieldNavigationManager.setDelegate(this)
+ if (isDetoxE2E) {
+ window.decorView.post { window.decorView.requestFocus() }
+ }
+ }
+
+ override fun onPause() {
+ // Release ownership before another host can become the active delegate.
+ BrownfieldNavigationManager.clearDelegate()
+ super.onPause()
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ if (!hasFocus && isDetoxE2E) {
+ window.decorView.post { window.decorView.requestFocus() }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ supportFragmentManager.fragmentFactory = ReactNativeFragmentFactory()
+ super.onCreate(null)
+ enableEdgeToEdge()
+
+ setContent {
+ AndroidBrownfieldAppTheme {
+ ReactNativeView(
+ modifier = Modifier
+ .fillMaxSize()
+ .navigationBarsPadding()
+ .semantics { testTagsAsResourceId = true }
+ )
+ }
+ }
+ }
+
+ override fun navigateToSettings(user: UserType) {
+ startActivity(Intent(this, SettingsActivity::class.java))
+ }
+
+ override fun navigateToReferrals(userId: String) {
+ startActivity(
+ Intent(this, ReferralsActivity::class.java).putExtra(
+ ReferralsActivity.EXTRA_USER_ID,
+ userId
+ )
+ )
+ }
+
+ override fun requestNativeConfirmation(title: String, promise: Promise) {
+ runOnUiThread {
+ AlertDialog.Builder(this)
+ .setTitle(title)
+ .setPositiveButton("OK") { _, _ -> promise.resolve(true) }
+ .setNegativeButton("Cancel") { _, _ -> promise.resolve(false) }
+ .setCancelable(false)
+ .show()
+ }
+ }
+
+ override fun showNativeBanner(message: String, onDismiss: Callback) {
+ runOnUiThread {
+ AlertDialog.Builder(this)
+ .setMessage(message)
+ .setPositiveButton("Dismiss") { _, _ -> onDismiss.invoke() }
+ .setCancelable(false)
+ .show()
+ }
+ }
+}
+
+@Composable
+private fun ReactNativeView(
+ modifier: Modifier = Modifier
+) {
+ val activity = LocalContext.current as? Activity
+ val brownfieldE2E = remember(activity) {
+ activity?.intent?.getStringExtra("DetoxE2E") == "YES"
+ }
+
+ AndroidFragment(
+ modifier = modifier,
+ arguments = Bundle().apply {
+ putString(
+ ReactNativeFragmentArgNames.ARG_MODULE_NAME,
+ ReactNativeConstants.MAIN_MODULE_NAME
+ )
+ putBoolean(
+ ReactNativeFragmentArgNames.ARG_WAIT_FOR_FULL_DISPLAY,
+ ReactNativeConstants.WAIT_FOR_FULL_DISPLAY
+ )
+ putBoolean(
+ ReactNativeFragmentArgNames.ARG_COLLECT_THREAD_METRICS,
+ true
+ )
+ putBundle(
+ ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS,
+ Bundle().apply {
+ putString(
+ "nativeOsVersionLabel",
+ "Android ${Build.VERSION.RELEASE}"
+ )
+ putBoolean("brownfieldE2E", brownfieldE2E)
+ }
+ )
+ },
+ onUpdate = { fragment ->
+ fragment.onMetrics = OnDisplayMetrics(BrownfieldMetricsLogger::logDisplay)
+ }
+ )
+}
+
+private class ReactNativeFragmentFactory : FragmentFactory() {
+ override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+ if (className == ReactNativeFragment::class.java.name) {
+ return ReactNativeFragment().apply {
+ onMetrics = OnDisplayMetrics(BrownfieldMetricsLogger::logDisplay)
+ }
+ }
+ return super.instantiate(classLoader, className)
+ }
+}
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 a353f1868..4e51037de 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
@@ -3,4 +3,5 @@ package com.callstack.brownfield.android.example
object ReactNativeConstants {
const val MAIN_MODULE_NAME = "RNApp"
const val APP_NAME = "Android"
+ const val WAIT_FOR_FULL_DISPLAY = true
}
diff --git a/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Vanilla.xcscheme b/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Vanilla.xcscheme
index bb2231e3a..57b9711a3 100644
--- a/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Vanilla.xcscheme
+++ b/apps/AppleApp/Brownfield Apple App.xcodeproj/xcshareddata/xcschemes/Brownfield Apple App Vanilla.xcscheme
@@ -31,7 +31,7 @@
shouldAutocreateTestPlan = "YES">
String {
+ // busyRatio describes time beyond frame deadlines, not CPU utilization.
+ "fps=\(number(metrics.fps)) busyRatio=\(number(metrics.busyRatio)) sampledMs=\(String(format: "%.2f", metrics.sampledMs)) frames=\(metrics.frameCount)"
+ }
+
+ private static func number(_ value: NSNumber?) -> String {
+ guard let value else { return "unavailable" }
+ return String(format: "%.2f", value.doubleValue)
+ }
+}
diff --git a/apps/AppleApp/Brownfield Apple App/E2eTestIds.swift b/apps/AppleApp/Brownfield Apple App/E2eTestIds.swift
index c4fe83d8b..bcccc7f2b 100644
--- a/apps/AppleApp/Brownfield Apple App/E2eTestIds.swift
+++ b/apps/AppleApp/Brownfield Apple App/E2eTestIds.swift
@@ -3,6 +3,7 @@ import Foundation
/// Keep in sync with `@callstack/brownfield-example-shared-tests` `e2eTestIds`.
enum E2eTestIds {
static let appleAppGreeting = "brownfield-e2e-appleapp-greeting"
+ static let appleAppOpenReactNative = "brownfield-e2e-appleapp-open-react-native"
static let appleAppPostMessageSend = "brownfield-e2e-appleapp-post-message-send"
static let appleAppPostMessageToast = "brownfield-e2e-appleapp-post-message-toast"
static let appleAppNativeSettings = "brownfield-e2e-appleapp-native-settings"
diff --git a/apps/AppleApp/Brownfield Apple App/components/ContentView.swift b/apps/AppleApp/Brownfield Apple App/components/ContentView.swift
index 8a78d84f9..4ab83153c 100644
--- a/apps/AppleApp/Brownfield Apple App/components/ContentView.swift
+++ b/apps/AppleApp/Brownfield Apple App/components/ContentView.swift
@@ -1,7 +1,6 @@
import Brownie
import ReactBrownfield
import SwiftUI
-import UIKit
struct ChatMessage: Identifiable {
let id: Int
@@ -16,10 +15,8 @@ let initialState = BrownfieldStore(
#if USE_EXPO_HOST
private let hostAppName = "iOS Expo"
-private let reactNativeModuleName = "main"
#else
private let hostAppName = "iOS Vanilla"
-private let reactNativeModuleName = "RNApp"
#endif
private func brownfieldPostMessageText(from raw: String) -> String {
@@ -32,47 +29,37 @@ private func brownfieldPostMessageText(from raw: String) -> String {
return raw
}
-private var brownfieldInitialProperties: [String: Any] {
- [
- "nativeOsVersionLabel":
- "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
- "brownfieldE2E": ProcessInfo.processInfo.arguments.contains("-DetoxE2E"),
- ]
-}
-
struct ContentView: View {
@State private var messageObserver: NSObjectProtocol?
@State private var showPostMessageToast = false
@State private var postMessageToastText = ""
var body: some View {
- NavigationView {
- ZStack {
+ // Toast sits above NavigationView so RN postMessage stays visible on the pushed RN screen.
+ ZStack {
+ NavigationView {
ScrollView {
VStack(spacing: 16) {
GreetingCard(name: hostAppName)
- MessagesView()
+ NavigationLink(destination: RNAppScreen()) {
+ Text("Open React Native")
+ }
+ .buttonStyle(.borderedProminent)
+ .accessibilityIdentifier(E2eTestIds.appleAppOpenReactNative)
- ReactNativeView(
- moduleName: reactNativeModuleName,
- initialProperties: brownfieldInitialProperties
- )
- .navigationBarHidden(true)
- .clipShape(RoundedRectangle(cornerRadius: 16))
- .background(Color(UIColor.systemBackground))
- .frame(minHeight: 520)
+ MessagesView()
}
.frame(maxWidth: .infinity)
.padding(16)
}
+ }
- if showPostMessageToast {
- Toast(
- message: postMessageToastText,
- isShowing: $showPostMessageToast
- )
- }
+ if showPostMessageToast {
+ Toast(
+ message: postMessageToastText,
+ isShowing: $showPostMessageToast
+ )
}
}
.onAppear {
diff --git a/apps/AppleApp/Brownfield Apple App/components/RNAppScreen.swift b/apps/AppleApp/Brownfield Apple App/components/RNAppScreen.swift
new file mode 100644
index 000000000..217f2749d
--- /dev/null
+++ b/apps/AppleApp/Brownfield Apple App/components/RNAppScreen.swift
@@ -0,0 +1,34 @@
+import ReactBrownfield
+import SwiftUI
+import UIKit
+
+#if USE_EXPO_HOST
+private let reactNativeModuleName = "main"
+// The Expo example bundles do not yet send the JS full-display marker.
+private let waitForFullDisplay = false
+#else
+private let reactNativeModuleName = "RNApp"
+private let waitForFullDisplay = true
+#endif
+
+private var brownfieldInitialProperties: [String: Any] {
+ [
+ "nativeOsVersionLabel":
+ "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)",
+ "brownfieldE2E": ProcessInfo.processInfo.arguments.contains("-DetoxE2E"),
+ ]
+}
+
+struct RNAppScreen: View {
+ var body: some View {
+ ReactNativeView(
+ moduleName: reactNativeModuleName,
+ initialProperties: brownfieldInitialProperties,
+ waitForFullDisplay: waitForFullDisplay,
+ collectThreadMetrics: true,
+ onMetrics: BrownfieldMetricsLogger.logDisplay
+ )
+ .navigationBarHidden(true)
+ .ignoresSafeArea()
+ }
+}
diff --git a/apps/ExpoApp57/RNApp.tsx b/apps/ExpoApp57/RNApp.tsx
index 0ced41f70..54c987449 100644
--- a/apps/ExpoApp57/RNApp.tsx
+++ b/apps/ExpoApp57/RNApp.tsx
@@ -4,12 +4,20 @@ import BrownfieldNavigation from '@callstack/brownfield-navigation';
import { checkAndFetchUpdate } from './src/utils/expo-rn-updates';
import Counter from './src/components/counter';
+import ReactNativeBrownfield from '@callstack/react-native-brownfield';
+import { useMarkFullyDisplayed } from './src/hooks/use-mark-fully-displayed';
type RNAppProps = {
nativeOsVersionLabel?: string;
+ brownfieldPresentationID?: string;
};
-export default function RNApp({ nativeOsVersionLabel }: RNAppProps) {
+export default function RNApp({
+ nativeOsVersionLabel,
+ brownfieldPresentationID,
+}: RNAppProps) {
+ useMarkFullyDisplayed(brownfieldPresentationID);
+
const requestNativeConfirmation = async () => {
try {
await BrownfieldNavigation.requestNativeConfirmation(
@@ -57,17 +65,18 @@ export default function RNApp({ nativeOsVersionLabel }: RNAppProps) {
title="Navigate to Referrals"
onPress={() => BrownfieldNavigation.navigateToReferrals('123')}
/>
-
-
+
+