From e264d44b5cc7ef74da394bb0f45eadeb4108ed97 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 26 Aug 2026 05:46:38 -0400 Subject: [PATCH 1/6] feat: expose Expo scene deep-link readiness --- README.md | 11 +++- __tests__/deep-link-routing-readiness.test.ts | 50 +++++++++++++++++++ .../reactnative/sdk/NativeCustomerIOModule.kt | 4 ++ .../customerio-reactnative.api.md | 1 + customerio-reactnative.podspec | 3 ++ .../CustomerIOReactNativeDeepLinkRouter.swift | 34 +++++++++++-- ios/wrappers/NativeCustomerIO.mm | 5 ++ ios/wrappers/NativeCustomerIO.swift | 21 +++++++- ios/wrappers/customerio_reactnative.h | 6 +++ src/customerio-cdp.ts | 10 ++++ src/specs/modules/NativeCustomerIO.ts | 1 + 11 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 __tests__/deep-link-routing-readiness.test.ts create mode 100644 ios/wrappers/customerio_reactnative.h diff --git a/README.md b/README.md index 1e5d900b..862dede2 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,16 @@ On iOS, a `UIScene` host must install the wrapper's deep-link bridge before Reac NativeCustomerIO.configureSceneDeepLinkRouting() ``` -Then register the app's JavaScript `Linking` URL listener before calling `CustomerIO.initialize`. This ordering is required. Customer.io push, in-app, and inbox destinations are buffered during cold launch, then delivered through React Native's standard `Linking` API after initialization. If React Native does not initialize within ten seconds, the SDK opens the destination through the system instead of retaining it indefinitely. The listener owns the routing decision: navigate destinations your app handles, and use the app's normal external-browser path for other HTTP(S) destinations. React Native's native URL event has no handled result that the SDK can use for this decision, so a destination published after initialization without a listener is not opened externally, which would risk duplicate navigation. Natively initialized hosts, including Expo config-plugin integrations, keep their existing callback. Older React Native versions and AppDelegate-only hosts also keep their existing deep-link integration. +Then register the app's JavaScript `Linking` URL listener before calling `CustomerIO.initialize`. This ordering is required. The Expo plugin detects its scene lifecycle during initialization, so Expo apps do not call the native installation method themselves. Customer.io push, in-app, and inbox destinations are buffered during cold launch, then delivered through React Native's standard `Linking` API after initialization. If React Native does not initialize within ten seconds, the SDK opens the destination through the system instead of retaining it indefinitely. The listener owns the routing decision: navigate destinations your app handles, and use the app's normal external-browser path for other HTTP(S) destinations. React Native's native URL event has no handled result that the SDK can use for this decision, so a destination published after initialization without a listener is not opened externally, which would risk duplicate navigation. Older React Native versions and AppDelegate-only hosts keep their existing deep-link integration. + +An Expo app using config-plugin auto-initialization does not call `CustomerIO.initialize`. Register its `Linking` listener, then mark that listener ready instead: + +```typescript +const subscription = Linking.addEventListener('url', ({ url }) => { + // Route the URL in your app. +}); +CustomerIO.setDeepLinkRoutingReady(); +``` This integration applies after the host has adopted React Native's UIScene lifecycle; the plugin does not replace React Native's root application lifecycle. diff --git a/__tests__/deep-link-routing-readiness.test.ts b/__tests__/deep-link-routing-readiness.test.ts new file mode 100644 index 00000000..98b24d92 --- /dev/null +++ b/__tests__/deep-link-routing-readiness.test.ts @@ -0,0 +1,50 @@ +jest.mock('react-native', () => ({ + Platform: { + OS: 'ios', + select: (spec: { [key: string]: unknown }) => + spec.ios ?? spec.default ?? undefined, + }, +})); + +jest.mock('../src/customerio-geofence', () => ({ + CustomerIOGeofence: class {}, +})); +jest.mock('../src/customerio-inapp', () => ({ + CustomerIOInAppMessaging: class {}, +})); +jest.mock('../src/customerio-liveactivities', () => ({ + CustomerIOLiveActivities: class {}, +})); +jest.mock('../src/customerio-location', () => ({ + CustomerIOLocation: class {}, +})); +jest.mock('../src/customerio-push', () => ({ + CustomerIOPushMessaging: class {}, +})); +jest.mock('../src/native-logger-listener', () => ({ + NativeLoggerListener: { + initNativeLogger: jest.fn(), + initialize: jest.fn(), + }, +})); +jest.mock('../src/specs/modules/NativeCustomerIO', () => ({ + __esModule: true, + default: { + setDeepLinkRoutingReady: jest.fn(), + }, +})); + +import { CustomerIO } from '../src/customerio-cdp'; +import NativeCustomerIO from '../src/specs/modules/NativeCustomerIO'; + +describe('CustomerIO scene deep-link readiness', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('notifies the native router after the host registers its Linking listener', () => { + CustomerIO.setDeepLinkRoutingReady(); + + expect(NativeCustomerIO.setDeepLinkRoutingReady).toHaveBeenCalledTimes(1); + }); +}); diff --git a/android/src/main/java/io/customer/reactnative/sdk/NativeCustomerIOModule.kt b/android/src/main/java/io/customer/reactnative/sdk/NativeCustomerIOModule.kt index dcd66a82..2fc0dc51 100644 --- a/android/src/main/java/io/customer/reactnative/sdk/NativeCustomerIOModule.kt +++ b/android/src/main/java/io/customer/reactnative/sdk/NativeCustomerIOModule.kt @@ -143,6 +143,10 @@ class NativeCustomerIOModule( } } + override fun setDeepLinkRoutingReady() { + // UIScene and React Native Linking readiness are iOS-only concerns. + } + override fun identify(params: ReadableMap?) { val userId = params?.getString("userId") val traits = params?.getMap("traits") diff --git a/api-extractor-output/customerio-reactnative.api.md b/api-extractor-output/customerio-reactnative.api.md index de162768..47125147 100644 --- a/api-extractor-output/customerio-reactnative.api.md +++ b/api-extractor-output/customerio-reactnative.api.md @@ -115,6 +115,7 @@ export class CustomerIO { static readonly pushMessaging: CustomerIOPushMessaging; static readonly registerDeviceToken: (token: string) => Promise; static readonly screen: (title: string, properties?: Record) => Promise; + static readonly setDeepLinkRoutingReady: () => void; static readonly setDeviceAttributes: (attributes: Record) => Promise; static readonly setProfileAttributes: (attributes: Record) => Promise; static readonly track: (name: string, properties?: Record) => Promise; diff --git a/customerio-reactnative.podspec b/customerio-reactnative.podspec index 8d443dbe..74cd05e2 100644 --- a/customerio-reactnative.podspec +++ b/customerio-reactnative.podspec @@ -14,6 +14,9 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/customerio/customerio-ios.git", :tag => "#{s.version}" } + # The generated Swift compatibility header imports + # in static-library builds. + s.header_dir = "customerio_reactnative" s.source_files = "ios/wrappers/**/*.{h,m,mm,swift}" # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. diff --git a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift index 1b3546bf..03653c39 100644 --- a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift +++ b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift @@ -21,23 +21,45 @@ enum CustomerIOReactNativeDeepLinkRouter { private static var isReactNativeReady = false private static var pendingUrls: [PendingUrl] = [] + private static var hasSceneManifest: Bool { + guard let manifest = Bundle.main.object(forInfoDictionaryKey: sceneManifestKey) as? [String: Any], + let configurations = manifest[sceneConfigurationsKey] as? [String: Any] + else { return false } + + return !configurations.isEmpty + } + /// Mirrors `RCTIsSceneDelegateApp()` without linking to the internal symbol, and also requires /// React Native's scene-Linking entry point. A scene manifest alone does not make older React /// Native versions scene-aware. static var isSceneLifecycleEnabled: Bool { - guard let manifest = Bundle.main.object(forInfoDictionaryKey: sceneManifestKey) as? [String: Any], - let configurations = manifest[sceneConfigurationsKey] as? [String: Any] - else { return false } - guard !configurations.isEmpty, + guard hasSceneManifest, let linkingManager = NSClassFromString("RCTLinkingManager") else { return false } return linkingManager.responds(to: sceneOpenURLContextsSelector) } + /// Expo owns scene-to-Linking forwarding even when its React Native version does not expose + /// the scene selector itself. + static var isExpoSceneLifecycleEnabled: Bool { + hasSceneManifest + } + static func install() { guard isSceneLifecycleEnabled else { return } + installCallback() + } + + /// Expo owns scene-to-Linking forwarding even on React Native versions that do not expose the + /// scene selector themselves. The scene manifest remains required so AppDelegate hosts cannot + /// accidentally opt into this path. + static func installForExpoSceneLifecycle() { + guard isExpoSceneLifecycleEnabled else { return } + installCallback() + } + private static func installCallback() { DIGraphShared.shared.deepLinkUtil.setDeepLinkCallback { url in accept(url) return true @@ -111,7 +133,9 @@ enum CustomerIOReactNativeDeepLinkRouter { stateLock.unlock() DIGraphShared.shared.logger.error( - "Customer.io is opening an SDK deep link externally because React Native did not initialize in time" + "Customer.io is opening an SDK deep link externally because React Native did not " + + "initialize in time. Native-auto-initialized apps must call " + + "CustomerIO.setDeepLinkRoutingReady() after registering their Linking listener" ) UIApplication.shared.open(url) { opened in guard !opened else { return } diff --git a/ios/wrappers/NativeCustomerIO.mm b/ios/wrappers/NativeCustomerIO.mm index 8ff7170a..14cb690b 100644 --- a/ios/wrappers/NativeCustomerIO.mm +++ b/ios/wrappers/NativeCustomerIO.mm @@ -47,6 +47,11 @@ - (void)initialize:(NSDictionary *)config [_swiftBridge initialize:config args:args resolve:resolve reject:reject]; } +- (void)setDeepLinkRoutingReady { + [self assertBridgeAvailable:@"during setDeepLinkRoutingReady"]; + [_swiftBridge setDeepLinkRoutingReady]; +} + - (void)identify:(NSDictionary *)params { [self assertBridgeAvailable:@"during identify"]; [_swiftBridge identify:params]; diff --git a/ios/wrappers/NativeCustomerIO.swift b/ios/wrappers/NativeCustomerIO.swift index fc1ca96e..de42cca6 100644 --- a/ios/wrappers/NativeCustomerIO.swift +++ b/ios/wrappers/NativeCustomerIO.swift @@ -20,6 +20,22 @@ public class NativeCustomerIO: NSObject { CustomerIOReactNativeDeepLinkRouter.install() } + /// Installs deep-link routing for an Expo-owned scene lifecycle. + /// + /// Expo forwards scene URLs into React Native Linking itself, including on React Native + /// versions that do not expose the scene Linking selector used by the generic integration. + @objc + public static func configureExpoSceneDeepLinkRouting() { + guard CustomerIO.shared.implementation == nil else { return } + CustomerIOReactNativeDeepLinkRouter.installForExpoSceneLifecycle() + } + + /// Marks the React Native Linking listener as ready for buffered scene URLs. + @objc + func setDeepLinkRoutingReady() { + CustomerIOReactNativeDeepLinkRouter.markReactNativeReady() + } + /// Ensures that the CustomerIO SDK is initialized before performing operations. /// Logs an error and returns false if the SDK is not initialized. private func ensureInitialized() -> Bool { @@ -59,7 +75,10 @@ public class NativeCustomerIO: NSObject { let sdkConfigBuilder = try SDKConfigBuilder.create(from: config) - if CustomerIOReactNativeDeepLinkRouter.isSceneLifecycleEnabled { + let shouldInstallSceneRouter = + CustomerIOReactNativeDeepLinkRouter.isSceneLifecycleEnabled || + (packageSource == "Expo" && CustomerIOReactNativeDeepLinkRouter.isExpoSceneLifecycleEnabled) + if shouldInstallSceneRouter { _ = sdkConfigBuilder.deepLinkCallback { url in CustomerIOReactNativeDeepLinkRouter.accept(url) return true // React Native Linking cannot report handling; JavaScript owns fallback. diff --git a/ios/wrappers/customerio_reactnative.h b/ios/wrappers/customerio_reactnative.h new file mode 100644 index 00000000..b8e48597 --- /dev/null +++ b/ios/wrappers/customerio_reactnative.h @@ -0,0 +1,6 @@ +#pragma once + +#import + +// Public module header required when CocoaPods builds the wrapper as a static library. Swift's +// generated compatibility header imports this path before exposing the wrapper's Swift types. diff --git a/src/customerio-cdp.ts b/src/customerio-cdp.ts index 4e58a623..ee1e002d 100644 --- a/src/customerio-cdp.ts +++ b/src/customerio-cdp.ts @@ -56,6 +56,16 @@ export class CustomerIO { }); }; + /** + * Signal that the app's React Native Linking listener is ready to receive Customer.io URLs. + * + * This is only needed by UIScene hosts that initialize Customer.io natively, such as Expo + * config-plugin auto-initialization. Call it after registering the Linking listener. + */ + static readonly setDeepLinkRoutingReady = () => { + return withNativeModule((native) => native.setDeepLinkRoutingReady()); + }; + /** Identify a user to start tracking their activity. Requires userId, traits, or both. */ static readonly identify = async ({ userId, diff --git a/src/specs/modules/NativeCustomerIO.ts b/src/specs/modules/NativeCustomerIO.ts index 25988e03..114f7570 100644 --- a/src/specs/modules/NativeCustomerIO.ts +++ b/src/specs/modules/NativeCustomerIO.ts @@ -41,6 +41,7 @@ export interface Spec extends TurboModule { config: NativeBridgeObject, args: NativeBridgeObject ): Promise; + setDeepLinkRoutingReady(): void; identify(params?: NativeBridgeObject): void; clearIdentify(): void; track(name: string, properties?: NativeBridgeObject): void; From aefe06290ab94c0e88f6234b301f0c185681e336 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 26 Aug 2026 06:27:52 -0400 Subject: [PATCH 2/6] docs: clarify Expo scene routing ownership --- README.md | 2 +- ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift | 4 ++-- ios/wrappers/NativeCustomerIO.swift | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 862dede2..3f28c8d7 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ On iOS, a `UIScene` host must install the wrapper's deep-link bridge before Reac NativeCustomerIO.configureSceneDeepLinkRouting() ``` -Then register the app's JavaScript `Linking` URL listener before calling `CustomerIO.initialize`. This ordering is required. The Expo plugin detects its scene lifecycle during initialization, so Expo apps do not call the native installation method themselves. Customer.io push, in-app, and inbox destinations are buffered during cold launch, then delivered through React Native's standard `Linking` API after initialization. If React Native does not initialize within ten seconds, the SDK opens the destination through the system instead of retaining it indefinitely. The listener owns the routing decision: navigate destinations your app handles, and use the app's normal external-browser path for other HTTP(S) destinations. React Native's native URL event has no handled result that the SDK can use for this decision, so a destination published after initialization without a listener is not opened externally, which would risk duplicate navigation. Older React Native versions and AppDelegate-only hosts keep their existing deep-link integration. +Then register the app's JavaScript `Linking` URL listener before calling `CustomerIO.initialize`. This ordering is required. The Expo plugin detects its scene lifecycle during initialization, so Expo app code does not call the native installation method itself. Customer.io push, in-app, and inbox destinations are buffered during cold launch, then delivered through React Native's standard `Linking` API after initialization. If React Native does not initialize within ten seconds, the SDK opens the destination through the system instead of retaining it indefinitely. The listener owns the routing decision: navigate destinations your app handles, and use the app's normal external-browser path for other HTTP(S) destinations. React Native's native URL event has no handled result that the SDK can use for this decision, so a destination published after initialization without a listener is not opened externally, which would risk duplicate navigation. Older React Native versions and AppDelegate-only hosts keep their existing deep-link integration. An Expo app using config-plugin auto-initialization does not call `CustomerIO.initialize`. Register its `Linking` listener, then mark that listener ready instead: diff --git a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift index 03653c39..3095398b 100644 --- a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift +++ b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift @@ -52,8 +52,8 @@ enum CustomerIOReactNativeDeepLinkRouter { } /// Expo owns scene-to-Linking forwarding even on React Native versions that do not expose the - /// scene selector themselves. The scene manifest remains required so AppDelegate hosts cannot - /// accidentally opt into this path. + /// scene selector themselves. Non-empty scene configurations opt UIKit into the scene lifecycle, + /// so the manifest prevents AppDelegate-only hosts from entering this path. static func installForExpoSceneLifecycle() { guard isExpoSceneLifecycleEnabled else { return } installCallback() diff --git a/ios/wrappers/NativeCustomerIO.swift b/ios/wrappers/NativeCustomerIO.swift index de42cca6..a9239a6b 100644 --- a/ios/wrappers/NativeCustomerIO.swift +++ b/ios/wrappers/NativeCustomerIO.swift @@ -27,6 +27,7 @@ public class NativeCustomerIO: NSObject { @objc public static func configureExpoSceneDeepLinkRouting() { guard CustomerIO.shared.implementation == nil else { return } + // Native initialization preserves an existing callback when its config does not provide one. CustomerIOReactNativeDeepLinkRouter.installForExpoSceneLifecycle() } From 346bb93dd08a02f2e0d7f1fa2c90f59b3cb1b54b Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 26 Aug 2026 06:45:33 -0400 Subject: [PATCH 3/6] fix: scope Expo scene routing to Expo scene hosts --- ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift index 3095398b..444d2955 100644 --- a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift +++ b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift @@ -43,7 +43,7 @@ enum CustomerIOReactNativeDeepLinkRouter { /// Expo owns scene-to-Linking forwarding even when its React Native version does not expose /// the scene selector itself. static var isExpoSceneLifecycleEnabled: Bool { - hasSceneManifest + hasSceneManifest && NSClassFromString("EXExpoAppSceneDelegate") != nil } static func install() { @@ -52,8 +52,8 @@ enum CustomerIOReactNativeDeepLinkRouter { } /// Expo owns scene-to-Linking forwarding even on React Native versions that do not expose the - /// scene selector themselves. Non-empty scene configurations opt UIKit into the scene lifecycle, - /// so the manifest prevents AppDelegate-only hosts from entering this path. + /// scene selector themselves. Require both a non-empty scene manifest and Expo's scene delegate + /// class so older Expo apps with unrelated scene configurations keep their existing behavior. static func installForExpoSceneLifecycle() { guard isExpoSceneLifecycleEnabled else { return } installCallback() From f73b460cdf8cdce8d9947aec95b68616b30e0dd2 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 26 Aug 2026 08:35:08 -0400 Subject: [PATCH 4/6] fix: keep scene URL transform available without Live Activities --- ios/wrappers/NativeCustomerIO.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ios/wrappers/NativeCustomerIO.swift b/ios/wrappers/NativeCustomerIO.swift index a9239a6b..b6ddf462 100644 --- a/ios/wrappers/NativeCustomerIO.swift +++ b/ios/wrappers/NativeCustomerIO.swift @@ -2,6 +2,7 @@ import CioAnalytics import CioDataPipelines import CioInternalCommon import CioMessagingInApp +import Foundation @objc(NativeCustomerIO) public class NativeCustomerIO: NSObject { @@ -31,6 +32,18 @@ public class NativeCustomerIO: NSObject { CustomerIOReactNativeDeepLinkRouter.installForExpoSceneLifecycle() } + /// Reports and unwraps a Live Activity widget URL when that module is installed. + /// Keeping this entry point in the base wrapper lets generated scene code remain valid when an + /// incremental prebuild later disables the Live Activities subspec. + @objc + public static func handleLiveActivityWidgetUrl(_ url: URL) -> URL? { + #if CIO_LIVEACTIVITIES_ENABLED + NativeLiveActivities.handleWidgetUrl(url) + #else + url + #endif + } + /// Marks the React Native Linking listener as ready for buffered scene URLs. @objc func setDeepLinkRoutingReady() { From 8664b74f590bcb03d94c04528c45989af4e57683 Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 26 Aug 2026 22:49:31 -0400 Subject: [PATCH 5/6] fix: clarify Expo scene readiness diagnostics --- .../CustomerIOReactNativeDeepLinkRouter.swift | 8 ++++++++ ios/wrappers/NativeCustomerIO.swift | 12 ------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift index 444d2955..b4edc976 100644 --- a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift +++ b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift @@ -69,9 +69,17 @@ enum CustomerIOReactNativeDeepLinkRouter { static func accept(_ url: URL) { stateLock.lock() if !isReactNativeReady { + let isFirstPendingUrl = pendingUrls.isEmpty let pendingUrl = PendingUrl(id: UUID(), url: url) pendingUrls.append(pendingUrl) stateLock.unlock() + if isFirstPendingUrl { + DIGraphShared.shared.logger.info( + "Customer.io buffered an SDK deep link until React Native Linking is ready. " + + "Native-auto-initialized apps must call CustomerIO.setDeepLinkRoutingReady() " + + "after registering their Linking listener" + ) + } DispatchQueue.main.asyncAfter(deadline: .now() + readinessTimeout) { expire(pendingUrl.id) } diff --git a/ios/wrappers/NativeCustomerIO.swift b/ios/wrappers/NativeCustomerIO.swift index b6ddf462..0234b3f7 100644 --- a/ios/wrappers/NativeCustomerIO.swift +++ b/ios/wrappers/NativeCustomerIO.swift @@ -32,18 +32,6 @@ public class NativeCustomerIO: NSObject { CustomerIOReactNativeDeepLinkRouter.installForExpoSceneLifecycle() } - /// Reports and unwraps a Live Activity widget URL when that module is installed. - /// Keeping this entry point in the base wrapper lets generated scene code remain valid when an - /// incremental prebuild later disables the Live Activities subspec. - @objc - public static func handleLiveActivityWidgetUrl(_ url: URL) -> URL? { - #if CIO_LIVEACTIVITIES_ENABLED - NativeLiveActivities.handleWidgetUrl(url) - #else - url - #endif - } - /// Marks the React Native Linking listener as ready for buffered scene URLs. @objc func setDeepLinkRoutingReady() { From 9a5ed33ef472f9f13b8d45374acc0b4342b4e87d Mon Sep 17 00:00:00 2001 From: Shahroz Khan Date: Wed, 26 Aug 2026 23:59:28 -0400 Subject: [PATCH 6/6] fix: diagnose unsupported Expo scene routing --- .../CustomerIOReactNativeDeepLinkRouter.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift index b4edc976..39c11eb3 100644 --- a/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift +++ b/ios/wrappers/CustomerIOReactNativeDeepLinkRouter.swift @@ -52,10 +52,16 @@ enum CustomerIOReactNativeDeepLinkRouter { } /// Expo owns scene-to-Linking forwarding even on React Native versions that do not expose the - /// scene selector themselves. Require both a non-empty scene manifest and Expo's scene delegate - /// class so older Expo apps with unrelated scene configurations keep their existing behavior. + /// scene selector themselves. Prefer the generic React Native capability when available, then + /// fall back to Expo's scene delegate capability. static func installForExpoSceneLifecycle() { - guard isExpoSceneLifecycleEnabled else { return } + guard isSceneLifecycleEnabled || isExpoSceneLifecycleEnabled else { + DIGraphShared.shared.logger.error( + "Customer.io could not install Expo scene deep-link routing because the host " + + "does not expose a supported React Native or Expo scene lifecycle" + ) + return + } installCallback() }