@callstack/react-native-brownfield 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </a>
4
4
 
5
5
  <p align="center">
6
- Set of helpers to make your brownfield integration smooth and easy.
6
+ A set of helpers to make your brownfield integration smooth and easy.
7
7
  </p>
8
8
 
9
9
  ---
@@ -21,86 +21,204 @@
21
21
 
22
22
  ## Features
23
23
 
24
- - **Easily integrate** React Native with existing native app
24
+ - **Easily integrate** React Native with an existing native app
25
25
  - Start React Native with **one method** and invoke code as soon as it's loaded
26
- - Compatible with **both old and new React Native architecture**!
26
+ - Compatible with **both legacy and new React Native architecture**!
27
27
  - Reuse the same instance of React Native between different components
28
28
  - Use predefined **native building blocks** - crafted for React Native
29
29
  - Disable and enable **native gestures and hardware buttons** from JavaScript
30
- - Works well with **any native navigation** pattern, as well as every React Native JavaScript based navigation
30
+ - Works well with **any native navigation** pattern, as well as any React Native JavaScript-based navigation
31
31
  - Compatible with all native languages **Objective-C**, **Swift**, **Java** and **Kotlin**
32
32
  - Supports UIKit and SwiftUI on iOS and Fragments and Jetpack Compose on Android
33
33
 
34
+ ## React Native version compatibility matrix
35
+
36
+ | Tested React Native Version | React Native Brownfield Version |
37
+ | --------------------------- | ------------------------------- |
38
+ | 0.81.x, 0.82.x | ^2.0.0-rc.0 |
39
+ | 0.78.x | ^1.2.0 |
34
40
 
35
41
  ## Installation
36
42
 
43
+ The React Native Brownfield library is intended to be installed in a React Native app that is later consumed as a framework artifact by your native iOS or Android app.
44
+
45
+ In your React Native project run:
46
+
37
47
  ```sh
38
48
  npm install @callstack/react-native-brownfield
39
49
  ```
40
50
 
41
- or
51
+ ## Usage
42
52
 
43
- ```sh
44
- yarn add @callstack/react-native-brownfield
53
+ <a href="https://www.callstack.com/ebooks/incremental-react-native-adoption-in-native-apps?utm_campaign=brownfield&utm_source=github&utm_medium=referral&utm_content=react-native-brownfield" align="center">
54
+ <img alt="Download a free copy of Incremental React Native adoption in native apps ebook" src="https://github.com/user-attachments/assets/ba42bb29-1e7a-4683-80c5-2602afb1a7e6">
55
+ </a>
56
+
57
+ ### Packaging React Native app as a framework
58
+
59
+ First, we need to package our React Native app as an XCFramework or Fat-AAR.
60
+
61
+ #### With Rock
62
+
63
+ Follow [Integrating with Native Apps](https://www.rockjs.dev/docs/brownfield/intro) steps in Rock docs and run:
64
+
65
+ - `rock package:ios` for iOS
66
+ - `rock package:aar` for Android
67
+
68
+ #### With custom scripts
69
+
70
+ Instead of using Rock, you can create your own custom packaging scripts. Here are base versions for iOS and Android that you'll need to adjust for your project-specific setup:
71
+
72
+ - [Example iOS script](https://github.com/callstackincubator/modern-brownfield-ref/blob/main/scripts/build-xcframework.sh)
73
+ - [Example Android script](https://github.com/callstackincubator/modern-brownfield-ref/blob/main/scripts/build-aar.sh)
74
+
75
+ ### Native iOS app
76
+
77
+ In your native iOS app, initialize React Native and display it where you like. For example, to display React Native views in SwiftUI, use the provided `ReactNativeView` component:
78
+
79
+ ```swift
80
+ import SwiftUI
81
+ import ReactBrownfield # exposed by RN app framework
82
+
83
+ @main
84
+ struct MyApp: App {
85
+ init() {
86
+ ReactNativeBrownfield.shared.startReactNative {
87
+ print("React Native bundle loaded")
88
+ }
89
+ }
90
+
91
+ var body: some Scene {
92
+ WindowGroup {
93
+ ContentView()
94
+ }
95
+ }
96
+ }
97
+
98
+ struct ContentView: View {
99
+ var body: some View {
100
+ NavigationView {
101
+ VStack {
102
+ Text("Welcome to the Native App")
103
+ .padding()
104
+
105
+ NavigationLink("Push React Native Screen") {
106
+ ReactNativeView(moduleName: "ReactNative")
107
+ .navigationBarHidden(true)
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
45
113
  ```
46
114
 
47
- ## Enabling New Architecture
115
+ For more detailed instructions and API for iOS, see docs for:
116
+
117
+ - [Objective C](docs/OBJECTIVE_C.md)
118
+ - [Swift](docs/SWIFT.md)
119
+
120
+ ### Native Android app
48
121
 
49
- ### Android
50
- Add the following to your `android/gradle.properties`:
122
+ In your native Android app, create a new `RNAppFragment.kt`:
51
123
 
124
+ ```kt
125
+
126
+ import android.os.Bundle
127
+ import android.view.LayoutInflater
128
+ import android.view.View
129
+ import android.view.ViewGroup
130
+ import androidx.fragment.app.Fragment
131
+
132
+ class RNAppFragment : Fragment() {
133
+ override fun onCreateView(
134
+ inflater: LayoutInflater,
135
+ container: ViewGroup?,
136
+ savedInstanceState: Bundle?,
137
+ ): View? = ReactNativeBrownfield.shared.createView(activity, "BrownFieldTest")
138
+ }
52
139
  ```
53
- # Enable new architecture
54
- newArchEnabled=true
140
+
141
+ Add a button to your `activity_main.xml`:
142
+
143
+ ```xml
144
+ <Button
145
+ android:id="@+id/show_rn_app_btn"
146
+ android:layout_width="wrap_content"
147
+ android:layout_height="wrap_content"
148
+ android:text="Show RN App"
149
+ app:layout_constraintBottom_toBottomOf="parent"
150
+ app:layout_constraintEnd_toEndOf="parent"
151
+ app:layout_constraintStart_toStartOf="parent"
152
+ app:layout_constraintTop_toTopOf="parent" />
55
153
  ```
56
154
 
57
- ### iOS
58
- Install cocoapods with the flag:
155
+ Add a fragment container:
59
156
 
60
- ```
61
- RCT_NEW_ARCH_ENABLED=1 pod install
157
+ ```xml
158
+ <FrameLayout
159
+ android:id="@+id/fragmentContainer"
160
+ android:layout_width="match_parent"
161
+ android:layout_height="match_parent" />
62
162
  ```
63
163
 
64
- > [!NOTE]
65
- > New Architecture is enabled by default from React Native 0.76
164
+ Update your `MainActivity` to initialize React Native and show the fragment:
66
165
 
67
- ## Usage
166
+ ```kt
167
+ class MainActivity : AppCompatActivity() {
168
+ private lateinit var showRNAppBtn: Button
169
+
170
+ override fun onCreate(savedInstanceState: Bundle?) {
171
+ super.onCreate(savedInstanceState)
172
+ ReactNativeHostManager.shared.initialize(this.application) {
173
+ println("JS bundle loaded")
174
+ }
175
+
176
+ showRNAppBtn = findViewById(R.id.show_rn_app_btn)
177
+ showRNAppBtn.setOnClickListener {
178
+ supportFragmentManager
179
+ .beginTransaction()
180
+ .replace(R.id.fragmentContainer, RNAppFragment())
181
+ .commit()
182
+ }
183
+ }
184
+
185
+ }
186
+ ```
68
187
 
69
- React Native Brownfield library works with all major native programming languages. Majority of its API is exposed on the native side. Click on the logo to choose the one that interests you:
188
+ For more detailed instructions and API for Android, see docs for:
70
189
 
71
- | [<img src="https://user-images.githubusercontent.com/7837457/63374769-cafd1e80-c38a-11e9-9724-e797a199ebab.png" width="100px;" alt="Objective-C"/><br /><sub><b>Objective-C</b></sub>](docs/OBJECTIVE_C.md) | [<img src="https://user-images.githubusercontent.com/7837457/63374778-ce90a580-c38a-11e9-8f37-72a9a5b9a52f.png" width="100px;" alt="Swift"/><br /><sub><b>Swift</b></sub>](docs/SWIFT.md) | [<img src="https://user-images.githubusercontent.com/7837457/63374794-d2bcc300-c38a-11e9-9a7f-d538563b75db.png" width="100px;" alt="Java"/><br /><sub><b>Java</b></sub>](docs/JAVA.md) | [<img src="https://user-images.githubusercontent.com/7837457/63374783-d0f2ff80-c38a-11e9-9790-041cad53b259.png" width="100px;" alt="Kotlin"/><br /><sub><b>Kotlin</b></sub>](docs/KOTLIN.md) |
72
- | :---: | :---: | :---: | :---: |
190
+ - [Java](docs/JAVA.md)
191
+ - [Kotlin](docs/KOTLIN.md)
73
192
 
74
193
  ### JavaScript Module
75
194
 
76
- Besides native components, we are exposing JavaScript functions to control the behavior of those components.
195
+ Besides native components, we are exposing JavaScript functions to control the behavior of those components from React Native app.
77
196
 
78
- To use the module, simply import it:
197
+ To use the module, import it:
79
198
 
80
199
  ```js
81
200
  import ReactNativeBrownfield from '@callstack/react-native-brownfield';
82
201
  ```
83
202
 
84
- ### JavaScript API Reference:
203
+ and use the available methods:
85
204
 
86
- **setNativeBackGestureAndButtonEnabled(enabled: boolean)**
205
+ #### setNativeBackGestureAndButtonEnabled(enabled: boolean)
87
206
 
88
- A method used to toggle iOS native back gesture and Android hardware back button.
207
+ A method used to toggle iOS native back gesture and Android hardware back button.
89
208
 
90
- ```js
209
+ ```ts
91
210
  ReactNativeBrownfield.setNativeBackGestureAndButtonEnabled(true);
92
211
  ```
93
212
 
94
- **popToNative(animated[iOS only]: boolean)**
213
+ #### popToNative(animated[iOS only]: boolean)
95
214
 
96
- A method to pop to native screen used to push React Native experience.
215
+ A method to pop to native screen used to push React Native experience.
97
216
 
98
- ```js
217
+ ```ts
99
218
  ReactNativeBrownfield.popToNative(true);
100
219
  ```
101
220
 
102
- > NOTE: Those methods works only with native components provided by this library.
103
-
221
+ > **Note:** These methods work only with native components provided by this library.
104
222
 
105
223
  ## Made with ❤️ at Callstack
106
224
 
@@ -115,13 +233,14 @@ Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds
115
233
  <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
116
234
  <!-- prettier-ignore -->
117
235
  | [<img src="https://avatars0.githubusercontent.com/u/7837457?s=460&v=4" width="100px;" alt="Michał Chudziak"/><br /><sub><b>Michał Chudziak</b></sub>](https://twitter.com/michalchudziak)<br />[💻](https://github.com/callstack/react-native-brownfield/commits?author=michalchudziak "Code") [📖](https://github.com/callstack/react-native-brownfield/commits?author=michalchudziak "Documentation") [🤔](#ideas-michalchudziak "Ideas, Planning, & Feedback") | [<img src="https://avatars1.githubusercontent.com/u/16336501?s=400&v=4" width="100px;" alt="Piotr Drapich"/><br /><sub><b>Piotr Drapich</b></sub>](https://twitter.com/dratwas)<br />[💻](https://github.com/callstack/react-native-brownfield/commits?author=dratwas "Code") [🤔](#ideas-dratwas "Ideas, Planning, & Feedback") |
118
- | :---: | :---: |
236
+ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
119
237
 
120
238
  <!-- ALL-CONTRIBUTORS-LIST:END -->
121
239
 
122
240
  This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
123
241
 
124
242
  <!-- badges -->
243
+
125
244
  [build-badge]: https://img.shields.io/circleci/build/github/callstack/react-native-brownfield/master.svg?style=flat-square
126
245
  [build]: https://circleci.com/gh/callstack/react-native-brownfield
127
246
  [version-badge]: https://img.shields.io/npm/v/@callstack/react-native-brownfield.svg?style=flat-square
@@ -1,3 +1,15 @@
1
+ import groovy.json.JsonSlurper
2
+
3
+ def reactNativeVersion = null
4
+ def packageJsonFile = rootProject.file("../node_modules/react-native/package.json")
5
+
6
+ if (packageJsonFile.exists()) {
7
+ def json = new JsonSlurper().parse(packageJsonFile)
8
+ reactNativeVersion = json.version
9
+ } else {
10
+ reactNativeVersion = "unknown"
11
+ }
12
+
1
13
  buildscript {
2
14
  // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3
15
  def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["RNBrownfield_kotlinVersion"]
@@ -64,6 +76,7 @@ android {
64
76
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
65
77
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
66
78
  buildConfigField "boolean", "IS_HERMES_ENABLED", isHermesEnabled().toString()
79
+ buildConfigField "String", "RN_VERSION", "\"$reactNativeVersion\""
67
80
  }
68
81
 
69
82
  buildFeatures {
@@ -0,0 +1,36 @@
1
+ package com.callstack.reactnativebrownfield
2
+
3
+ import android.os.Bundle
4
+ import androidx.activity.ComponentActivity
5
+ import com.facebook.react.ReactDelegate
6
+ import com.facebook.react.ReactHost
7
+ import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
8
+
9
+ class ReactDelegateWrapper(
10
+ private val activity: ComponentActivity?,
11
+ resolvedReactHost: ReactHost?,
12
+ moduleName: String,
13
+ launchOptions: Bundle?
14
+ ) : ReactDelegate(
15
+ activity = activity!!,
16
+ resolvedReactHost,
17
+ appKey = moduleName,
18
+ launchOptions = launchOptions,
19
+ ) {
20
+ private lateinit var hardwareBackHandler: () -> Unit
21
+ private val backBtnHandler = DefaultHardwareBackBtnHandler {
22
+ hardwareBackHandler()
23
+ }
24
+
25
+ /**
26
+ * This is invoked when there is no more RN Stack to pop.
27
+ * What it means that this is now the initial RN screen.
28
+ */
29
+ fun setHardwareBackHandler(backHandler: () -> Unit) {
30
+ hardwareBackHandler = backHandler
31
+ }
32
+
33
+ fun onReactHostResume() {
34
+ super.reactHost?.onHostResume(activity, backBtnHandler)
35
+ }
36
+ }
@@ -1,141 +1,168 @@
1
1
  package com.callstack.reactnativebrownfield
2
2
 
3
3
  import android.app.Application
4
- import android.content.Context
5
4
  import android.os.Bundle
6
5
  import android.widget.FrameLayout
6
+ import androidx.activity.OnBackPressedCallback
7
7
  import androidx.fragment.app.FragmentActivity
8
8
  import androidx.lifecycle.DefaultLifecycleObserver
9
9
  import androidx.lifecycle.LifecycleOwner
10
- import com.facebook.react.ReactDelegate
10
+ import com.callstack.reactnativebrownfield.utils.VersionUtils
11
+ import com.facebook.react.ReactHost
11
12
  import com.facebook.react.ReactInstanceEventListener
12
- import com.facebook.react.ReactInstanceManager
13
- import com.facebook.react.ReactNativeHost
14
13
  import com.facebook.react.ReactPackage
15
- import com.facebook.react.ReactRootView
16
14
  import com.facebook.react.bridge.ReactContext
17
- import com.facebook.react.defaults.DefaultReactNativeHost
15
+ import com.facebook.react.common.build.ReactBuildConfig
16
+ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
17
+ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
18
18
  import com.facebook.react.soloader.OpenSourceMergedSoMapping
19
19
  import com.facebook.soloader.SoLoader
20
20
  import java.util.concurrent.atomic.AtomicBoolean
21
- import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
22
- import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
23
21
 
24
- interface InitializedCallback {
25
- operator fun invoke(initialized: Boolean)
22
+ fun interface OnJSBundleLoaded {
23
+ operator fun invoke(initialized: Boolean)
26
24
  }
27
25
 
28
- class ReactNativeBrownfield private constructor(val reactNativeHost: ReactNativeHost) {
29
- companion object {
30
- private lateinit var instance: ReactNativeBrownfield
31
- private val initialized = AtomicBoolean()
32
-
33
- @JvmStatic
34
- val shared: ReactNativeBrownfield get() = instance
26
+ /**
27
+ * The threshold RN version based on which we decide whether to
28
+ * load JNI libs or not. We only load JNI libs on version less
29
+ * than this.
30
+ */
31
+ private const val RN_THRESHOLD_VERSION = "0.80.0"
35
32
 
36
- @JvmStatic
37
- fun initialize(application: Application, rnHost: ReactNativeHost) {
38
- if (!initialized.getAndSet(true)) {
39
- instance = ReactNativeBrownfield(rnHost)
40
- SoLoader.init(application.applicationContext, OpenSourceMergedSoMapping)
41
- }
42
- }
33
+ class ReactNativeBrownfield private constructor(val reactHost: ReactHost) {
34
+ companion object {
35
+ private lateinit var instance: ReactNativeBrownfield
36
+ private val initialized = AtomicBoolean()
43
37
 
44
- @JvmStatic
45
- fun initialize(application: Application, options: HashMap<String, Any>) {
46
- val reactNativeHost: ReactNativeHost =
47
- object : DefaultReactNativeHost(application) {
38
+ @JvmStatic
39
+ val shared: ReactNativeBrownfield get() = instance
48
40
 
49
- override fun getJSMainModuleName(): String {
50
- return options["mainModuleName"] as? String ?: super.getJSMainModuleName()
51
- }
41
+ private fun loadNativeLibs(application: Application) {
42
+ val rnVersion = BuildConfig.RN_VERSION
52
43
 
53
- override fun getPackages(): List<ReactPackage> {
54
- return (options["packages"] as? List<*> ?: emptyList<ReactPackage>())
55
- .filterIsInstance<ReactPackage>()
56
- }
44
+ if (VersionUtils.isVersionLessThan(rnVersion, RN_THRESHOLD_VERSION)) {
45
+ SoLoader.init(application.applicationContext, OpenSourceMergedSoMapping)
46
+ load()
47
+ }
48
+ }
57
49
 
58
- override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
50
+ @JvmStatic
51
+ @JvmOverloads
52
+ fun initialize(
53
+ application: Application,
54
+ reactHost: ReactHost,
55
+ onJSBundleLoaded: OnJSBundleLoaded? = null
56
+ ) {
57
+ if (!initialized.getAndSet(true)) {
58
+ loadNativeLibs(application)
59
+ instance = ReactNativeBrownfield(reactHost)
60
+
61
+ preloadReactNative {
62
+ onJSBundleLoaded?.invoke(true)
63
+ }
64
+ }
65
+ }
59
66
 
60
- override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
61
- override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
67
+ @JvmStatic
68
+ @JvmOverloads
69
+ fun initialize(
70
+ application: Application,
71
+ options: HashMap<String, Any>,
72
+ onJSBundleLoaded: OnJSBundleLoaded? = null
73
+ ) {
74
+ val reactHost: ReactHost by lazy {
75
+ getDefaultReactHost(
76
+ context = application,
77
+ packageList = (options["packages"] as? List<*> ?: emptyList<ReactPackage>())
78
+ .filterIsInstance<ReactPackage>(),
79
+ jsMainModulePath = options["mainModuleName"] as? String ?: "index",
80
+ useDevSupport = options["useDeveloperSupport"] as? Boolean
81
+ ?: ReactBuildConfig.DEBUG,
82
+ jsRuntimeFactory = null
83
+ )
84
+ }
85
+
86
+ initialize(application, reactHost, onJSBundleLoaded)
62
87
  }
63
88
 
64
- initialize(application, reactNativeHost)
65
- }
89
+ @JvmStatic
90
+ @JvmOverloads
91
+ fun initialize(
92
+ application: Application,
93
+ packages: List<ReactPackage>,
94
+ onJSBundleLoaded: OnJSBundleLoaded? = null
95
+ ) {
96
+ val options = hashMapOf("packages" to packages, "mainModuleName" to "index")
66
97
 
67
- @JvmStatic
68
- fun initialize(application: Application, packages: List<ReactPackage>) {
69
- val options = hashMapOf("packages" to packages, "mainModuleName" to "index")
98
+ initialize(application, options, onJSBundleLoaded)
99
+ }
70
100
 
71
- initialize(application, options)
101
+ private fun preloadReactNative(callback: ((Boolean) -> Unit)) {
102
+ shared.reactHost.addReactInstanceEventListener(object :
103
+ ReactInstanceEventListener {
104
+ override fun onReactContextInitialized(context: ReactContext) {
105
+ callback(true)
106
+ shared.reactHost.removeReactInstanceEventListener(this)
107
+ }
108
+ })
109
+ shared.reactHost.start()
110
+ }
72
111
  }
73
112
 
74
-
75
- }
76
-
77
- fun startReactNative(callback: InitializedCallback?) {
78
- startReactNative { callback?.invoke(it) }
79
- }
80
-
81
- @JvmName("startReactNativeKotlin")
82
- fun startReactNative(callback: ((initialized: Boolean) -> Unit)?) {
83
- reactNativeHost.reactInstanceManager.addReactInstanceEventListener(object :
84
- ReactInstanceEventListener {
85
- override fun onReactContextInitialized(reactContext: ReactContext) {
86
- callback?.let { it(true) }
87
- reactNativeHost.reactInstanceManager.removeReactInstanceEventListener(this)
88
- }
89
- })
90
- reactNativeHost.reactInstanceManager?.createReactContextInBackground()
91
-
92
- if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
93
- // If you opted-in for the New Architecture, we load the native entry point for this app.
94
- load()
95
- }
96
- }
97
-
98
- fun createView(
99
- context: Context,
100
- activity: FragmentActivity?,
101
- moduleName: String,
102
- launchOptions: Bundle? = null,
103
- ): FrameLayout {
104
- if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
105
- val reactHost = getDefaultReactHost(
106
- context,
107
- shared.reactNativeHost
108
- )
109
- val reactDelegate = ReactDelegate(activity, reactHost, moduleName, launchOptions)
110
-
111
- activity?.lifecycle?.addObserver(object : DefaultLifecycleObserver {
112
- override fun onResume(owner: LifecycleOwner) {
113
- reactDelegate.onHostResume()
113
+ fun createView(
114
+ activity: FragmentActivity?,
115
+ moduleName: String,
116
+ reactDelegate: ReactDelegateWrapper? = null,
117
+ launchOptions: Bundle? = null,
118
+ ): FrameLayout {
119
+ val reactHost = shared.reactHost
120
+ val resolvedDelegate =
121
+ reactDelegate ?: ReactDelegateWrapper(activity, reactHost, moduleName, launchOptions)
122
+
123
+ val mBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
124
+ override fun handleOnBackPressed() {
125
+ // invoked for JS stack back navigation
126
+ resolvedDelegate.onBackPressed()
127
+ }
114
128
  }
115
129
 
116
- override fun onPause(owner: LifecycleOwner) {
117
- reactDelegate.onHostPause()
130
+ // Register back press callback
131
+ activity?.onBackPressedDispatcher?.addCallback(mBackPressedCallback)
132
+ // invoked on the last RN screen exit
133
+ resolvedDelegate.setHardwareBackHandler {
134
+ mBackPressedCallback.isEnabled = false
135
+ activity?.onBackPressedDispatcher?.onBackPressed()
118
136
  }
119
137
 
120
- override fun onDestroy(owner: LifecycleOwner) {
121
- reactDelegate.onHostDestroy()
122
- owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks
138
+ /**
139
+ * When createView method is called in ReactNativeFragment, a reactDelegate
140
+ * instance is required. In such a case, we use the lifeCycle events of the fragment.
141
+ * When createView method is called elsewhere, then reactDelegate is not required.
142
+ * In such a case, we set the lifeCycle observer.
143
+ */
144
+ if (reactDelegate == null) {
145
+ activity?.lifecycle?.addObserver(getLifeCycleObserver(resolvedDelegate))
123
146
  }
124
- })
125
147
 
126
- reactDelegate.loadApp()
127
- return reactDelegate.reactRootView!!
148
+ resolvedDelegate.loadApp()
149
+ return resolvedDelegate.reactRootView!!
128
150
  }
129
151
 
130
- val instanceManager: ReactInstanceManager? = shared.reactNativeHost?.reactInstanceManager
131
- val reactView = ReactRootView(context)
132
- reactView.startReactApplication(
133
- instanceManager,
134
- moduleName,
135
- launchOptions,
136
- )
152
+ private fun getLifeCycleObserver(reactDelegate: ReactDelegateWrapper): DefaultLifecycleObserver {
153
+ return object : DefaultLifecycleObserver {
154
+ override fun onResume(owner: LifecycleOwner) {
155
+ reactDelegate.onReactHostResume()
156
+ }
137
157
 
138
- return reactView
139
- }
140
- }
158
+ override fun onPause(owner: LifecycleOwner) {
159
+ reactDelegate.onHostPause()
160
+ }
141
161
 
162
+ override fun onDestroy(owner: LifecycleOwner) {
163
+ reactDelegate.onHostDestroy()
164
+ owner.lifecycle.removeObserver(this) // Cleanup to avoid leaks
165
+ }
166
+ }
167
+ }
168
+ }
@@ -1,23 +1,22 @@
1
1
  package com.callstack.reactnativebrownfield
2
2
 
3
3
  import android.view.View
4
- import java.util.Collections
5
-
6
4
  import com.facebook.react.ReactPackage
7
5
  import com.facebook.react.bridge.NativeModule
8
6
  import com.facebook.react.bridge.ReactApplicationContext
9
- import com.facebook.react.uimanager.ViewManager
10
7
  import com.facebook.react.uimanager.ReactShadowNode
8
+ import com.facebook.react.uimanager.ViewManager
9
+ import java.util.Collections
11
10
 
12
11
 
13
12
  class ReactNativeBrownfieldPackage : ReactPackage {
14
- override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {
15
- return Collections.emptyList()
16
- }
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {
14
+ return Collections.emptyList()
15
+ }
17
16
 
18
- override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
19
- val modules = ArrayList<NativeModule>()
20
- modules.add(ReactNativeBrownfieldModule(reactContext))
21
- return modules
22
- }
17
+ override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
18
+ val modules = ArrayList<NativeModule>()
19
+ modules.add(ReactNativeBrownfieldModule(reactContext))
20
+ return modules
21
+ }
23
22
  }
@@ -1,170 +1,140 @@
1
- package com.callstack.reactnativebrownfield;
1
+ package com.callstack.reactnativebrownfield
2
2
 
3
- import android.annotation.TargetApi
4
- import android.os.Build
5
3
  import android.os.Bundle
6
- import android.view.KeyEvent
7
- import com.facebook.infer.annotation.Assertions
4
+ import android.util.Log
5
+ import android.view.LayoutInflater
6
+ import android.view.View
7
+ import android.view.ViewGroup
8
+ import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames
8
9
  import com.facebook.react.ReactFragment
9
10
  import com.facebook.react.ReactHost
10
- import com.facebook.react.ReactNativeHost
11
11
  import com.facebook.react.bridge.Callback
12
12
  import com.facebook.react.bridge.WritableMap
13
- import com.facebook.react.common.LifecycleState
14
- import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
15
- import com.facebook.react.devsupport.DoubleTapReloadRecognizer
16
- import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler
17
13
  import com.facebook.react.modules.core.PermissionAwareActivity
18
14
  import com.facebook.react.modules.core.PermissionListener
19
15
 
20
16
  class ReactNativeFragment : ReactFragment(), PermissionAwareActivity {
21
- private lateinit var doubleTapReloadRecognizer: DoubleTapReloadRecognizer
22
- private lateinit var permissionsCallback: Callback
23
- private var permissionListener: PermissionListener? = null
24
-
25
- override fun onCreate(savedInstanceState: Bundle?) {
26
- super.onCreate(savedInstanceState)
27
- doubleTapReloadRecognizer = DoubleTapReloadRecognizer()
28
- }
29
-
30
- override fun getReactHost(): ReactHost? {
31
- return activity?.let {
32
- getDefaultReactHost(
33
- it.applicationContext,
34
- ReactNativeBrownfield.shared.reactNativeHost
35
- )
36
- }
37
- }
38
-
39
- override fun getReactNativeHost(): ReactNativeHost? {
40
- return ReactNativeBrownfield.shared.reactNativeHost
41
- }
42
-
43
- override fun onResume() {
44
- super.onResume()
45
- if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
46
- ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostResume(
47
- activity,
48
- activity as DefaultHardwareBackBtnHandler
49
- )
50
- }
51
- }
52
-
53
- override fun onPause() {
54
- super.onPause()
55
- if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
56
- ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager?.onHostPause(
57
- activity
58
- )
17
+ private lateinit var permissionsCallback: Callback
18
+ private var permissionListener: PermissionListener? = null
19
+ private lateinit var moduleName: String
20
+
21
+ override fun onCreate(savedInstanceState: Bundle?) {
22
+ /**
23
+ * ReactFragment.onCreate will throw an exception if we do not provide arg_component_name as arguments.
24
+ * We silently catch this exception. The reason is we want to invoke the super<Fragment>.onCreate in
25
+ * ReactFragment. Then initialise the mReactDelegate with ReactDelegateWrapper instead of ReactDelegate.
26
+ *
27
+ * So we purposely force ReactFragment.onCreate to throw an exception, so that we can provide our own
28
+ * implementation for mReactDelegate: ReactDelegateWrapper
29
+ */
30
+ try {
31
+ super.onCreate(savedInstanceState)
32
+ } catch (e: IllegalStateException) {
33
+ Log.w(
34
+ "ReactNativeFragment",
35
+ "ReactFragment threw due to missing arg_component_name: ${e.message} - This is an expected behaviour."
36
+ )
37
+ }
38
+
39
+ moduleName = arguments?.getString(ReactNativeFragmentArgNames.ARG_MODULE_NAME)!!
40
+ this.reactDelegate =
41
+ ReactDelegateWrapper(
42
+ activity,
43
+ this.reactHost,
44
+ moduleName,
45
+ arguments?.getBundle(ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS)
46
+ )
59
47
  }
60
- }
61
48
 
62
- override fun onDestroy() {
63
- super.onDestroy()
64
- if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
65
- val reactInstanceMgr = ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager
66
-
67
- if (reactInstanceMgr.lifecycleState != LifecycleState.RESUMED) {
68
- reactInstanceMgr.onHostDestroy(activity)
69
- }
70
- }
71
- }
72
-
73
- override fun onRequestPermissionsResult(
74
- requestCode: Int,
75
- permissions: Array<String>,
76
- grantResults: IntArray
77
- ) {
78
- permissionsCallback = Callback {
79
- if (permissionListener != null) {
80
- permissionListener?.onRequestPermissionsResult(
81
- requestCode,
82
- permissions,
83
- grantResults
49
+ override fun onCreateView(
50
+ inflater: LayoutInflater,
51
+ container: ViewGroup?,
52
+ savedInstanceState: Bundle?
53
+ ): View {
54
+ return ReactNativeBrownfield.shared.createView(
55
+ activity,
56
+ moduleName,
57
+ this.reactDelegate as ReactDelegateWrapper
84
58
  )
85
-
86
- permissionListener = null
87
- }
88
59
  }
89
- }
90
60
 
91
- override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
92
- return requireActivity().checkPermission(permission, pid, uid)
93
- }
61
+ override val reactHost: ReactHost?
62
+ get() = ReactNativeBrownfield.shared.reactHost
94
63
 
95
- @TargetApi(Build.VERSION_CODES.M)
96
- override fun checkSelfPermission(permission: String): Int {
97
- return requireActivity().checkSelfPermission(permission)
98
- }
99
-
100
- override fun requestPermissions(
101
- permissions: Array<String>,
102
- requestCode: Int,
103
- listener: PermissionListener?
104
- ) {
105
- permissionListener = listener
106
- this.requestPermissions(permissions, requestCode)
107
- }
108
-
109
- override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
110
- var handled = false
111
- if (ReactNativeBrownfield.shared.reactNativeHost.useDeveloperSupport && ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
112
- if (keyCode == KeyEvent.KEYCODE_MENU) {
113
- ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.showDevOptionsDialog()
114
- handled = true
115
- }
116
- val didDoubleTapR = activity?.currentFocus?.let {
117
- Assertions.assertNotNull(doubleTapReloadRecognizer)
118
- .didDoubleTapR(keyCode, it)
119
- }
120
- if (didDoubleTapR == true) {
121
- ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.devSupportManager.handleReloadJS()
122
- handled = true
123
- }
64
+ override fun onResume() {
65
+ try {
66
+ super.onResume()
67
+ } catch (_: ClassCastException) {
68
+ (this.reactDelegate as ReactDelegateWrapper).onReactHostResume()
69
+ }
124
70
  }
125
- return handled
126
- }
127
71
 
128
- fun onBackPressed(backBtnHandler: DefaultHardwareBackBtnHandler) {
129
- if (ReactNativeBrownfieldModule.shouldPopToNative) {
130
- backBtnHandler.invokeDefaultOnBackPressed()
131
- } else if (ReactNativeBrownfield.shared.reactNativeHost.hasInstance()) {
132
- ReactNativeBrownfield.shared.reactNativeHost.reactInstanceManager.onBackPressed()
72
+ override fun onRequestPermissionsResult(
73
+ requestCode: Int,
74
+ permissions: Array<String>,
75
+ grantResults: IntArray
76
+ ) {
77
+ permissionsCallback = Callback {
78
+ if (permissionListener != null) {
79
+ permissionListener?.onRequestPermissionsResult(
80
+ requestCode,
81
+ permissions,
82
+ grantResults
83
+ )
84
+
85
+ permissionListener = null
86
+ }
87
+ }
133
88
  }
134
- }
135
89
 
136
- companion object {
137
- @JvmStatic
138
- @JvmOverloads
139
- fun createReactNativeFragment(
140
- moduleName: String,
141
- initialProps: Bundle? = null
142
- ): ReactNativeFragment {
143
- val fragment = ReactNativeFragment()
144
- val args = Bundle()
145
- args.putString(ARG_COMPONENT_NAME, moduleName)
146
- if (initialProps != null) {
147
- args.putBundle(ARG_LAUNCH_OPTIONS, initialProps)
148
- }
149
- fragment.arguments = args
150
- return fragment
90
+ override fun checkPermission(permission: String, pid: Int, uid: Int): Int {
91
+ return requireActivity().checkPermission(permission, pid, uid)
151
92
  }
152
93
 
153
- @JvmStatic
154
- fun createReactNativeFragment(
155
- moduleName: String,
156
- initialProps: HashMap<String, *>
157
- ): ReactNativeFragment {
158
- return createReactNativeFragment(moduleName, PropsBundle.fromHashMap(initialProps))
94
+ override fun checkSelfPermission(permission: String): Int {
95
+ return requireActivity().checkSelfPermission(permission)
159
96
  }
160
97
 
161
- @JvmStatic
162
- fun createReactNativeFragment(
163
- moduleName: String,
164
- initialProps: WritableMap
165
- ): ReactNativeFragment {
166
- return createReactNativeFragment(moduleName, initialProps.toHashMap())
98
+ override fun requestPermissions(
99
+ permissions: Array<String>,
100
+ requestCode: Int,
101
+ listener: PermissionListener?
102
+ ) {
103
+ permissionListener = listener
104
+ this.requestPermissions(permissions, requestCode)
167
105
  }
168
- }
169
106
 
107
+ companion object {
108
+ @JvmStatic
109
+ @JvmOverloads
110
+ fun createReactNativeFragment(
111
+ moduleName: String,
112
+ initialProps: Bundle? = null
113
+ ): ReactNativeFragment {
114
+ val fragment = ReactNativeFragment()
115
+ val args = Bundle()
116
+ args.putString(ReactNativeFragmentArgNames.ARG_MODULE_NAME, moduleName)
117
+ if (initialProps != null) {
118
+ args.putBundle(ReactNativeFragmentArgNames.ARG_LAUNCH_OPTIONS, initialProps)
119
+ }
120
+ fragment.arguments = args
121
+ return fragment
122
+ }
123
+
124
+ @JvmStatic
125
+ fun createReactNativeFragment(
126
+ moduleName: String,
127
+ initialProps: HashMap<String, *>
128
+ ): ReactNativeFragment {
129
+ return createReactNativeFragment(moduleName, PropsBundle.fromHashMap(initialProps))
130
+ }
131
+
132
+ @JvmStatic
133
+ fun createReactNativeFragment(
134
+ moduleName: String,
135
+ initialProps: WritableMap
136
+ ): ReactNativeFragment {
137
+ return createReactNativeFragment(moduleName, initialProps.toHashMap())
138
+ }
139
+ }
170
140
  }
@@ -0,0 +1,20 @@
1
+ package com.callstack.reactnativebrownfield.constants
2
+
3
+ import com.facebook.react.ReactFragment
4
+
5
+ /**
6
+ * Convenience export of arguments that can be used
7
+ */
8
+ class ReactNativeFragmentArgNames private constructor() :
9
+ ReactFragment() // subclass to gain access to protected constants
10
+ {
11
+ companion object {
12
+ /**
13
+ * The module name to be loaded
14
+ */
15
+ const val ARG_MODULE_NAME = "arg_module_name"
16
+
17
+ // re-export constants from ReactFragment to enable access
18
+ const val ARG_LAUNCH_OPTIONS: String = "arg_launch_options"
19
+ }
20
+ }
@@ -0,0 +1,17 @@
1
+ package com.callstack.reactnativebrownfield.utils
2
+
3
+ object VersionUtils {
4
+ fun isVersionLessThan(version: String, threshold: String): Boolean {
5
+ val versionParts = version.split(".").map { it.toIntOrNull() ?: 0 }
6
+ val thresholdParts = threshold.split(".").map { it.toIntOrNull() ?: 0 }
7
+
8
+ val maxLength = maxOf(versionParts.size, thresholdParts.size)
9
+ for (i in 0 until maxLength) {
10
+ val vPart = versionParts.getOrNull(i) ?: 0
11
+ val tPart = thresholdParts.getOrNull(i) ?: 0
12
+ if (vPart != tPart) return vPart < tPart
13
+ }
14
+
15
+ return false // equal versions are not less than
16
+ }
17
+ }
@@ -4,34 +4,34 @@ import com.facebook.react.bridge.ReactApplicationContext
4
4
  import com.facebook.react.bridge.ReactMethod
5
5
 
6
6
  class ReactNativeBrownfieldModule(reactContext: ReactApplicationContext) :
7
- NativeReactNativeBrownfieldModuleSpec(reactContext) {
8
- companion object {
9
- var shouldPopToNative: Boolean = false
10
- }
7
+ NativeReactNativeBrownfieldModuleSpec(reactContext) {
8
+ companion object {
9
+ var shouldPopToNative: Boolean = false
10
+ }
11
11
 
12
- @ReactMethod
13
- override fun popToNative(animated: Boolean) {
14
- shouldPopToNative = true
15
- onBackPressed()
16
- }
12
+ @ReactMethod
13
+ override fun popToNative(animated: Boolean) {
14
+ shouldPopToNative = true
15
+ onBackPressed()
16
+ }
17
17
 
18
- @ReactMethod
19
- override fun setPopGestureRecognizerEnabled(enabled: Boolean) {
20
- shouldPopToNative = enabled
21
- }
18
+ @ReactMethod
19
+ override fun setPopGestureRecognizerEnabled(enabled: Boolean) {
20
+ shouldPopToNative = enabled
21
+ }
22
22
 
23
- @ReactMethod
24
- override fun setHardwareBackButtonEnabled(enabled: Boolean) {
25
- shouldPopToNative = enabled
26
- }
23
+ @ReactMethod
24
+ override fun setHardwareBackButtonEnabled(enabled: Boolean) {
25
+ shouldPopToNative = enabled
26
+ }
27
27
 
28
- private fun onBackPressed() {
29
- reactApplicationContext.currentActivity?.runOnUiThread {
30
- reactApplicationContext.currentActivity?.onBackPressed()
28
+ private fun onBackPressed() {
29
+ reactApplicationContext.currentActivity?.runOnUiThread {
30
+ reactApplicationContext.currentActivity?.onBackPressed()
31
+ }
31
32
  }
32
- }
33
33
 
34
- override fun getName(): String {
35
- return "ReactNativeBrownfield"
36
- }
34
+ override fun getName(): String {
35
+ return "ReactNativeBrownfield"
36
+ }
37
37
  }
@@ -88,7 +88,7 @@ class ReactNativeBrownfieldDelegate: RCTDefaultReactNativeFactoryDelegate {
88
88
  initialProps: [AnyHashable: Any]?,
89
89
  launchOptions: [AnyHashable: Any]? = nil
90
90
  ) -> UIView? {
91
- reactNativeFactory?.rootViewFactory.view(
91
+ rootViewFactory?.view(
92
92
  withModuleName: moduleName,
93
93
  initialProperties: initialProps,
94
94
  launchOptions: launchOptions
@@ -8,7 +8,10 @@ struct ReactNativeViewRepresentable: UIViewControllerRepresentable {
8
8
  var initialProperties: [String: Any] = [:]
9
9
 
10
10
  func makeUIViewController(context: Context) -> UIViewController {
11
- return ReactNativeViewController(moduleName: moduleName)
11
+ return ReactNativeViewController(
12
+ moduleName: moduleName,
13
+ initialProperties: initialProperties
14
+ )
12
15
  }
13
16
 
14
17
  func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@callstack/react-native-brownfield",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "author": "Michal Chudziak <mike.chudziak@callstack.com>",
6
6
  "contributors": [
@@ -60,7 +60,7 @@
60
60
  "access": "public"
61
61
  },
62
62
  "resolutions": {
63
- "@types/react": "19.0.0"
63
+ "@types/react": "19.1.1"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "react": "*",
@@ -70,25 +70,25 @@
70
70
  "@babel/core": "^7.25.2",
71
71
  "@babel/preset-env": "^7.25.3",
72
72
  "@babel/runtime": "^7.25.0",
73
- "@react-native/babel-preset": "0.78.0",
74
- "@react-native/eslint-config": "0.78.0",
75
- "@react-native/typescript-config": "0.78.0",
73
+ "@react-native/babel-preset": "0.82.1",
74
+ "@react-native/eslint-config": "0.82.1",
75
+ "@react-native/typescript-config": "0.82.1",
76
76
  "@release-it/conventional-changelog": "^5.0.0",
77
77
  "@types/jest": "^29.5.13",
78
- "@types/react": "^19.0.0",
79
- "@types/react-test-renderer": "^19.0.0",
78
+ "@types/react": "^19.1.1",
79
+ "@types/react-test-renderer": "^19.1.0",
80
80
  "babel-plugin-module-resolver": "5.0.0",
81
81
  "eslint": "^8.19.0",
82
82
  "eslint-config-prettier": "^9.1.0",
83
83
  "eslint-plugin-prettier": "^5.1.3",
84
84
  "jest": "^29.6.3",
85
85
  "prettier": "^3.5.3",
86
- "react": "19.0.0",
87
- "react-native": "0.78.0",
86
+ "react": "19.1.1",
87
+ "react-native": "0.82.1",
88
88
  "react-native-builder-bob": "^0.37.0",
89
- "react-test-renderer": "19.0.0",
89
+ "react-test-renderer": "19.1.1",
90
90
  "release-it": "^18.1.2",
91
- "typescript": "5.0.4"
91
+ "typescript": "5.8.3"
92
92
  },
93
93
  "release-it": {
94
94
  "git": {
@@ -172,7 +172,7 @@
172
172
  ]
173
173
  },
174
174
  "engines": {
175
- "node": ">=18"
175
+ "node": ">=20"
176
176
  },
177
177
  "packageManager": "yarn@3.6.4"
178
178
  }