Pattern: Identifier-first auth
Universal — email (or username) step → adaptive credentials (sign-in / create / passkey).
Applies to Tasteful iOS (AuthenticationView), product web (LoginPageClient), and future Intrinsic apps with password/passkey auth.
Principles (non-negotiable)
- Platform HIG beats custom chrome (
principles/FOUNDATION.md) — nativeFormfields, editor sheets, toolbar Cancel, swipe-to-dismiss on iOS. - Sub-tasks are sheets (
patterns/EDITOR_SHEET.md) — changing identity, reset password, or any “leave the default” flow is a modal with Cancel, not in-place layout morph on the parent. - Reversible by default — dismiss sheet = parent state unchanged (saved email chip still there).
- Identifier-first copy — read-only email row shows trailing Change (opens sheet); VoiceOver / sheet context use Use a Different Email; not “Use Another Account” unless showing a multi-account list.
- Trust — legal consent on email + create-account steps only; footnote/
text-smsecondary tier, not caption/text-xs.
iOS — screen states
| State | UI | Notes |
|---|---|---|
| No saved email | Email field on main + Continue | First launch |
| Saved email (returning) | Read-only row: email + trailing Change + Continue | First launch / email step only |
| Post sign-out | Credentials — passkey-first when account has enrolled passkeys + Use Password Instead; password-first when none; email row + Change; no auto Face ID | preferIdentifierStepOnNextAuth + shouldUsePasskeyFirst |
| Different email | DifferentEmailEntrySheet / web modal — Change opens sheet; Close restores parent | |
| Credentials | Email row + Change + passkey (when offered) or password; Sign In with Passkey nudge when returning and device supports passkeys — direct ceremony; passkey-first layout only when passkeys enrolled | |
| Forgot password | ForgotPasswordView sheet | Canonical specimen |
Dismiss (iOS auth sheets)
- × in top-trailing toolbar (
AuthSheetDismissButton) — matches Settings, Account, compose sheets in Tasteful - Swipe down remains available
- Not leading Close / Cancel text on dismiss-only sheets
Primary actions (validation)
See patterns/PRIMARY_ACTION.md (universal). Auth-specific specimens below.
Main screen layout (canonical)
One root Form — the only scroll container on AuthenticationView:
- Hero — first
Sectionrow,.listRowBackground(.clear), centered (frame(maxWidth: .infinity)) - Fields —
authFormSections(native grouped rows) - Actions — glass Continue / Sign In in transparent list rows (not
safeAreaInset, not siblingVStack) - Legal —
Sectionfooter on email + create-account steps
Simulators can lie; device-test after any auth layout change.
Hard bans (iOS)
- ❌
.toolbarRole(.editor)on single-screen dismiss-only sheets — editor role expects Cancel + confirmation in the toolbar; otherwise iOS 26 may show spurious back chrome beside Cancel - ❌
ForminsideScrollViewon auth screens — email, password, and create-account fields collapse to zero height on device - ❌ Sibling
VStack+Form+.fixedSize(vertical:)on the main auth screen — Form reports zero height; fields disappear on device - ❌
safeAreaInsetpinning Continue — fights natural auth flow; CTA belongs in-form after fields - ❌ Morph main auth layout when user changes email — always sheet/modal via Use a Different Email
- ❌ Hand-built rounded
TextFieldchrome whenFormworks (use Form inside sheet; main email step may use plain field only when Form-in-ScrollView collapses — prefer chip + sheet instead) - ❌ Skinny text-only “Back to …” as the only escape from a sub-task
- ❌ Clear parent
emailbefore user commits in sheet - ❌ “Use Another Account” on single-saved-email chip
Canonical specimens (Tasteful)
AuthenticationView.swift— chip +DifferentEmailEntrySheetForgotPasswordView— reset flow (same sheet vocabulary)AuthCopy.swift/auth-progressive.ts— shared copy
Web
| State | UI |
|---|---|
| Saved email | Row: email + trailing Change |
| Different email | Modal dialog with Cancel + backdrop dismiss |
| Legal | APP_URLS → https://tasteful.to/terms, https://tasteful.to/privacy |
Agent checklist (before shipping auth UI)
- Compared to
ForgotPasswordView/DifferentEmailEntrySheetsheet pattern - Sub-task dismiss restores parent without data loss
- Copy synced iOS ↔ web (
AuthCopy/AUTH_COPY) - No archive
DESIGN_SYSTEM.md— Intrinsic Design repo + live site only - Device test: sign-out → passkey-first credentials → Change → dismiss → email restored