Files
2026-06-28 21:59:59 +03:30

343 lines
26 KiB
Markdown

# Frontend Phase 1 — Auth: phone-OTP login & role routing
> **Mission:** make people able to actually sign in. Replace the inherited username/password stub with
> Balinyaar's real credential — **phone-OTP** — and wire the gate that every other screen sits behind.
> Build the customer login (A1), the OTP screen (A2), the family↔nurse login switch (B1/B2 entry), and
> the **role router** that reads `/me` after sign-in and sends a customer to the family app, a nurse to
> the nurse app, and a brand-new user with no role to role selection. After this phase the app has a real
> front door; f2-b3 (onboarding & profiles) and every authenticated screen build on it.
>
> **Track:** frontend · **Depends on:** [`frontend-phase-0`](./frontend-phase-0.md) (shells, the OTP/phone
> composites, the `services/{domain}` + Query pattern, the cookie manager, `AuthContext`) · **backend
> contract** [`dev/contracts/domains/identity-auth.md`](../../contracts/domains/identity-auth.md) (from
> backend-phase-2) · **Unlocks:** every authenticated screen; f2-b3 onboarding & profiles
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.
---
## 1. Context — where this sits
The client (`client/`, **Next.js 16 App Router + React 19 + MUI v9 + next-intl**, `fa` default & RTL) has
a full app shell, the three actor app shells from f0, and a cookie/fetch/Query/auth stack — but **no way
to log in**. Today's auth domain is a **username/password stub** (`useLogin({ username, password })`
`/auth/login`) with no UI; per the product docs, Balinyaar's only real credential is **phone-OTP**
([`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md):
"Phone number is the primary login credential … Authentication is phone-OTP"). This phase swaps the stub
for the real thing and adds the role router that turns "authenticated" into "in the right app".
**What already exists (do not rebuild) — confirmed in the codebase:**
- **From [`frontend-phase-0`](./frontend-phase-0.md):** the three actor app shells + role-scoped route
groups under `(private-routes)`; the **shared OTP code input** and **Iranian phone-number field**
composites (`src/components/…` with co-located tests); the **`services/{domain}` + TanStack Query**
reference pattern (a `keys.ts` factory, `apis/clientApi.ts` over `clientFetch`, one-hook-per-file,
invalidate-on-mutation); the **types-from-contract** convention; the money/format util; the `auth`,
`common`, `nav` i18n namespaces seeded in **both** `en.json`/`fa.json`. Read the f0 report
([`reports/frontend-phase-0-report.md`](../../shared-working-context/reports/frontend-phase-0-report.md))
for exactly what shipped and how the mock-`clientApi` seam works.
- **The stack (built before f0):** root layout `src/app/[locale]/layout.tsx` (providers NextIntl → Auth →
Theme → Query → Notistack); `clientFetch`/`serverFetch` + `ApiError` (`@/lib/api`) — **401 already
clears cookies + toasts + redirects without throwing; 403/5xx toast+throw; other 4xx throw-no-toast**;
the **cookie manager** (`@/lib/cookies/client``setClientCookie`/`deleteClientCookie`, with
`AUTH_ACCESS_COOKIE_OPTIONS` ≈15 min and `AUTH_REFRESH_COOKIE_OPTIONS` ≈7 d, both **non-httpOnly** by
the current design); `AuthContext` (`useReducer` `[state, dispatch]`, actions `LOG_IN{user?}`/`LOG_OUT`,
server-seeded `initialState`); `makeQueryClient` (staleTime 30 s); the **middleware auth gate**
(`isTokenAlive` on `access_token`, redirects non-public paths to `/${locale}/login`).
- **The auth domain (the stub you are replacing):** `src/services/auth/``types.ts`
(`LoginDto {username,password}`, `AuthTokens {accessToken,refreshToken}`, `User {id,username}`),
`keys.ts` (`authKeys.currentUser()`), `apis/clientApi.ts` (`AuthClientApi.login/logout/getCurrentUser`),
`hooks/useLogin|useLogout|useCurrentUser`, `index.ts`. The login hook writes both tokens as non-httpOnly
cookies and dispatches `LOG_IN`; `useLogout` deletes both, dispatches `LOG_OUT`, redirects to
`/${locale}/login`. **There is no login page yet** — only the hooks. You extend this domain in place.
**What you are replacing / removing in this phase:**
- The username/password `LoginDto` + `useLogin({username,password})` + `POST /auth/login` path — replaced
by the OTP request/verify pair. Remove the dead `username/password` types and the `login` api/hook once
the OTP flow is wired; don't leave both (no-unused-vars is a lint **error**).
> **Wireframe reality check:** A1/A2 (customer) and B1/B2 (nurse) are the **same OTP mechanism** with
> different copy and a different post-login destination. Build **one** OTP flow and parameterise the
> intended-role/copy — do not fork two parallel login stacks.
## 2. Required reading (do this first)
- [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and
[`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md) — how you
work, the gate, the §6 contract-gap handoff, the caching/re-render rules.
- **The contract you consume:** [`dev/contracts/domains/identity-auth.md`](../../contracts/domains/identity-auth.md)
(published by **backend-phase-2**) — the **source of truth** for the OTP/refresh/logout/me shapes, routes,
status codes, and the `Me`/roles enum. **Do not guess shapes.** Read it together with
[`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md)
(envelope, `snake_case` routes — `[controller]/[action]` so `RequestOtp``.../request_otp`; 400/401/403/409
meanings; the locale header) and
[`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md)
(enums cross the wire as **stable string codes**`male`/`female`, roles as codes — and the frontend
mirrors them as string-literal unions; **never** hardcode a display label off a code, labels are i18n keys).
**If the contract isn't published yet or a needed shape is missing** (e.g. the exact `Me` role codes, the
resend-cooldown / max-attempts fields), follow operating-rules §6: append a request to
[`dev/shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
and mock behind the `services/auth` seam meanwhile (§4 below).
- [`client/CLAUDE.md`](../../../client/CLAUDE.md) — RSC/client boundary, the cookie rules (**auth state only
through the cookie manager — never `document.cookie`/`localStorage`**), the services pattern, anti-patterns.
- **Invoke the `frontend-designer` skill** before any visual work — A1/A2/B1 are full branded screens
(brand mark, teal `#1d4a40` primary CTA, RTL Persian, Vazirmatn/Mikhak). It is the palette/token/typography
and `App*`-library contract; all the login UI goes through it.
- [`product/wireframes/index.html`](../../../product/wireframes/index.html) — screens **A1, A2, B1, B2**
(and the role-router branch implied by A5 home vs B3 nurse status). The exact Persian strings are there:
A1 CTA **"دریافت کد تایید"**, nurse-switch link **"پرستار هستید؟ ورود پرستاران ←"**; A2 resend
**"ارسال مجدد کد تا ۰۰:۴۵"** + CTA **"تایید و ادامه"**; B1 subtitle
**"ویژه پرستاران دارای پروانه نظام پرستاری"**, CTA **"دریافت کد تایید"**; B2 CTA **"تایید و ورود"**.
- [`product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md) —
the auth/role rules: **phone is the primary credential** (email never a login key), customer browses with
only a verified phone, **sessions are revocable with refresh-token rotation + stolen-token detection**,
admin roles are **internal-only / never self-assigned**.
- The existing `src/services/auth/*`, `src/context/auth/*`, `src/lib/cookies/*`, `src/lib/api/*`,
`src/middleware.ts` — the exact code you extend.
## 3. Scope — build this
A vertical slice: **service → hooks → screens → role router**, all over the published contract.
### 3.1 Rewrite `services/auth` for OTP (replace the username/password stub)
Keep the f0 service shape (`types.ts` / `keys.ts` / `apis/clientApi.ts` / `hooks/use*.ts` / `index.ts`);
swap its contents to OTP. **Mirror the published contract for every shape** — the names below are the
intended surface; reconcile field names/casing against `swagger.json` and file a `for-backend.md` request
for any gap.
- **`types.ts`** — replace `LoginDto` with:
- `OtpRequest``{ phone: string; intended_role?: RoleCode }` (the role the user is logging in *as*,
from the A1/B1 switch — used only for routing/copy; the server decides actual roles).
- `OtpVerify``{ phone: string; code: string }`.
- `Tokens``{ accessToken: string; refreshToken: string }` (rename/keep from `AuthTokens`; match wire casing).
- `Me` — `{ id: number; phone: string; roles: RoleCode[]; active_role?: RoleCode;
profile_completed: boolean; nurse_verification_status?: NurseVerificationStatus }` — the role-router
input. Trim/extend to **exactly** what `GET /me` returns in the contract.
- `RoleCode` — string-literal union `'customer' | 'nurse' | 'admin'` (codes, not labels).
- `NurseVerificationStatus` — string-literal union mirroring the contract's enum (e.g.
`'not_started' | 'in_progress' | 'pending_review' | 'verified' | 'rejected'`); used by the router to
decide whether a nurse lands on B3 status vs the nurse home.
- `OtpRequestResult` — whatever `request_otp` returns (e.g. `{ resend_after_seconds: number;
code_length: number; expires_in_seconds: number }`); drive the **resend countdown** and **OTP box
count** from this, not from a hardcoded `45`/`4`.
- **`keys.ts`** — `authKeys.me()` (replaces `currentUser()`); keep `authKeys.all`.
- **`apis/clientApi.ts`** — `AuthClientApi` over `clientFetch`:
- `requestOtp(body: OtpRequest): Promise<OtpRequestResult>` → `POST .../auth/request_otp`
- `verifyOtp(body: OtpVerify): Promise<Tokens>` → `POST .../auth/verify_otp`
- `refresh(refreshToken: string): Promise<Tokens>` → `POST .../auth/refresh`
- `logout(): Promise<void>` → `POST .../auth/logout`
- `getMe(): Promise<Me>` → `GET .../me`
(Use the **exact snake_case routes** from the contract; the examples follow the `[controller]/[action]`
transform.) Remove the old `login` call.
- **`hooks/` (one per file):**
- `useRequestOtp.ts` — `useMutation` over `requestOtp`. `onSuccess` hands the cooldown/length back to A2.
**Do not toast 401/403/5xx** (the fetch layer does) — only surface **domain 4xx** (e.g. 429
rate-limit "try again in N s", invalid-phone 400) as inline field/screen errors.
- `useVerifyOtp.ts` — `useMutation` over `verifyOtp`. `onSuccess`: **persist tokens via the cookie
manager** (`setClientCookie` with `AUTH_ACCESS_COOKIE_OPTIONS`/`AUTH_REFRESH_COOKIE_OPTIONS`),
dispatch **`LOG_IN`**, **`invalidateQueries(authKeys.me())`**, then trigger the role router (3.4).
Map the contract's wrong-code / expired-code / max-attempts (lockout) failures to inline states.
- `useLogout.ts` — keep behaviour: call `logout`, `deleteClientCookie` both tokens, dispatch `LOG_OUT`,
**`invalidateQueries(authKeys.me())`** (or `queryClient.clear()` for auth keys), redirect to
`/${locale}/login`.
- `useMe.ts` — `useQuery(authKeys.me(), getMe)` (replaces `useCurrentUser`); `enabled` only when
authenticated; sensible `staleTime`. This is the role router's data source and the shell's role source.
- `useRefresh.ts` (DEFERRED token-rotation UI is out of scope, but) — expose `refresh` as a callable so
the **silent refresh** path can rotate the access token when it expires (see 3.5). If the existing
fetch layer is to own refresh, document that instead and don't duplicate it; flag the decision in your
report.
- **`index.ts`** — re-export the new hooks; drop `useLogin`/`useCurrentUser`.
### 3.2 A1 — customer phone login screen
The public login page (the route the middleware redirects to: `/${locale}/login`). Per A1 / the
frontend-designer skill:
- Brand mark + tagline; the **Iranian phone-number field** composite from f0 (placeholder `۰۹۱۲ ۰۰۰ ۰۰۰۰`,
RTL-safe, validates an Iranian mobile).
- Primary CTA **"دریافت کد تایید"** → `useRequestOtp({ phone, intended_role: 'customer' })`; on success,
advance to A2 (carry the masked phone + cooldown + code length).
- Secondary link **"پرستار هستید؟ ورود پرستاران ←"** → switches to the **nurse login** variant (B1).
- States: **sending** (CTA spinner/disabled), **invalid/non-Iranian number** (inline), **rate-limit hit**
("تا N ثانیه دیگر دوباره تلاش کنید"), **network error** (the fetch layer toasts; keep the field intact).
### 3.3 B1/B2 — nurse login switch (same flow, nurse intent)
Not a separate stack — the **same** login screen with `intended_role: 'nurse'`, nurse copy (B1 subtitle
**"ویژه پرستاران دارای پروانه نظام پرستاری"**, OTP CTA **"تایید و ورود"**), and a link back to family
login. Carry `intended_role` through A1→A2 (B1→B2) so the OTP screen shows the right CTA and the router
knows the entry intent. Implement the variant via a query param or a small client switch on the login
page — **no second route tree**.
### 3.4 A2 — OTP screen + auto-advance
Per A2/B2, using the **shared OTP code input** from f0:
- N-digit boxes (N from `OtpRequestResult.code_length`, default 4 per the wireframe), **masked phone echo**
("کد به شماره ۰۹۱۲•••۰۰۰۰ ارسال شد"), **resend countdown** **"ارسال مجدد کد تا ۰۰:۴۵"** driven by
`resend_after_seconds` (a single timer, cleaned up on unmount — no leaked intervals).
- CTA **"تایید و ادامه"** (customer) / **"تایید و ورود"** (nurse) → `useVerifyOtp`.
- States: **auto-advance** (verify fires automatically when the last box is filled), **wrong code**
(inline, clear boxes, keep focus), **expired code** (offer resend), **max-attempts lockout** (disable
input + show the locked message + when it lifts), **resend** (re-calls `useRequestOtp`, restarts the
countdown, disabled until the countdown hits 0).
- On verify success the hook stores tokens + dispatches `LOG_IN` + invalidates `/me`; then the **role
router** takes over (next).
### 3.5 The role router (after `/me`)
A small client component/hook (`useRoleRouter` or a `RoleRouter` boundary placed where the shell decides
chrome) that consumes `useMe()` and routes:
- **loading** (`/me` in flight) → the f0 `AppLoading` splash; never flash the wrong shell.
- **roles includes `nurse`** → the **nurse app** home; if `nurse_verification_status` is not `verified`,
land on **B3 verification status** (built in f5) — for now route to the nurse shell's home/placeholder
and leave a clear pointer (f5 fills B3). A persistent "verification in progress" banner is **f5's** job;
here just route correctly.
- **roles includes `customer`** (and not nurse, or `active_role === 'customer'`) → the **customer/family
app** home (A5, built in f4 — route to the customer shell's home/placeholder now).
- **roles empty / no usable role** → the **SelectRole** screen (3.6).
- **`active_role`** (if the contract returns one) wins when a user has multiple roles; otherwise prefer the
`intended_role` carried from login, else default customer.
- **admin** → the admin shell (f15) — route correctly but don't build admin screens here.
Honour the **middleware gate**: it already redirects unauthenticated users to `/login`; the role router
runs **after** auth and only decides *which app*. Don't duplicate the auth check in the router.
### 3.6 SelectRole screen (first use)
A minimal post-login screen for a brand-new user with **no role**: choose **خانواده (customer)** or
**پرستار (nurse)** → call the role-selection mutation (`POST /me/role`, `SelectRoleCommand` per the
digest; consume the contract's exact route/body) → `invalidateQueries(authKeys.me())` → re-run the router.
Admin is **never** selectable here (admin roles are internal-only). If `intended_role` from login is set,
pre-select it. Keep this screen shared/simple; the full onboarding (A3 "who is care for", A4 add-patient,
nurse profile bootstrap) is **f2-b3** — link it, don't build it.
### 3.7 Seed `AuthState` with roles
Extend the auth context beyond `{ id, username }`:
- `src/context/auth/types.ts` — extend `AuthState.currentUser` to carry `{ id; phone; roles: RoleCode[];
active_role?: RoleCode }` (mirror the `Me` essentials the shell needs for chrome) and keep
`isAuthenticated`. Keep the reducer's `LOG_IN{user?}`/`LOG_OUT` actions; just widen the `user` payload.
- The server-seeded `initialState` (root layout) should seed roles when it can derive them
(it reads cookies/`getServerAuthState`); if roles aren't available server-side yet, seed
`isAuthenticated` only and let `useMe()` hydrate roles client-side — **don't** put a second source of
truth for roles. Document which it is in your report.
- The f0 shells already read "a role" to pick chrome — now feed them the real `roles`/`active_role`.
### 3.8 i18n
All A1/A2/B1/B2/SelectRole/role-router strings as keys in the **`auth`** namespace (and `common`/`nav`
for shared bits) in **both** `messages/en.json` and `messages/fa.json`, in sync, RTL-first. No label is
ever derived from a role/status **code** in code — codes map to i18n keys. (DEFERRED) onboarding copy
(A3/A4) belongs to f2's `onboarding` namespace.
## 4. Mocks & seams in this phase
This phase **introduces no new external-service seam** — OTP/SMS delivery is the **backend's** seam
(`ISmsSender`, introduced in backend-phase-2; the frontend never sends SMS). The only frontend "mock" is
the standard **contract-gap fallback** from operating-rules §6 and the f0 pattern:
- **If [`identity-auth.md`](../../contracts/domains/identity-auth.md) / the b2 swagger snapshot isn't
merged when you start**, build a **mock `AuthClientApi`** behind the same `services/auth` seam (the f0
mock-`clientApi` pattern): `requestOtp` returns a fixed `{ resend_after_seconds: 45, code_length: 4,
expires_in_seconds: 120 }`; `verifyOtp` accepts a fixed dev code (e.g. `0000`) and returns fake tokens
+ a `Me` you can toggle (customer / nurse-unverified / no-role) to exercise all three router branches;
`getMe` returns the matching `Me`. Selection is by config (env/flag), **never** an `if (mock)` in a
hook or screen.
- **Record it:** append a row to
[`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) (seam =
`AuthClientApi` mock + file, what's faked, the config key, exact swap steps = point the service at the
real `clientApi` once the contract is live) and a follow-up in your phase report.
- Any shape the contract doesn't cover (resend/lockout fields, `Me` role codes, `active_role`) →
**append a request** to
[`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md); do **not** edit
backend files.
(Reuse the f0 mock-seam mechanism — don't invent a new one.)
## 5. Critical rules you must not get wrong
- **Phone is the only credential.** No username/password anywhere after this phase; email is never a login
key. Remove the stub `login` path completely (no dead code — it's a lint error).
- **No token in `localStorage`, ever.** Tokens live **only** in cookies via the cookie manager
(`@/lib/cookies/client`), non-httpOnly per the current design. Never `document.cookie`/`localStorage`
for auth. Logout must actually clear both cookies *and* the server session (call `logout`).
- **Sessions are revocable; refresh rotates.** Wire `refresh` so an expired access token is silently
rotated (or confirm the fetch layer owns this and don't duplicate it). The server does reuse-detection;
the client must send the current refresh token and replace **both** tokens on a successful refresh.
- **`invalidateQueries(authKeys.me())` on login and logout.** `/me` is the role router's input — a stale
`/me` routes the wrong person to the wrong app. Never read roles from two sources.
- **Don't toast 401/403/5xx in hooks** — the fetch layer already does. Hooks surface only **domain 4xx**
(invalid phone, rate-limit/429, wrong/expired code, lockout) as inline UI states.
- **OTP states are explicit, not edges:** sending, resend-cooldown, wrong code, expired code,
**max-attempts lockout**, auto-advance. A single timer, cleaned up on unmount (no leaked intervals →
no re-render storms).
- **The router never flashes the wrong shell.** Show `AppLoading` while `/me` is in flight; only render a
shell once the role is known. **admin role is never self-assignable** — SelectRole offers customer/nurse
only.
- **RSC/client boundary:** login screens, OTP, the router, and the cookie writes are **client**
(`'use client'`); no `next/headers`/`@/lib/cookies/server` in them. The middleware already gates routes —
don't re-implement the gate in a component.
- **Both locales, RTL-first, tokens for colour, MUI v9, MUI primitives stay MUI.** Every string in
`en.json` **and** `fa.json`; no label derived from a code.
- **Minimise re-renders:** the countdown timer and per-box OTP state stay **low** (in the OTP component),
not in a high context; `useMe` subscribers use `select` if they only need `roles`.
## 6. Definition of Done
The shared [definition-of-done.md](../_shared/definition-of-done.md), plus:
- [ ] `services/auth` is OTP-based: `OtpRequest`/`OtpVerify`/`Tokens`/`Me`(+roles) types, `authKeys.me()`,
`requestOtp`/`verifyOtp`/`refresh`/`logout`/`getMe` apis, and the `useRequestOtp`/`useVerifyOtp`/
`useMe`/`useLogout` hooks — the username/password stub is **removed**, not left dangling.
- [ ] A1 (customer login), A2 (OTP, auto-advance, resend countdown, lockout), and the **B1/B2 nurse
switch** render per the wireframe via the f0 phone/OTP composites, RTL, both locales, branded.
- [ ] The **role router** sends customer→family app, nurse→nurse app (B3 status if unverified),
no-role→**SelectRole**, admin→admin shell; it shows `AppLoading` during `/me` and never flashes the
wrong shell.
- [ ] Tokens are stored **only** via the cookie manager; `LOG_IN`/`LOG_OUT` dispatched; `authKeys.me()`
invalidated on login **and** logout; refresh wired (or fetch-layer ownership documented).
- [ ] `AuthState` carries `roles`/`active_role`; the f0 shells pick chrome from the real role.
- [ ] All strings in `auth`/`common`/`nav` in **both** `en.json` and `fa.json`, in sync.
- [ ] `npm run check` green; `npm run test:ci` green (you touched/added shared composites and the
router — add tests for the OTP state machine and the router branches).
- [ ] `client/CLAUDE.md` updated: the OTP login flow, the role-router seam, and the widened `AuthState`
noted in *Project Structure* / the auth notes; the now-stale username/password references corrected.
## 7. How to test (what a human can verify after this phase)
With the b2 backend running (or the mock seam configured), `npm run dev`:
- **Customer happy path:** open `/fa/login` → enter a valid mobile → "دریافت کد تایید" → A2 shows the
masked phone + a counting-down resend → enter the code (auto-advance fires verify) → tokens appear in
cookies (DevTools → Application → Cookies; **nothing** in localStorage) → redirected to the **customer**
app home. `/me` is in the Query cache (React Query Devtools).
- **Nurse switch:** from A1 tap "پرستار هستید؟ ورود پرستاران ←" → B1 nurse copy/subtitle → request →
verify ("تایید و ورود") → routed to the **nurse** app (B3 status placeholder if unverified).
- **No-role user:** verify as a user whose `/me` has empty `roles` → lands on **SelectRole** → choose a
role → re-routed into that app.
- **OTP edge states:** wrong code → inline error + boxes clear; let the code expire → resend re-enabled by
the countdown; trigger max attempts → input locks with a clear message.
- **Session:** log out → both cookies cleared, `/me` invalidated, redirected to `/login`; let the access
token expire and act → silent refresh rotates it (or you're sent to `/login` cleanly).
- **i18n/RTL:** switch to `en` → all auth strings translate, layout stays correct; `fa` is RTL.
- `npm run check` and `npm run test:ci` pass (OTP state-machine + router-branch tests included).
## 8. Hand off & document (close the phase)
- **Docs:** update [`client/CLAUDE.md`](../../../client/CLAUDE.md) — the phone-OTP flow, the `useRoleRouter`
seam, the widened `AuthState` (roles/active_role), and the new auth screens/route; **fix the stale
username/password references**. If you add a route group/folder, update *Project Structure* in the same
change. No `product/` rule changes expected (you're implementing decided rules); if you discover drift,
record it there.
- **Contract:** **consume** [`dev/contracts/domains/identity-auth.md`](../../contracts/domains/identity-auth.md)
— types come from it, not guesses. Append every gap (resend/lockout fields, exact `Me` role codes,
`active_role`, role-selection route/body) to
[`dev/shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md).
Frontend produces **no** contract.
- **Handoff & report:** append to
[`dev/shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md);
write [`reports/frontend-phase-1-report.md`](../../shared-working-context/reports/frontend-phase-1-report.md)
(what was built, **what is now testable and exactly how**, what's mocked behind the `AuthClientApi` seam
+ how f-next swaps it, the contract consumed + gaps filed, follow-ups: B3 banner is f5, A3/A4 onboarding
is f2, admin screens are f15). Update
[`reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) for the
`AuthClientApi` mock if you used it.
- **Memory:** save a `project` memory note for the phone-OTP decision, the role-router seam + branch logic,
and the widened `AuthState`, with a `MEMORY.md` pointer — what a future agent can't cheaply re-derive.