-
Notifications
You must be signed in to change notification settings - Fork 15
feat: add Expo scene deep-link readiness (MBL-2304) #646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e264d44
aefe062
346bb93
f73b460
686e0f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| # <customerio_reactnative/customerio_reactnative.h> in static-library builds. | ||
| s.header_dir = "customerio_reactnative" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 — |
||
| 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 && NSClassFromString("EXExpoAppSceneDelegate") != nil | ||
| } | ||
|
|
||
| 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. 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() | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 — this install path succeeds silently, and the requirement it creates is only observable 10 seconds after the first deep link. For a config-plugin auto-init host, readiness can arrive only via The |
||
|
|
||
| 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 } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| #pragma once | ||
|
|
||
| #import <Foundation/Foundation.h> | ||
|
|
||
| // 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2 — this asserts the TypeScript wrapper forwards to the native module. The logic the PR actually adds is untested:
isExpoSceneLifecycleEnabled(router:45-47)installForExpoSceneLifecycle(router:57-60)shouldInstallSceneRoutercomposition (NativeCustomerIO.swift:92-95)There's no Swift test target in this repo, and #643's E2E builds a plain React Native host rather than an Expo one, so the Expo branch has no coverage at any level. customerio/customerio-flutter has
test/ios27_lifecycle/swift/CustomerIOURLRoutingBehaviorTests.swiftfor the equivalent logic — worth standing up the same here.