Summary

Client-side enforcement of the closed-beta gate. An authenticated account whose profiles.access_status is not approved is held on a lock screen and cannot reach the app until an admin approves it or the user redeems an invite code. This is the mobile half of the backend feature feat-beta-access-gate.

Files Changed

  • lib/features/access/notifiers/access_gate_notifier.dartAccessGateNotifier
    • accessGateProvider. Reads own access_status, exposes redeem(code) via the redeem_invite_code RPC, and subscribes to Realtime on the own profile row.
  • lib/features/access/screens/access_pending_screen.dart — lock screen (pending / rejected states, invite-code field, manual + 20s periodic refresh, logout).
  • lib/core/router/app_router.dart/access/pending route, RouterNotifier listens to accessGateProvider, redirect gates non-approved accounts.
  • lib/l10n/app_{en,it,es}.arbaccess* keys (see localization).

Key Decisions

  • Dedicated provider, not the User model. access_status is read by a standalone accessGateProvider rather than threaded through the large User model and its serialization — minimal blast radius.
  • Fail closed. If the status read errors, the gate treats the account as pending (locked, recoverable via refresh) rather than letting it through.
  • Realtime + polling. Live unlock uses a Postgres-changes subscription on the caller’s own profiles row; a 20s periodic refresh on the pending screen is the fallback if profiles is not in the realtime publication.
  • unknown/loading count as not-approved in the redirect, so an unapproved user never flashes the app; the pending screen shows a spinner while resolving, so approved users only see a brief loader before /home.

Architecture Notes

auth becomes authenticated
  → AccessGateNotifier.load()  (reads profiles.access_status for auth.uid())
  → RouterNotifier listens to accessGateProvider
  → GoRouter.redirect: status != approved  → /access/pending
                       status == approved   → /home

AccessPendingScreen
  → invite code → accessGateProvider.redeem() → redeem_invite_code RPC
                  → ok → status=approved → redirect to /home
  → Realtime UPDATE on own profile (admin approval) → status flips live
  → periodic 20s load() fallback

Realtime prerequisite

Live unlock needs public.profiles in the supabase_realtime publication. If not enabled, approval still works via the manual/periodic refresh. Enabling it must keep RLS so a user only receives their own row — do not broadcast all profile changes.

Invite deep link

The approval/invite email links to NEXT_PUBLIC_APP_INVITE_URL with ?code=. Wiring that universal/app link to open AccessPendingScreen with the code prefilled is a remaining task (see feat-beta-access-gate P1).