From 1b0e03ba928cab2d41956bdd7c1c96e72734eb2a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 7 May 2026 19:37:41 +0000 Subject: [PATCH] feat: add React Native New Architecture support (RN 0.83+) - Android: Apply com.facebook.react plugin unconditionally for New Architecture - Android: Replace hardcoded react-android dependency with dynamic version - Android: Migrate CourierReactNativePackage to TurboReactPackage for proper Turbo Module discovery - Android: Fix currentActivity null safety for bridgeless mode (replace ReactActivity casting with Activity) - Android: Fix CourierPreferencesViewManager null safety on currentActivity - Android: Update SDK versions (compileSdk/targetSdk 34, minSdk 24, Kotlin 1.9.24, NDK 26.1) - iOS: Update podspec with cleaner New Architecture configuration - iOS: Fix CourierReactNativeDelegate for bridgeless mode compatibility (conditional RCTBridge import, string-based notification name) - iOS: Add bridgeless mode documentation to notification observers - JS: Add TurboModuleRegistry support in Modules.tsx with NativeModules fallback for backward compatibility Closes #35 --- android/build.gradle | 19 +++------- android/gradle.properties | 10 ++--- .../courierreactnative/CourierClientModule.kt | 5 ++- .../CourierPreferencesViewManager.kt | 4 +- .../CourierReactNativePackage.kt | 38 +++++++++++++++---- .../courierreactnative/CourierSystemModule.kt | 12 +++--- .../courierreactnative/ReactNativeModule.kt | 8 +--- courier-react-native.podspec | 32 ++++++++-------- ios/CourierReactNative-Bridging-Header.h | 4 ++ ios/CourierReactNativeDelegate.h | 2 +- ios/CourierReactNativeDelegate.m | 14 +++++-- ios/CourierSystemModule.swift | 3 ++ src/Modules.tsx | 36 ++++++++++++------ 13 files changed, 112 insertions(+), 75 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 740e60e..b5ef9c0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,12 +21,9 @@ def isNewArchitectureEnabled() { apply plugin: "com.android.library" apply plugin: "kotlin-android" - def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') } -if (isNewArchitectureEnabled()) { - apply plugin: "com.facebook.react" -} +apply plugin: "com.facebook.react" def getExtOrDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["CourierReactNative_" + name] @@ -100,10 +97,8 @@ def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { - // For < 0.71, this will be from the local maven repo - // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin //noinspection GradleDynamicVersion - implementation "com.facebook.react:react-android:0.73.7" + implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // For converting to json @@ -115,10 +110,8 @@ dependencies { } -if (isNewArchitectureEnabled()) { - react { - jsRootDir = file("../src/") - libraryName = "CourierReactNativeView" - codegenJavaPackageName = "com.courierreactnative" - } +react { + jsRootDir = file("../src/") + libraryName = "CourierReactNativeView" + codegenJavaPackageName = "com.courierreactnative" } diff --git a/android/gradle.properties b/android/gradle.properties index 4390899..d91b938 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ -CourierReactNative_kotlinVersion=1.7.0 -CourierReactNative_minSdkVersion=23 -CourierReactNative_targetSdkVersion=31 -CourierReactNative_compileSdkVersion=31 -CourierReactNative_ndkversion=21.4.7075529 +CourierReactNative_kotlinVersion=1.9.24 +CourierReactNative_minSdkVersion=24 +CourierReactNative_targetSdkVersion=34 +CourierReactNative_compileSdkVersion=34 +CourierReactNative_ndkversion=26.1.10909125 diff --git a/android/src/main/java/com/courierreactnative/CourierClientModule.kt b/android/src/main/java/com/courierreactnative/CourierClientModule.kt index 3463204..ca903bc 100644 --- a/android/src/main/java/com/courierreactnative/CourierClientModule.kt +++ b/android/src/main/java/com/courierreactnative/CourierClientModule.kt @@ -77,7 +77,8 @@ class CourierClientModule( return@launch } - if (reactActivity == null) { + val currentActivity = activity + if (currentActivity == null) { promise.rejectMissingContext() return@launch } @@ -97,7 +98,7 @@ class CourierClientModule( client.tokens.putUserToken( token = token, provider = provider, - device = courierDevice ?: CourierDevice.current(reactActivity!!) + device = courierDevice ?: CourierDevice.current(currentActivity) ) promise.resolve(null) } catch (e: Exception) { diff --git a/android/src/main/java/com/courierreactnative/CourierPreferencesViewManager.kt b/android/src/main/java/com/courierreactnative/CourierPreferencesViewManager.kt index e0c14e9..8ced36c 100644 --- a/android/src/main/java/com/courierreactnative/CourierPreferencesViewManager.kt +++ b/android/src/main/java/com/courierreactnative/CourierPreferencesViewManager.kt @@ -43,8 +43,8 @@ class CourierPreferencesViewManager : SimpleViewManager() { override fun createViewInstance(reactContext: ThemedReactContext): CourierPreferences { themedReactContext = reactContext - val activity = reactContext.currentActivity as FragmentActivity - return CourierReactNativePreferencesView(activity) + val activity = reactContext.currentActivity as? FragmentActivity ?: reactContext.reactApplicationContext.currentActivity as? FragmentActivity + return CourierReactNativePreferencesView(activity ?: reactContext) } @ReactProp(name = "onScrollPreferences") diff --git a/android/src/main/java/com/courierreactnative/CourierReactNativePackage.kt b/android/src/main/java/com/courierreactnative/CourierReactNativePackage.kt index 0cea9a8..2cd1c57 100644 --- a/android/src/main/java/com/courierreactnative/CourierReactNativePackage.kt +++ b/android/src/main/java/com/courierreactnative/CourierReactNativePackage.kt @@ -1,18 +1,40 @@ package com.courierreactnative -import com.facebook.react.ReactPackage +import com.facebook.react.TurboReactPackage import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider import com.facebook.react.uimanager.ViewManager -class CourierReactNativePackage : ReactPackage { +class CourierReactNativePackage : TurboReactPackage() { - override fun createNativeModules(reactContext: ReactApplicationContext): List { - return listOf( - CourierClientModule(reactContext), - CourierSharedModule(reactContext), - CourierSystemModule(reactContext), - ) + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return when (name) { + "CourierClientModule" -> CourierClientModule(reactContext) + "CourierSharedModule" -> CourierSharedModule(reactContext) + "CourierSystemModule" -> CourierSystemModule(reactContext) + else -> null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + val modules = mutableMapOf() + val moduleNames = listOf("CourierClientModule", "CourierSharedModule", "CourierSystemModule") + for (moduleName in moduleNames) { + modules[moduleName] = ReactModuleInfo( + moduleName, + moduleName, + false, + false, + true, + false, + false + ) + } + modules + } } override fun createViewManagers(reactContext: ReactApplicationContext): List> { diff --git a/android/src/main/java/com/courierreactnative/CourierSystemModule.kt b/android/src/main/java/com/courierreactnative/CourierSystemModule.kt index 1808d6d..b2209c4 100644 --- a/android/src/main/java/com/courierreactnative/CourierSystemModule.kt +++ b/android/src/main/java/com/courierreactnative/CourierSystemModule.kt @@ -45,8 +45,8 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod @ReactMethod fun registerPushNotificationClickedOnKilledState() { - reactActivity?.let { activity -> - checkIntentForPushNotificationClick(activity.intent) + activity?.let { a -> + checkIntentForPushNotificationClick(a.intent) } } @@ -66,8 +66,8 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod @ReactMethod fun requestNotificationPermission(promise: Promise) { - reactActivity?.let { activity -> - Courier.shared.requestNotificationPermission(activity) + activity?.let { a -> + Courier.shared.requestNotificationPermission(a) } promise.resolve("unknown") @@ -77,9 +77,9 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod @ReactMethod fun getNotificationPermissionStatus(promise: Promise) { - reactActivity?.let { context -> + activity?.let { a -> - val isGranted = Courier.shared.isPushPermissionGranted(context) + val isGranted = Courier.shared.isPushPermissionGranted(a) val status = if (isGranted) "authorized" else "denied" promise.resolve(status) return diff --git a/android/src/main/java/com/courierreactnative/ReactNativeModule.kt b/android/src/main/java/com/courierreactnative/ReactNativeModule.kt index 09e09c2..1545822 100644 --- a/android/src/main/java/com/courierreactnative/ReactNativeModule.kt +++ b/android/src/main/java/com/courierreactnative/ReactNativeModule.kt @@ -1,7 +1,7 @@ package com.courierreactnative +import android.app.Activity import com.courier.android.Courier -import com.facebook.react.ReactActivity import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule @@ -10,14 +10,10 @@ abstract class ReactNativeModule(val tag: String, private val name: String, reac override fun getName() = name - val reactActivity: ReactActivity? get() = currentActivity as? ReactActivity + val activity: Activity? get() = currentActivity init { - - // User Agent is used to ensure we know the SDK - // the requests come from Courier.agent = Utils.COURIER_AGENT - } internal fun Promise.rejectMissingContext() { diff --git a/courier-react-native.podspec b/courier-react-native.podspec index 200e37a..dd00bc0 100644 --- a/courier-react-native.podspec +++ b/courier-react-native.podspec @@ -20,26 +20,26 @@ Pod::Spec.new do |s| s.dependency "Courier_iOS", "5.8.0" # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. + # This automatically handles both Old and New Architecture (Turbo Modules + Fabric) setup. # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. if respond_to?(:install_modules_dependencies, true) install_modules_dependencies(s) else - s.dependency "React-Core" + s.dependency "React-Core" - # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then - s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" - s.pod_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", - "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", - "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" - } - s.dependency "React-RCTFabric" - s.dependency "React-Codegen" - s.dependency "RCT-Folly" - s.dependency "RCTRequired" - s.dependency "RCTTypeSafety" - s.dependency "ReactCommon/turbomodule/core" - end + if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" + s.pod_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", + "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", + "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" + } + s.dependency "React-RCTFabric" + s.dependency "React-Codegen" + s.dependency "RCT-Folly" + s.dependency "RCTRequired" + s.dependency "RCTTypeSafety" + s.dependency "ReactCommon/turbomodule/core" + end end end diff --git a/ios/CourierReactNative-Bridging-Header.h b/ios/CourierReactNative-Bridging-Header.h index 8992b22..1c1fdb5 100644 --- a/ios/CourierReactNative-Bridging-Header.h +++ b/ios/CourierReactNative-Bridging-Header.h @@ -1,3 +1,7 @@ #import #import #import + +#if __has_include() +#import +#endif diff --git a/ios/CourierReactNativeDelegate.h b/ios/CourierReactNativeDelegate.h index 0b08018..d9c92db 100644 --- a/ios/CourierReactNativeDelegate.h +++ b/ios/CourierReactNativeDelegate.h @@ -5,7 +5,7 @@ // Created by Michael Miller on 8/29/23. // -#import "RCTAppDelegate.h" +#import #import #import diff --git a/ios/CourierReactNativeDelegate.m b/ios/CourierReactNativeDelegate.m index 484a55a..abaa318 100644 --- a/ios/CourierReactNativeDelegate.m +++ b/ios/CourierReactNativeDelegate.m @@ -8,10 +8,13 @@ @import Courier_iOS; #import "CourierReactNativeDelegate.h" #import -#import -#import #import +// Conditionally import bridge headers for backward compatibility +#if __has_include() +#import +#endif + #pragma GCC diagnostic ignored "-Wprotocol" #pragma clang diagnostic ignored "-Wprotocol" @@ -49,10 +52,13 @@ - (id)init { name:CourierForegroundOptionsDidChangeNotification object:nil]; + // RCTBridgeWillReloadNotification is only available in bridge mode. + // In bridgeless mode (New Architecture), this notification doesn't exist. + NSString *bridgeReloadNotification = @"RCTBridgeWillReloadNotification"; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onBridgeWillReload) - name:RCTBridgeWillReloadNotification + name:bridgeReloadNotification object:nil]; [[NSNotificationCenter defaultCenter] @@ -77,7 +83,7 @@ - (void)onReactNativeReady:(__unused NSNotification *)note } } -// Called when there is a reload to React Native +// Called when there is a reload to React Native (bridge mode only) - (void)onBridgeWillReload { self.isReactNativeReady = NO; diff --git a/ios/CourierSystemModule.swift b/ios/CourierSystemModule.swift index 37a5119..3897bc0 100644 --- a/ios/CourierSystemModule.swift +++ b/ios/CourierSystemModule.swift @@ -47,6 +47,9 @@ class CourierSystemModule: CourierReactNativeEventEmitter { object: nil ) + // RCTBridgeWillReloadNotification is only available in bridge mode. + // In bridgeless mode (New Architecture), this notification is not posted, + // but registering for it is harmless. notificationCenter.addObserver( self, selector: #selector(onBridgeWillReload), diff --git a/src/Modules.tsx b/src/Modules.tsx index 6257fcf..8344131 100644 --- a/src/Modules.tsx +++ b/src/Modules.tsx @@ -1,10 +1,27 @@ import { NativeModules, Platform, + TurboModuleRegistry, UIManager, requireNativeComponent, } from 'react-native'; +function getModule(moduleName: string): any { + // Try TurboModuleRegistry first (New Architecture / bridgeless mode) + const turboModule = TurboModuleRegistry.get(moduleName as any); + if (turboModule) { + return turboModule; + } + + // Fall back to NativeModules (bridge mode / interop layer) + const bridgeModule = NativeModules[moduleName]; + if (bridgeModule) { + return bridgeModule; + } + + return undefined; +} + export class Modules { static readonly LINKING_ERROR = `The package '@trycourier/courier-react-native' doesn't seem to be linked. Make sure: \n\n` + @@ -12,27 +29,22 @@ export class Modules { '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n'; - static readonly Client = Modules.getNativeModule( - NativeModules.CourierClientModule - ); - static readonly Shared = Modules.getNativeModule( - NativeModules.CourierSharedModule - ); - static readonly System = Modules.getNativeModule( - NativeModules.CourierSystemModule - ); + static readonly Client = Modules.getNativeModule('CourierClientModule'); + static readonly Shared = Modules.getNativeModule('CourierSharedModule'); + static readonly System = Modules.getNativeModule('CourierSystemModule'); - static getNativeModule(nativeModule: T | undefined): T { + static getNativeModule(moduleName: string): any { + const nativeModule = getModule(moduleName); return nativeModule ? nativeModule - : (new Proxy( + : new Proxy( {}, { get() { throw new Error(Modules.LINKING_ERROR); }, } - ) as T); + ); } static getNativeComponent(componentName: string) {