Repository navigation
Mobile: atomic compareAndWriteTokens + deep-link host remap (Sprint 10 PR 10.3) - #93
Merged
Merged
Conversation
…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>
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.
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.InMemoryTokenStorageimpl is trivially atomic._RealStoragestill has a narrow platform-call-latency window between the two awaited platform writes (documented inline); fully closing it needspackage:synchronizedor a platform atomic API._attemptRefreshswitches to the atomic method.refresh_test.dartpin the contract.2. Custom-scheme deep-link routing
signupflow://invitation?token=...(the formatapi/services/email_service.py:1018emits) putsinvitationin the URI host with empty path.go_routermatches by path, so the link would land on/._hostRouteRemapinrouter.dart: when location is/andstate.uri.hostmatches a known route (invitation / reset-password / forgot-password), redirect to the path-form. Handles both URL shapes (host-style + triple-slash path-style) defensively.mobile/integration_test/deep_link_test.dartpushes 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
mobile/scripts/run_integration_tests.sh integration_test/deep_link_test.dartagainst a simulator/emulator — both URL forms route correctly.🤖 Generated with Claude Code