Repository navigation
fix(ios): deliver warm deep links in UIScene hosts on React Native 0.88 - #525
Open
dustinsoftware wants to merge 1 commit into
Open
dustinsoftware wants to merge 1 commit into
dustinsoftware wants to merge 1 commit into
Conversation
React Native 0.88 returns NO from RCTLinkingManager's app delegate openURL and continueUserActivity entry points when the app declares UISceneConfigurations, so URLs forwarded through brownfield never reached JS while the app was running. When RCTLinkingManager declines, post RCTOpenURLNotification with the same payload React Native uses. The example AppleApp now declares a scene configuration and a URL scheme and forwards URLs from SwiftUI, and the Expo E2E suite opens a deep link while the app is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are recorded for the iOS scene-based deep-link and universal-link delivery changes.
No open issues 🎉
What changed in this PR
Restores delivery of deep links and universal links received while iOS scene-based hosts are running on React Native 0.88, using a shared helper for both Expo and vanilla runtimes.
Changes:
- Adds a notification fallback when React Native’s app-delegate linking methods decline a supported link.
- Configures AppleApp as a scene-based host and forwards SwiftUI link events to brownfield.
- Adds a system-delivered warm deep-link E2E test and a patch changeset.
| File | Description |
|---|---|
packages/react-native-brownfield/ios/Vanilla/ReactNativeHostRuntime.swift |
Routes link handling through the shared helper. |
packages/react-native-brownfield/ios/Expo/ExpoHostRuntime.swift |
Uses the helper while preserving Expo delegate evaluation order. |
packages/react-native-brownfield/ios/BrownfieldLinking.swift |
Implements fallback notifications for URLs and web user activities. |
apps/brownfield-example-shared-tests/e2e/detoxTiming.cjs |
Adds a timeout for the system URL-opening prompt. |
apps/brownfield-example-shared-tests/e2e/appleAppExpoBrownfield.e2e.js |
Tests navigation from a warm deep link. |
apps/brownfield-example-shared-tests/e2e/appleAppDetoxUtils.cjs |
Adds simulator URL delivery, prompt handling, and Explore-screen verification. |
apps/AppleApp/Brownfield-Apple-App-Info.plist |
Registers the URL scheme and explicit scene configuration. |
apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift |
Forwards SwiftUI URL and universal-link callbacks. |
apps/AppleApp/Brownfield Apple App.xcodeproj/project.pbxproj |
Removes generated scene manifests so the explicit configuration is retained. |
.changeset/ios-scene-warm-links.md |
Records the linking fix for a patch release. |
Review effort: Balanced
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR addresses an issue we hit while updating brownfield, expo, and react-native in our production app. As far as I can tell, it shouldn't affect any projects using React native 0.87 and lower. We are running this patch internally to work around the issue.
Summary
In an iOS host that declares
UISceneConfigurationsin its Info.plist and forwards URLs to React Native through brownfield, deep links and universal links that arrive while the app is running never reach JS on React Native 0.88. TheLinkingurlevent does not fire, so Expo Router / React Navigation do not navigate. This PR delivers those links again, without changing behaviour for hosts without scenes or for React Native 0.86 / 0.87.Found with React Native 0.88.1, Expo SDK 58,
@callstack/react-native-brownfield6.1.1, Xcode 27 and an iOS 26.5 simulator. It reproduces onmainwithExpoApp58(React Native 0.88.0-rc.1).Root cause
React Native 0.88 added a scene check to the app delegate entry points of
RCTLinkingManager(Libraries/LinkingIOS/RCTLinkingManager.mm):+application:continueUserActivity:restorationHandler:has the same check.RCTIsSceneDelegateApp()is true for any app whose Info.plist has a non-emptyUISceneConfigurations. Brownfield forwardsReactNativeBrownfield.shared.application(_:open:options:)andapplication(_:continue:restorationHandler:)to those entry points (ExpoHostRuntime/ReactNativeHostRuntime), so in a scene host they returnNOandRCTOpenURLNotificationis never posted. React Native 0.86 and 0.87 have no such check.React Native 0.88's replacement,
+scene:openURLContexts:, needs aUIOpenURLContext, which cannot be constructed in code, and+postNotificationWithURL:is private. SwiftUI's.onOpenURLand many host setups only have aURLat hand.Fix
A small internal helper,
BrownfieldLinking(ios/BrownfieldLinking.swift), used by both host runtimes:RCTLinkingManagerexactly as before.RCTLinkingManagerdeclines (returnsfalse), it postsRCTOpenURLNotificationwithuserInfo: ["url": url.absoluteString], the same payload React Native's own+postNotificationWithURL:posts. For user activities this is limited toNSUserActivityTypeBrowsingWebwith awebpageURL, mirroring React Native's+handleUserActivity:.The fallback is keyed on the return value rather than on
RCTIsSceneDelegateApp()/RCTLinkingManager.scene(_:continue:), because those symbols do not exist in React Native 0.86 / 0.87, which this repo still builds against (ExpoApp57,RNApp). On 0.86 / 0.87, and on 0.88 without scenes,RCTLinkingManagerreturnstrueand the helper does nothing extra, so there is no double delivery from React Native itself. InExpoHostRuntimethe existing evaluation order and result combining are unchanged.Trade-offs
RCTOpenURLNotificationis a React Native internal name. It has been stable for years, but it should be re-checked on React Native upgrades.RCTLinkingManager.scene(_:openURLContexts:)itself, in addition to forwarding to brownfield, would now get the event twice. Before this change the brownfield forward did nothing in that setup. Expo SDK 58'sExpoAppSceneDelegatealready de-duplicates a matchingRCTOpenURLNotification.application(_:open:options:)now returnstruewhen React Native 0.88 declines in a scene app, which matches the pre-0.88 behaviour.Alternative considered
New scene-shaped public entry points on
ReactNativeBrownfieldthat forward toRCTLinkingManager.scene(_:openURLContexts:)/scene(_:continue:). These would only exist on React Native 0.88+, and they still cannot serve hosts that only have aURL(SwiftUI.onOpenURL, custom routers). They could be added later alongside this fallback for UIKitSceneDelegatehosts.Example app changes (needed for the repro)
AppleAppuses SwiftUI's generated scene manifest, whoseUISceneConfigurationsis empty, soRCTIsSceneDelegateApp()was false and the bug was masked. To make the example a real scene host:Brownfield-Apple-App-Info.plist: abrownfield-apple-appURL scheme and an explicitUIApplicationSceneManifestwith oneUIWindowSceneSessionRoleApplicationconfiguration (single scene).project.pbxproj: removedINFOPLIST_KEY_UIApplicationSceneManifest_Generationfrom all targets, because the generated manifest overwrites the one in the Info.plist.BrownfieldAppleApp.swift:.onOpenURLand.onContinueUserActivity(NSUserActivityTypeBrowsingWeb)forward toReactNativeBrownfield.shared.E2E test
appleAppExpoBrownfield.e2e.jsgains "routes a deep link that arrives while the app is running". It opens the Expo home, opensbrownfield-apple-app://explorewithxcrun simctl openurl, accepts the system "Open in ...?" prompt via Detox'ssystemAPI, and expects the Expo Router Explore screen.simctl openurlis used instead ofdevice.openURL, because Detox delivers the latter by calling the app delegate, which bypasses the scene path this PR is about.Only the Expo 58 variant exercises the fix: on Expo 57 (React Native 0.86) the test passes with or without it. The vanilla variant (
RNApp, React Native 0.87) has no router orLinkinglistener, so it has no deep link test. Universal links are not covered by E2E (they need associated domains).Test plan
Local runs on Xcode 27.2 with the Detox
iPhone 17simulator (iOS 27.2):ci:local:appleapp:e2e:ios --variant expo58, after the fixUISceneConfigurationsremoved (host without scenes)ci:local:appleapp:e2e:ios(vanilla, React Native 0.87)ci:local:appleapp:e2e:ios --variant expo57AppleApppasseslaunchOptions: nil, and in scene apps the launch URL is not inlaunchOptions.yarn build,yarn lint,yarn typecheck,yarn test:appsyarn lintwith the generatedpackages/brownfield-navigationfiles frombrownfield:package:iosreverted, asCONTRIBUTING.mddescribes)yarn test:packagesmainas well, unrelated:packages/cli/.../strip-framework-binary.test.tsnestsit()insideit()in a block that only runs on macOSNotes for local runs:
CODE_SIGNING_ALLOWED=NO, and the Expo precompiled frameworks then end up unsigned in the app. On the iOS 26.5 and 27.2 simulator runtimes, dyld refuses to load them (Library missing ... ExpoModulesWorklets.framework). The local runs above ad-hoc signed the built app (codesign --force --sign -) before testing. CI onmacos-26may not hit this.lottie-react-native,ReachabilitySwift), and the Xcode 26.6 install on the test machine has no iOS device platform for the packaging step.Recordings
Detox recordings of the new test were captured for each case below (simulator only, example app only) and can be attached on request:
Possible follow-ups
.onOpenURL/SceneDelegateforwarding toReactNativeBrownfield.shared.RNAppis on React Native 0.88.🤖 Generated with Claude Code