critical priority medium complexity infrastructure pending infrastructure specialist Tier 0

Acceptance Criteria

DeepLinkHandler is a Dart service class in lib/core/deep_links/ initialised once at app startup (before GoRouter is used)
Cold-start: when the app is launched from a referral link, getInitialLink() is called and if a referral URI is detected the router navigates to the onboarding screen with the token parameter before displaying any other screen
Foreground-resume: the service subscribes to uriLinkStream and on receiving a referral URI navigates to the onboarding screen with the token parameter; non-referral URIs are ignored
Referral URIs are identified by path matching /referral/:token (both custom scheme and HTTPS universal/app links)
The extracted token is passed as a GoRouter path or query parameter to the new-member onboarding route
Unrecognised deep links are logged (debug only) and not processed further — existing GoRouter routes handle them normally
The service disposes its StreamSubscription when no longer needed to prevent memory leaks
Both iOS and Android platform configurations are updated: iOS Associated Domains (or custom URL scheme in Info.plist) and Android intent-filter in AndroidManifest.xml are documented (or configured) for the referral URI scheme
The handler is covered by unit tests with a mocked app_links AppLinks instance
No crash occurs when the link contains a malformed token or missing query parameter — the handler logs a warning and does not navigate

Technical Requirements

frameworks
Flutter
app_links (AppLinks, uriLinkStream, getInitialLink)
go_router (GoRouter, router.go / router.push)
apis
iOS Universal Links (apple-app-site-association)
Android App Links (Digital Asset Links) or custom URL scheme
data models
ReferralCode (token extraction only)
performance requirements
Cold-start link processing must complete before the first frame is painted (use getInitialLink in a FutureBuilder or before runApp returns)
Stream subscription must not delay foreground link processing by more than 50 ms
security requirements
The extracted token must be treated as untrusted input — length-limit to 128 characters and strip non-alphanumeric characters before passing to the router
Universal Links must be preferred over custom schemes on iOS to prevent URI scheme hijacking by other apps
Token must be validated against the backend (via ReferralCodeRepository.lookupByCodeString) before proceeding with onboarding — do not assume a token is valid because it was received via deep link

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Initialise DeepLinkHandler as a singleton Riverpod Provider and call handler.init(router) inside the top-level ProviderScope's override or in the root widget's initState — before the first frame. For cold-start, use `await AppLinks().getInitialLink()` inside a `WidgetsBinding.instance.addPostFrameCallback` to ensure the router is mounted. For the stream subscription, use `AppLinks().uriLinkStream.listen(...)` and store the subscription for cancellation. Define the referral route in GoRouter as `/referral/:token` and use `router.go('/referral/$token')` for navigation.

Document the required Info.plist and AndroidManifest.xml changes in a companion README or inline code comments so the platform configuration is not overlooked during CI/CD setup. Be careful not to intercept links intended for BankID or Vipps authentication callbacks — check the URI host and path before claiming the link.

Testing Requirements

Unit tests with flutter_test and mocktail: (1) a referral URI received on uriLinkStream triggers router navigation with the correct token; (2) a non-referral URI on uriLinkStream is ignored (no navigation); (3) getInitialLink returning a referral URI triggers cold-start navigation; (4) getInitialLink returning null does not navigate; (5) a malformed URI (no token) logs a warning and does not navigate or crash; (6) StreamSubscription is cancelled when the service is disposed. Widget/integration test verifying GoRouter pushes to the correct named route with the token query parameter. Manual QA on a physical iOS device (TestFlight build) and Android device covering both cold-start and foreground-resume scenarios.

Component
Deep Link / OAuth Redirect Handler
infrastructure medium
Epic Risks (3)
high impact medium prob technical

iOS Universal Links and Android App Links have distinct configuration requirements (apple-app-site-association, assetlinks.json, entitlements). A misconfiguration causes the OS to open the referral URL in a browser instead of the app, completely breaking the onboarding funnel for new members on one platform.

Mitigation & Contingency

Mitigation: Configure both Universal Links and App Links from the start of this epic using the project's existing Supabase-hosted domain. Write an E2E test on both simulators that taps a referral URL and asserts the onboarding screen is reached. Document the required server-side JSON files alongside the migration.

Contingency: If platform deep-link configuration cannot be resolved before the UI epics need the handler, implement a fallback custom-scheme URI (e.g., likeperson://referral?code=XYZ) that works unconditionally, and schedule Universal/App Link fix as a follow-up task.

high impact high prob security

Referral click events must be writable without an authenticated session (a new member who has not yet registered is tapping the link). Standard Supabase RLS cannot grant anonymous inserts without opening a security hole. If this is not solved early it blocks the entire attribution pipeline.

Mitigation & Contingency

Mitigation: Design referral_events writes to go exclusively through a Supabase Edge Function that validates the referral code exists and is active before inserting. The Edge Function uses the service-role key server-side; the client only calls the function endpoint. This is documented in the feature spec.

Contingency: If the Edge Function approach is delayed, temporarily allow anon inserts restricted by a CHECK constraint that event_type = 'click' and new_member_id IS NULL, then tighten to Edge Function writes in a follow-up migration before the feature goes to production.

medium impact low prob dependency

The qr_flutter package version pinned in pubspec may conflict with the current Flutter SDK version or with other packages in the monorepo, causing build failures that block QR code delivery.

Mitigation & Contingency

Mitigation: Verify qr_flutter compatibility against the project's Flutter SDK version as the very first task in this epic. If a conflict exists, resolve it before any other work proceeds.

Contingency: If qr_flutter cannot be made compatible, evaluate mobile_scanner (already likely in pubspec for QR scanning) which also supports generation, or implement QR generation via a lightweight Dart port as a last resort.