From 4548e9a28580207ca14b934f2224b9ac0a47da06 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 7 May 2026 22:24:53 +0000 Subject: [PATCH] feat: add React Native New Architecture support (Turbo Modules + Fabric interop) Addresses #35: Support for React Native New Architecture (Expo SDK 55 / RN 0.83+) Changes: - Use TurboModuleRegistry with NativeModules fallback for module resolution so native modules are discoverable in both bridgeless and bridge modes - Fix currentActivity null safety: avoid casting to ReactActivity which may not exist in bridgeless mode; use Activity directly - Fix CourierClientModule: use reactApplicationContext for CourierDevice.current() instead of force-unwrapping reactActivity - Fix CourierPreferencesViewManager: safe-cast currentActivity to FragmentActivity with fallback to ThemedReactContext - Update react-android dependency to use dynamic version (+) so the consuming app's RN version is resolved automatically - Update default SDK versions (compileSdk/targetSdk 34, minSdk 24, Kotlin 1.9.24) - Add safeExtGet helper for SDK version resolution from consuming project iOS side already supports New Architecture through the interop layer: - install_modules_dependencies in podspec handles both architectures - RCTEventEmitter/RCTViewManager work through Fabric/TurboModule interop --- android/build.gradle | 15 ++++++++------- android/gradle.properties | 10 +++++----- .../courierreactnative/CourierClientModule.kt | 8 ++------ .../CourierPreferencesViewManager.kt | 4 ++-- .../courierreactnative/CourierSystemModule.kt | 14 ++++++-------- .../com/courierreactnative/ReactNativeModule.kt | 8 ++------ src/Modules.tsx | 17 ++++++++++++++--- 7 files changed, 39 insertions(+), 37 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index da7de90..d4fc414 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,13 +21,16 @@ 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" } +def safeExtGet(prop, fallback) { + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback +} + def getExtOrDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["CourierReactNative_" + name] } @@ -61,15 +64,15 @@ android { } } - compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + compileSdkVersion safeExtGet("compileSdkVersion", getExtOrIntegerDefault("compileSdkVersion")) buildFeatures { buildConfig true } defaultConfig { - minSdkVersion getExtOrIntegerDefault("minSdkVersion") - targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + minSdkVersion safeExtGet("minSdkVersion", getExtOrIntegerDefault("minSdkVersion")) + targetSdkVersion safeExtGet("targetSdkVersion", getExtOrIntegerDefault("targetSdkVersion")) buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() } @@ -100,10 +103,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-android:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" // For converting to json 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..19588bb 100644 --- a/android/src/main/java/com/courierreactnative/CourierClientModule.kt +++ b/android/src/main/java/com/courierreactnative/CourierClientModule.kt @@ -77,11 +77,7 @@ class CourierClientModule( return@launch } - if (reactActivity == null) { - promise.rejectMissingContext() - return@launch - } - + val context = reactApplicationContext val courierDevice = device?.let { CourierDevice( appId = it.getString("appId"), @@ -97,7 +93,7 @@ class CourierClientModule( client.tokens.putUserToken( token = token, provider = provider, - device = courierDevice ?: CourierDevice.current(reactActivity!!) + device = courierDevice ?: CourierDevice.current(context) ) 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 74b651f..a2a2f11 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 context = (reactContext.currentActivity as? FragmentActivity) ?: reactContext + return CourierReactNativePreferencesView(context) } @ReactProp(name = "onScrollPreferences") diff --git a/android/src/main/java/com/courierreactnative/CourierSystemModule.kt b/android/src/main/java/com/courierreactnative/CourierSystemModule.kt index 1808d6d..21b797a 100644 --- a/android/src/main/java/com/courierreactnative/CourierSystemModule.kt +++ b/android/src/main/java/com/courierreactnative/CourierSystemModule.kt @@ -22,7 +22,6 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod init { - // Listen to push notification events Courier.shared.onPushNotificationEvent { event -> when (event.trackingEvent) { CLICKED -> postPushNotificationJavascriptEvent(CourierEvents.Push.CLICKED_EVENT, event.remoteMessage) @@ -45,8 +44,8 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod @ReactMethod fun registerPushNotificationClickedOnKilledState() { - reactActivity?.let { activity -> - checkIntentForPushNotificationClick(activity.intent) + activity?.let { act -> + checkIntentForPushNotificationClick(act.intent) } } @@ -66,8 +65,8 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod @ReactMethod fun requestNotificationPermission(promise: Promise) { - reactActivity?.let { activity -> - Courier.shared.requestNotificationPermission(activity) + activity?.let { act -> + Courier.shared.requestNotificationPermission(act) } promise.resolve("unknown") @@ -77,9 +76,9 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod @ReactMethod fun getNotificationPermissionStatus(promise: Promise) { - reactActivity?.let { context -> + activity?.let { act -> - val isGranted = Courier.shared.isPushPermissionGranted(context) + val isGranted = Courier.shared.isPushPermissionGranted(act) val status = if (isGranted) "authorized" else "denied" promise.resolve(status) return @@ -92,7 +91,6 @@ class CourierSystemModule(reactContext: ReactApplicationContext): ReactNativeMod @ReactMethod(isBlockingSynchronousMethod = true) fun openSettingsForApp(): String? { - // TODO: Move this to the native package in the future val context = reactApplicationContext try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 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/src/Modules.tsx b/src/Modules.tsx index 6257fcf..c99fae7 100644 --- a/src/Modules.tsx +++ b/src/Modules.tsx @@ -2,6 +2,7 @@ import { NativeModules, Platform, UIManager, + TurboModuleRegistry, requireNativeComponent, } from 'react-native'; @@ -13,18 +14,28 @@ export class Modules { '- You are not using Expo Go\n'; static readonly Client = Modules.getNativeModule( + 'CourierClientModule', NativeModules.CourierClientModule ); static readonly Shared = Modules.getNativeModule( + 'CourierSharedModule', NativeModules.CourierSharedModule ); static readonly System = Modules.getNativeModule( + 'CourierSystemModule', NativeModules.CourierSystemModule ); - static getNativeModule(nativeModule: T | undefined): T { - return nativeModule - ? nativeModule + static getNativeModule( + moduleName: string, + bridgeModule: T | undefined + ): T { + const resolved = + (TurboModuleRegistry?.get(moduleName) as T | null) ?? + bridgeModule ?? + undefined; + return resolved + ? resolved : (new Proxy( {}, {