Skip to content

Mobile: atomic compareAndWriteTokens + deep-link host remap (Sprint 10 PR 10.3) - #93

Merged
tomqwu merged 4 commits into
mainfrom
sprint-10-3-mobile-concurrency
May 13, 2026
Merged

tomqwu merged 4 commits into
mainfrom
sprint-10-3-mobile-concurrency

Conversation

@tomqwu

@tomqwu tomqwu commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

Closes two deferred items from Sprint 9's closeout list (specs/024-sprint-9-completion/spec.md).

1. Mobile refresh inter-write race (residual from #86)

  • SecureTokenStorage.compareAndWriteTokens(expectedGen, access, refresh) → bool. Sync prelude (gen check + bump) runs to completion before any await — eliminates the "another mutator passes the check before we increment" race the old gen-check + two-separate-writes shape had.
  • InMemoryTokenStorage impl is trivially atomic.
  • _RealStorage still has a narrow platform-call-latency window between the two awaited platform writes (documented inline); fully closing it needs package:synchronized or a platform atomic API.
  • _attemptRefresh switches to the atomic method.
  • Two new unit tests in refresh_test.dart pin the contract.

2. Custom-scheme deep-link routing

  • Codex repeatedly flagged that signupflow://invitation?token=... (the format api/services/email_service.py:1018 emits) puts invitation in the URI host with empty path. go_router matches by path, so the link would land on /.
  • Added _hostRouteRemap in router.dart: when location is / and state.uri.host matches a known route (invitation / reset-password / forgot-password), redirect to the path-form. Handles both URL shapes (host-style + triple-slash path-style) defensively.
  • New mobile/integration_test/deep_link_test.dart pushes both forms through GoRouter and asserts each lands correctly with the token preserved.

specs/024-sprint-9-completion/spec.md: both items struck from the deferred list.

Test plan

  • Dart syntax: parses (analyzer runs in CI).
  • CI green (no backend impact).
  • Codex local review pass.
  • Operator runs mobile/scripts/run_integration_tests.sh integration_test/deep_link_test.dart against a simulator/emulator — both URL forms route correctly.

🤖 Generated with Claude Code

tomqwu and others added 4 commits May 13, 2026 16:04
…on test

Two deferred items from Sprint 9's closeout list, both closed here.

1. Mobile refresh inter-write race (residual from #86).
   - SecureTokenStorage gains compareAndWriteTokens(expectedGen,
     access, refresh) → bool. Sync prelude (gen check + bump) runs
     to completion before any await, so the "another mutator passes
     the check before we increment" race that the old gen-check +
     two-separate-writes shape had is eliminated.
   - InMemoryTokenStorage's implementation is trivially atomic
     (no awaits in the body).
   - _RealStorage's still has a narrow platform-call-latency window
     between the two awaited writes — documented inline. Closing it
     fully would need package:synchronized or a platform atomic API.
   - api/api_client.dart's _attemptRefresh switches to the atomic
     method; the old "if gen != genAtStart return stale" + two
     awaits collapses into one call with a clean true/false rejection.
   - Two new unit tests in refresh_test.dart pin the atomic contract:
     intervening write → reject + storage untouched; clean write →
     apply + exactly one gen bump.

2. Custom-scheme deep-link routing.
   - Codex repeatedly flagged that signupflow://invitation?token=...
     (the format api/services/email_service.py:1018 emits) puts
     "invitation" in the URI host with empty path. go_router matches
     by path, so the link would land on / instead of /invitation.
   - Added _hostRouteRemap in router.dart that maps known host names
     (invitation / reset-password / forgot-password) to their path
     equivalents in the redirect callback, defensively handling both
     URL forms (host-style and triple-slash path-style) so emails
     already in flight keep working.
   - New integration test mobile/integration_test/deep_link_test.dart
     pushes both URL forms through GoRouter and asserts each lands
     on the right route with the token preserved.

specs/024-sprint-9-completion/spec.md: both items struck from the
deferred list with strikethrough + pointers to this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…est context

Codex iter 1 caught two real issues:

P1: cold-start host-form deep links don't reach the router's redirect.
go_router normalizes the empty-path platform-default route to "/?..."
during bootstrap, dropping URI.host before the redirect callback runs.
My _hostRouteRemap (still landed in this PR) handles warm-start
re-navigations, but cold-launch from a tapped email link wouldn't
route to /invitation or /reset-password.

Fix: switch the email body URL generation in api/services/email_service.py
to triple-slash form: signupflow:///invitation?token=... (empty
authority, path="/invitation"). Cold-launch now routes correctly via
the standard go_router path-matching. The _hostRouteRemap stays as a
defensive fallback for any production email already in flight with
the old host-form URL.

P2: GoRouter.of(tester.element(find.byType(SignUpFlowApp))) couldn't
find the router because SignUpFlowApp is the ANCESTOR of
MaterialApp.router (where InheritedGoRouter is installed). Switched
to finding MaterialApp directly so the test pulls the router from a
context below the routed subtree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lpers

Codex iter 2 P2x2:

- InvitationScreen.initState calls invitationRepositoryProvider.verify
  immediately on /invitation route. Without an override, the
  integration test hit the real backend → Dio connect timeout flakes.
  Added FakeInvitationRepo that returns a canned InvitationPreview
  matching the actual class shape (token, orgId, invitedName,
  invitedEmail, invitedBy, roles). _launch override now includes
  invitationRepositoryProvider.

- Very Good Analysis rejects no_leading_underscores_for_local_identifiers.
  Renamed _launch → launchApp, _routerOf → routerOf, _currentLocation
  → currentLocation. Also fixed router.dart's `var loc` → `final loc`
  (never reassigned).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tomqwu
tomqwu merged commit 5714a28 into main May 13, 2026
4 checks passed
@tomqwu
tomqwu deleted the sprint-10-3-mobile-concurrency branch May 13, 2026 21:13
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