ios+mac: fold the sign-in doors away once signed in (#1146)
A landed sign-in kept all three doors on screen under the status line — 'Signed in' with a full menu of ways to sign in again. Now signed in means no sign-in options: sealed keeps exactly one control (the passkey — it's the unlock, not an alternative), and a quiet 'Sign in a different way' escape drops only the home session for the wrong-account landing (signOut is the full local wipe — oversized for a first-run screen).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+62
−1
ios/LChat/Views/SignInWithEleven.swift+28 −0ios/LChatMac/MacHomeSignIn.swift+34 −1
diff --git a/ios/LChat/Views/SignInWithEleven.swift b/ios/LChat/Views/SignInWithEleven.swift| index 26b19fb..0d8a49e 100644 |
| --- a/ios/LChat/Views/SignInWithEleven.swift |
| +++ b/ios/LChat/Views/SignInWithEleven.swift |
| @@ -106,6 +106,15 @@ final class HomeSignInModel: ObservableObject { |
| phase = .idle |
| } |
| + /// The wrong-door escape on the first-run screen: drop ONLY the home |
| + /// session — nothing has been restored to this device — and bring the |
| + /// doors back for a sign-in that landed on the wrong account. |
| + func switchAccount() { |
| + HomeSessionStore.clear() |
| + sessionExists = false |
| + phase = .idle |
| + } |
| + |
| /// Session adoption — the ONE seam every door funnels a finished sign-in |
| /// through (passkey ceremony finish, Apple verify, email verify): the |
| /// same Keychain write, the published flag every surface watches, and |
| @@ -434,6 +443,8 @@ struct SignInFirstRun: View { |
| .padding(.bottom, 48) |
| } else if let step = emailStep { |
| emailDoor(step) |
| + } else if model.phase.signedIn { |
| + landed |
| } else { |
| doors |
| } |
| @@ -466,6 +477,23 @@ struct SignInFirstRun: View { |
| return order |
| } |
| + // A landed sign-in folds the doors away entirely — signed in means no |
| + // sign-in options. Sealed keeps exactly one control, the passkey, |
| + // because it is the unlock rather than an alternative; the quiet escape |
| + // below it covers the wrong-account landing either way. |
| + private var landed: some View { |
| + VStack(spacing: 12) { |
| + if model.phase == .sealed { |
| + door(.passkey, leading: true) |
| + } |
| + Button("Sign in a different way") { model.switchAccount() } |
| + .font(.subheadline.weight(.medium)) |
| + .foregroundStyle(.secondary) |
| + } |
| + .padding(.horizontal, 32) |
| + .padding(.bottom, 8) |
| + } |
| + |
| private var doors: some View { |
| VStack(spacing: 12) { |
| ForEach(doorOrder, id: \.self) { method in |
diff --git a/ios/LChatMac/MacHomeSignIn.swift b/ios/LChatMac/MacHomeSignIn.swift| index 1e25183..3158da8 100644 |
| --- a/ios/LChatMac/MacHomeSignIn.swift |
| +++ b/ios/LChatMac/MacHomeSignIn.swift |
| @@ -134,6 +134,17 @@ final class MacHomeSignIn: ObservableObject { |
| } |
| } |
| + /// The wrong-door escape on the first-run screen: drop ONLY the home |
| + /// session — nothing has been restored to this Mac yet — and bring the |
| + /// doors back. signOut(from:) is the full local wipe + relaunch, wildly |
| + /// oversized for a sign-in that just landed on the wrong account. |
| + func switchAccount() { |
| + HomeSessionStore.clear() |
| + sessionExists = false |
| + sessionSealed = false |
| + phase = .idle |
| + } |
| + |
| /// Re-read the Keychain session: another surface's model (the first-run |
| /// hero, the browser handoff) may have signed in or out since this one was |
| /// created — Settings' copy went stale and hid its Sign Out button. |
| @@ -551,13 +562,16 @@ struct MacFirstRun: View { |
| .padding(.bottom, 56) |
| } else if let step = emailStep { |
| emailDoor(step) |
| + } else if model.phase.signedIn { |
| + landed |
| } else { |
| doors |
| } |
| } |
| .frame(maxWidth: .infinity, maxHeight: .infinity) |
| // A landed sign-in (any door) folds the email door away; the doors |
| - // return beneath the status — sealed promotes the passkey to unlock. |
| + // fold too (signed in = no sign-in options) — sealed keeps only the |
| + // passkey, promoted as the unlock. |
| .onChange(of: model.phase) { _, p in |
| if p.signedIn { |
| emailStep = nil |
| @@ -583,6 +597,25 @@ struct MacFirstRun: View { |
| return order |
| } |
| + // A landed sign-in folds the doors away entirely — signed in means no |
| + // sign-in options. Sealed keeps exactly one control, the passkey, |
| + // because it is the unlock rather than an alternative; the quiet escape |
| + // below it covers the wrong-account landing either way. |
| + private var landed: some View { |
| + VStack(spacing: 10) { |
| + if model.phase == .sealed { |
| + door(.passkey, leading: true) |
| + } |
| + Button("Sign in a different way") { model.switchAccount() } |
| + .buttonStyle(.plain) |
| + .font(.subheadline.weight(.medium)) |
| + .foregroundStyle(.secondary) |
| + .padding(.vertical, 2) |
| + } |
| + .frame(width: 280) |
| + .padding(.bottom, 48) |
| + } |
| + |
| private var doors: some View { |
| VStack(spacing: 10) { |
| ForEach(doorOrder, id: \.self) { method in |