Skip to content

fix(routing): handle cold-start deep links into sessions - #422

Open
RonenMars wants to merge 1 commit into
feat/live-activity-ios-renderfrom
fix/cold-start-deep-links
Open

fix(routing): handle cold-start deep links into sessions#422
RonenMars wants to merge 1 commit into
feat/live-activity-ios-renderfrom
fix/cold-start-deep-links

Conversation

@RonenMars

@RonenMars RonenMars commented Jul 24, 2026

Copy link
Copy Markdown
Owner

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 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 — without this the feature's primary interaction fails.

Verified pre-existing: getLastNotificationResponseAsync and useLastNotificationResponse appear nowhere in app/, hooks/, or services/, and there is no getInitialURL call 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 sessionNavGuard before pushing, so the session_ready listener does not fire a second navigation to the same place.

On testing

Parsing and precedence live in lib/coldStartDeepLink.ts as 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:unit 924/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, and Integration tests fail 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:

  • Type check — 3 errors: makeSearchStyles in app/conversation/[id].tsx:67, plus two i18n key-signature errors in components/onboarding/steps/ConnectStep.tsx:196,197.
  • i18n — 3 failures in 1 suite.
  • Integration tests — 4 suites / 34 tests, all __tests__/integration/conversation-*.

CI on this branch reports the identical Test Suites: 4 failed, 36 passed / Tests: 34 failed, 211 passed, and tsc reports 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.

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
RonenMars force-pushed the fix/cold-start-deep-links branch from 27a4db5 to b7e37b9 Compare July 25, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant