fix(routing): handle cold-start deep links into sessions - #422
Open
RonenMars wants to merge 1 commit into
Open
Conversation
RonenMars
force-pushed
the
fix/cold-start-deep-links
branch
from
July 25, 2026 00:14
344bae9 to
08cd2f4
Compare
RonenMars
force-pushed
the
feat/live-activity-ios-render
branch
from
July 25, 2026 21:22
02ee981 to
d21f19d
Compare
RonenMars
force-pushed
the
fix/cold-start-deep-links
branch
from
July 25, 2026 21:22
08cd2f4 to
27a4db5
Compare
A tap that launches the app from a terminated state landed on / instead of the session it named. The warm paths cover a running app only: expo-router consumes a URL delivered to a live process, and addNotificationResponseReceivedListener is registered after the launch event has already been consumed. Neither observes the event that started the process, so the destination was silently dropped. This mattered little while notifications were the only entry point, but a Lock Screen or Live Activity tap is overwhelmingly a cold start, which would have made the feature's primary interaction fail. Reads the launch URL and the last notification response once, after servers hydrate and the navigator has a key. Both conditions are load-bearing: pushing before the route tree exists drops the navigation, and pushing before hydration races AuthGate's redirect. A ref guards the effect so it resolves once per launch rather than on every dependency change. The URL wins when both are present, since a Live Activity tap always carries one. The session is marked in the nav guard before pushing so the session_ready listener does not fire a second navigation to the same place. Parsing and precedence live in lib/coldStartDeepLink.ts as pure functions. Rendering the root layout under test would pull in the whole app tree, so keeping the branching outside the effect is what makes it verifiable.
RonenMars
force-pushed
the
fix/cold-start-deep-links
branch
from
July 25, 2026 21:43
27a4db5 to
b7e37b9
Compare
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.
Stacked on #421.
A tap that launches the app from a terminated state landed on
/instead of the session it named.The gap
The warm paths cover a running app only.
expo-router consumes a URL delivered to a live process, and
addNotificationResponseReceivedListeneris registered after the launch event has already been consumed.Neither observes the event that started the process, so the destination was silently dropped.
This mattered little while notifications were the only entry point, but a Lock Screen or Live Activity tap is overwhelmingly a cold start — without this the feature's primary interaction fails.
Verified pre-existing:
getLastNotificationResponseAsyncanduseLastNotificationResponseappear nowhere inapp/,hooks/, orservices/, and there is nogetInitialURLcall either.The fix
Reads the launch URL and the last notification response once, after servers hydrate and the navigator has a key.
Both conditions are load-bearing: pushing before the route tree exists drops the navigation, and pushing before hydration races
AuthGate's redirect.A ref guards the effect so it resolves once per launch rather than on every dependency change.
The URL wins when both are present, since a Live Activity tap always carries one.
The session is marked in
sessionNavGuardbefore pushing, so thesession_readylistener does not fire a second navigation to the same place.On testing
Parsing and precedence live in
lib/coldStartDeepLink.tsas pure functions, and that is where the tests are.Rendering the root layout under test would pull in the whole app tree — there is no precedent for it in this repo — so keeping the branching outside the effect is what makes it verifiable at all.
The effect itself is thin: two reads, one resolve, one push.
Verification
12 unit tests: URL shapes including escaped ids and empty
server, notification payloads, precedence when both are present, and the ordinary-launch case that must stay put.npm run test:unit924/924 green; lint 0 errors; typecheck unchanged from baseline.CI is red on this branch, and it was red before this work
Type check,i18n, andIntegration testsfail here. None of those failures come from this stack.Measured on a clean worktree at
integration-merge-354-355-376(f2242bc4) with no changes applied:makeSearchStylesinapp/conversation/[id].tsx:67, plus two i18n key-signature errors incomponents/onboarding/steps/ConnectStep.tsx:196,197.__tests__/integration/conversation-*.CI on this branch reports the identical
Test Suites: 4 failed, 36 passed/Tests: 34 failed, 211 passed, andtscreports the same 3 errors and no others.Independently confirmed by the repo owner against their own clean worktree.
These are deliberately left alone — fixing them is unrelated to Live Activities and belongs in its own PR.