Rebuild backup and restore screens on standard SwiftUI form controls - #551
Merged
Merged
Conversation
The backup screens had drifted away from the rest of the app. They were built from a `ScrollView` of hand-rolled cards, each wrapped in `VaultCardModifier` with a coloured border, a tinted icon tile, and a full-width filled button inside the card. Nothing else in the app looks like that — every other screen, including the note editing flow, uses `Form` with `Section`s, `FormRow`, and section footers for explanatory copy. All three screens now use the standard controls: - `BackupCreateView` — one `Section` per capability, with the explanatory copy moved into section footers. - `BackupRestoreView` — merge and override become plain rows. The override path keeps its destructive emphasis through standard red row text rather than a red card border and filled button. The "Recommended" capsule badge becomes the merge section's header. - `AutoBackupSettingsView` — now vends a `Section` into its parent's form rather than rendering its own card. The status line moved into the section footer, and the retention control switched from a segmented picker to the standard form picker used elsewhere in settings. - `BackupKeyChangeView` — password entry becomes ordinary `SecureField` rows, keygen status moves to the section footer, and the details disclosure groups sit in a normal section. Behaviour is unchanged throughout. Every action, sheet, task, publisher subscription and ordering is preserved — including loading the backup password before presenting any import sheet, which is now done once in a shared row builder instead of at each call site. `VaultCardModifier` and `ProminentButtonModifier` are untouched and still used by the item preview and detail screens. One copy change: the override warning no longer opens with a "⚠️ Warning!" prefix, since the red row and the footer already carry that weight. The text still states that on-device data will be lost if it is not in the backup. Also fixes a pre-existing defect in BackupKeyChangeViewSnapshotTests. The scenario loop shared a single view across all six colour-scheme and type-size combinations, so the first snapshot's `onDisappear` reset `permissionState` to `.undetermined` and the remaining five silently captured the locked screen instead of the authenticated one. `layoutAuthenticated` was byte-identical to `layout`. Each scenario now builds its own view, so the authenticated layout is actually covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rendered as a list row with a leading icon, the caution read as a tappable control rather than a note. It is explanatory copy, so it belongs in a footer. The section now carries its header and footer with no rows, which renders as a plain paragraph under the heading — the standard treatment for this kind of note. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Finishes the design pass. The entry screens were converted already, but the flows they open were still on the old design, so starting a backup or an import dropped the user back into bordered cards and full-width filled buttons. `BackupImportFlowView` was the last screen built entirely from cards. Its root, its ready-to-import step, and its error and success states are now `Form` sections. The two import sources each get their own section with a header and a footer, matching how the entry screens present their options. The remaining flow screens were already `Form`-based but placed their primary action inside a section footer as a padded, centred `ProminentButtonModifier` pill. Those become ordinary button rows: - `BackupCreatePDFView` — "Make PDF" moves to its own section, and the error text moves to that section's footer - `BackupKeyDecryptorView` — "Decrypt" moves to its own section - `BackupGeneratedPDFView` — "Export & Save" moves to its own section, and the red export reminder becomes that section's footer rather than a bordered card - `DeviceTransferExportView` — "Try Again" becomes a plain row Status and terminal states use `PlaceholderView` inside a section, which is what the other flow screens already did for their generating, error and completed states. `BackupImportCodeScannerView` already matched the house style and is unchanged. No `VaultCardModifier` or `ProminentButtonModifier` usage remains anywhere under Views/Backup; both modifiers are untouched and still used by the preview and detail screens. Also moves the historical-backup caution into the Details disclosure group on the backup password screen, alongside About and Keygen Information, rather than occupying a section of its own. Behaviour is unchanged throughout: every action, sheet, navigation path, file importer, publisher and toolbar item is preserved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Importing an encrypted document used to add a "Decryption Password Needed" section to the top of the Import screen, which the user had to tap to reach the password field. There is nothing to decide at that point — the document is encrypted and the only way forward is the password — so the section was pure friction. Entering `needsPasswordEntry` now presents the password sheet directly and the section is gone. This needs a cancel path to be safe. `PayloadState` is `Equatable`, so leaving the state at `.needsPasswordEntry` after a dismissal would make a second import of the same document compare equal to the first, producing no change for `onChange` to react to — the sheet could never be re-presented and the user would be stuck on a screen with no way back to password entry. The same equality trap is why `.ready` already carries a UUID. `cancelPasswordEntry()` resets the state so that transition stays observable. It is guarded on `.needsPasswordEntry`, so a dismissal that follows a successful decode cannot clobber the `.ready` payload — the sheet's `onDismiss` fires for both outcomes. Covers both paths with tests: that cancelling clears the prompt and allows the same document to prompt again, and that dismissing after a successful decode leaves the ready payload intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bradleymackey
added a commit
that referenced
this pull request
Sep 14, 2026
Applies the button convention from #551 to the screens outside `Views/Backup`, and deletes the modifier that made the old treatment possible. ## The problem `ProminentButtonModifier` reimplemented `.borderedProminent` by hand: `.font(.headline)` + a forced `.foregroundStyle(.white)` + `padding(16/12)` + `.background(color)` + `RoundedRectangle(cornerRadius: 12)`. It applied `.buttonStyle(.borderless)` *inside* its own body, so the wrapped button never received a pressed state, and the hardcoded white label had no contrast guarantee against the tint it was placed on. Six of its nine call sites also placed the button inside a `Section` footer as a padded, centred pill — the exact pattern #551 called out and removed from the backup screens. ## What changed | Screen | Change | | --- | --- | | `SettingsDangerView` | "Delete All Data" becomes a red row in its own section; the error message becomes that section's footer | | `VaultTagDetailView` | "Delete Tag" becomes a red row | | `VaultItemDetailView` | "Unlock" and "Dismiss" move out of section footers into their own rows | | `EncryptedItemDetailView` | "Decrypt" moves out of the footer into its own section | | `VaultDetailEncryptionEditView` | "Encrypt" and "Remove Encryption" become plain rows | | `OTPCodeDetailView` | "Delete" moves out of the editing-actions footer into its own red section | | `SecureNoteDetailView` | "Delete" likewise | | `VaultAutofillConfigurationView` | "Continue" is a genuine standalone CTA outside any `Form`, so it becomes `.buttonStyle(.borderedProminent)` + `.controlSize(.large)` | Destructive emphasis is preserved the way #551 preserved it: a red `FormRow` glyph tile plus red row text, matching `BackupRestoreView`'s Import & Override. Primary actions use the accent colour the same way `BackupKeyDecryptorView` does. The `ProgressView` in each `loading:` branch loses its `.tint(.white)`, which only existed because the spinner sat on a filled pill. Toolbar *Cancel* buttons keep their explicit `.foregroundStyle(.red)`. `Button(role: .cancel)` does not render red in a toolbar, so dropping the tint would have been a visual regression rather than a standardization. ## Liquid Glass does not survive snapshot rendering "Continue" was first written as `.buttonStyle(.glassProminent)`. That rendered `VaultAutofillConfigurationView` as a **completely blank image** — not just the button, the whole hierarchy — dropping the reference from 151KB to 62KB. Liquid Glass samples a backdrop through the render server, and the `.image` snapshot strategy rasterises off-screen, so the effect resolves to nothing and takes the rest of the frame with it. `Snapshotting.image(drawHierarchyInKeyWindow:)` exists as a possible escape hatch but changes the rendering path for every test, so it is not something to adopt as a side effect of this PR. `.borderedProminent` is used instead. On iOS 26 the system already draws it as a capsule, which is the shape the hand-rolled 12pt rounded rectangle was imitating. ## Verification Local, iPhone 18 Pro Max / iOS 27.0: - `xcodebuild build-for-testing` — `TEST BUILD SUCCEEDED` - Affected suites pass: `OTPCodeDetailViewSnapshotTests`, `SecureNoteDetailViewSnapshotTests`, `VaultAutofillConfigurationViewSnapshotTests` - 43 snapshots re-recorded (18 OTP detail, 24 secure note, 1 autofill) and visually reviewed - `make format` + `make lint` — clean `SettingsDangerView`, `VaultTagDetailView`, `EncryptedItemDetailView` and `VaultDetailEncryptionEditView` have view-model tests but no snapshot coverage, so there was nothing to re-record for them.⚠️ Automatic CI is still disabled (#548), so this is local verification only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bradleymackey
added a commit
that referenced
this pull request
Sep 14, 2026
…560) Applies the button convention from #551 to the screens outside `Views/Backup`, and deletes the modifier that made the old treatment possible. ## The problem `ProminentButtonModifier` reimplemented `.borderedProminent` by hand: `.font(.headline)` + a forced `.foregroundStyle(.white)` + `padding(16/12)` + `.background(color)` + `RoundedRectangle(cornerRadius: 12)`. It applied `.buttonStyle(.borderless)` *inside* its own body, so the wrapped button never received a pressed state, and the hardcoded white label had no contrast guarantee against the tint it was placed on. Six of its nine call sites also placed the button inside a `Section` footer as a padded, centred pill — the exact pattern #551 called out and removed from the backup screens. ## What changed | Screen | Change | | --- | --- | | `SettingsDangerView` | "Delete All Data" becomes a red row in its own section; the error message becomes that section's footer | | `VaultTagDetailView` | "Delete Tag" becomes a red row | | `VaultItemDetailView` | "Unlock" and "Dismiss" move out of section footers into their own rows | | `EncryptedItemDetailView` | "Decrypt" moves out of the footer into its own section | | `VaultDetailEncryptionEditView` | "Encrypt" and "Remove Encryption" become plain rows | | `OTPCodeDetailView` | "Delete" moves out of the editing-actions footer into its own red section | | `SecureNoteDetailView` | "Delete" likewise | | `VaultAutofillConfigurationView` | "Continue" is a genuine standalone CTA outside any `Form`, so it becomes `.buttonStyle(.borderedProminent)` + `.controlSize(.large)` | Destructive emphasis is preserved the way #551 preserved it: a red `FormRow` glyph tile plus red row text, matching `BackupRestoreView`'s Import & Override. Primary actions use the accent colour the same way `BackupKeyDecryptorView` does. The `ProgressView` in each `loading:` branch loses its `.tint(.white)`, which only existed because the spinner sat on a filled pill. Toolbar *Cancel* buttons keep their explicit `.foregroundStyle(.red)`. `Button(role: .cancel)` does not render red in a toolbar, so dropping the tint would have been a visual regression rather than a standardization. ## Liquid Glass does not survive snapshot rendering "Continue" was first written as `.buttonStyle(.glassProminent)`. That rendered `VaultAutofillConfigurationView` as a **completely blank image** — not just the button, the whole hierarchy — dropping the reference from 151KB to 62KB. Liquid Glass samples a backdrop through the render server, and the `.image` snapshot strategy rasterises off-screen, so the effect resolves to nothing and takes the rest of the frame with it. `Snapshotting.image(drawHierarchyInKeyWindow:)` exists as a possible escape hatch but changes the rendering path for every test, so it is not something to adopt as a side effect of this PR. `.borderedProminent` is used instead. On iOS 26 the system already draws it as a capsule, which is the shape the hand-rolled 12pt rounded rectangle was imitating. ## Verification Local, iPhone 18 Pro Max / iOS 27.0: - `xcodebuild build-for-testing` — `TEST BUILD SUCCEEDED` - Affected suites pass: `OTPCodeDetailViewSnapshotTests`, `SecureNoteDetailViewSnapshotTests`, `VaultAutofillConfigurationViewSnapshotTests` - 43 snapshots re-recorded (18 OTP detail, 24 secure note, 1 autofill) and visually reviewed - `make format` + `make lint` — clean `SettingsDangerView`, `VaultTagDetailView`, `EncryptedItemDetailView` and `VaultDetailEncryptionEditView` have view-model tests but no snapshot coverage, so there was nothing to re-record for them.⚠️ Automatic CI is still disabled (#548), so this is local verification only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
bradleymackey
added a commit
that referenced
this pull request
Sep 15, 2026
## Changes (test-only) — final PR of the pre-release audit series (#561–#572) Snapshot coverage for security-relevant screens rebuilt in #551–#560 that had no view tests: - **`BackupKeyDecryptorViewSnapshotTests`** — initial state (light/dark × 3 type sizes, in a NavigationStack so the Cancel toolbar renders) plus a deterministic decrypt-failure state: the failure is produced by actually running `attemptDecryption()` with the fast testing deriver and an erroring decoder mock, not by faking view state. - **`BackupImportFlowViewSnapshotTests`** — all three `BackupImportContext` variants (empty vault / merge / override), light/dark. - **`VaultDetailEncryptionEditViewSnapshotTests`** — encryption-disabled (full grid) and encryption-enabled variants. - `SettingsDangerView` was covered in #565; `AutoBackupSettingsView` is deliberately not given a standalone suite — it is already snapshotted transitively through `BackupCreateView` (`BackupViewSnapshotTests`), and its enabled/error states are only reachable through an async `.task` handoff that would flake under synchronous snapshot rendering. Driving those states needs a small initial-state injection refactor — left as follow-up. Release gate: full `CI_iOS` scheme (all 13 test targets, `iOSAllTests` plan including the TSAN configuration) run locally on iPhone 18 Pro Max / iOS 27.0.⚠️ Automatic CI is still disabled (#548), so this is local verification only. --- ## Release findings — report-only (no code in this series) The pre-release audit surfaced the following items that need **design decisions**, not patches. Recorded here so they are not lost: **MANIFESTO C7 gaps (protective defaults):** - Clipboard paste TTL defaults to never-expire (`PasteTTL.default = nil`) — copied OTPs/passwords sit on the pasteboard indefinitely unless the user opts in to a TTL. - No screenshot / app-switcher privacy protection anywhere (no `privacySensitive()`, no capture detection, no cover view). - Danger Zone full wipe has no confirmation dialog — one tap + biometric. **Design-level:** - Backups export killphrase/search-passphrase salts+digests; anyone holding the backup password can enumerate which items are duress-protected (C5 tension). - The killphrase/search-passphrase HMAC keys are device-local and not exported, so a restore onto a new device silently disarms every killphrase and permanently hides `.onlyPassphrase` items (rows exist, digests unverifiable). - No app-level lock / auto-lock; background purge clears only the backup password from memory. - Killphrase-triggered auto-backup + widget reload is an out-of-band success signal for a hidden item's deletion (C2 tension). - `payloadHash` and `lastBackupHash` live in plaintext UserDefaults — mutation-time evidence (C6 tension). - `deleteVault()` does not refresh the auto-backup hash, so the newest auto-backup still describes the wiped vault (recovery safety net vs C6 — decide). - `DerivedEncryptionKey.debugDescription` prints raw key material as hex; keychain replace (remove→store) is non-atomic; killphrase/passphrase edit fields are plain `TextField` not `SecureField`; the `vault://` HOTP-increment deep link is unauthenticated; `Data.random` relies on `SystemRandomNumberGenerator` (CSPRNG on Apple platforms, but unannotated as the app's sole randomness source); no `protectedDataWillBecomeUnavailable` handling. **Hygiene (non-blocking):** CI triggers commented out; CHANGELOG ~9 versions stale vs MARKETING_VERSION 2.0; hardcoded strings in rebuilt screens bypass the string catalogs (app is currently English-only, so cosmetic); stale scheme/test-plan references (`CI_iOS` scheme, orphan `VaultUITests` scheme) and a stale snapshot directory; `VaultBackup.xcstrings` not declared as a target resource; the keygen speedtest CLI prints a derived key in hex; feed search reload has no debounce/cancellation; `ForEach` identity built from `Hasher().finalize()`; reorder persist failures are swallowed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
bradleymackey
added a commit
that referenced
this pull request
Sep 18, 2026
## Summary The feed's bottom bar (tag pills + item count + Clear/Edit) lost its container and its descriptive filter text. These were **two separate changes**, months apart — and neither was #551, which only touched the backup screens: - **The container** — `510bfeaa` (#555) deleted `.background(Color.primary.opacity(0.05))` + `.clipShape(RoundedRectangle(cornerRadius: 12))` from the status row, on the stated grounds that "the controls sit on the content the way a system bottom bar does". The controls were left with no surface at all, so the `LazyVGrid` scrolled directly under the status label. - **The detail** — `315486f5` (#499, January) replaced the localized prose *"2 tag filters"* with a bare `• 🏷 2` badge. For the record: there is **no sort control in the app and never has been** (`git log --all -i -S "sort" -- Sources/VaultiOS` is empty). Order is fixed to `.relativeOrder` at the store layer and rearranged by drag in edit mode. ## What changed - **The container is back exactly where it was**: `Color.primary.opacity(0.05)` + `RoundedRectangle(cornerRadius: 12)` around the **status row only**, inset by 12pt. The tag pills stay **above** it, sitting directly on the content — that is the original arrangement and it is the one restored here. - **A single active filter is named** (`• 🏷 work`). The pill row scrolls horizontally, so an active tag can sit off-screen; the name is information the bare count never carried. With more than one filter there is no room for names inside the container, so it shows the bare count exactly as before (`• 🏷 2`). - **Item count is localized**: `statusLabel` hardcoded English `count == 1 ? "item" : "items"`. It now uses the existing `feedViewModel.searching.title.%lld` plural via a new `VaultDataModel.itemsCountDescription`, mirroring `filteringByTagsDescription`. (That plural lives in VaultFeed's bundle, so it cannot be read from VaultiOS's `localized()`, which resolves against the "Feed" table.) The button styles introduced by #555 (`.bordered` / `.borderedProminent` capsules, `Toggle`-based tag pills) are left alone — those were an accessibility fix, not part of the regression. ### A material was tried and rejected The first attempt replaced the container with a full-width `.bar` material across the whole `safeAreaInset`. Two problems, both visible in the history of this branch: 1. It pulled the tag pills **inside** the band, which is not how the bar is meant to read. 2. `.background(.bar)` applied directly puts the label in a **vibrancy context**. That is not cosmetic — it rendered the `key.horizontal` and `tag.fill` icons completely invisible and forced the secondary text to black. (`.background { Rectangle().fill(.bar) }` avoids the vibrancy, if a material is ever wanted here.) ## Manifesto Reviewed against `MANIFESTO.md`. **C5 / C2** are the live ones and drove an explicit **non-goal**: no "matched of total" count. The feed query filters on `visibility == always` and unions search-passphrase matches only when the query matches (`PersistedLocalVaultStore.swift:42-49, 102-106`), so `items.count` deliberately excludes passphrase-hidden items. Surfacing an unfiltered total would disclose that hidden items exist and let a coercer diff the two numbers. The status label still counts only what the feed already displays, and `itemsCountDescription` carries a doc comment saying so. Nothing else is touched: no bulk operation (C1), no telemetry (C3), no auth change (C4), no undo or audit surface (C6), no default flipped (C7), no step removed from a sensitive flow (C8), no duress feature advertised (C9), no payload change (C10). ## Testing - Full `iOSAllTests` plan passes locally on iPhone 18 Pro Max / iOS 27.0. - Feed snapshots re-recorded and visually compared against `git show 510bfea^:…/unifiedBar_multipleTagsFiltered.1.png` to confirm the layout matches the original. Two added — `unifiedBar_singleFilterIsNamed` and `unifiedBar_multipleFiltersFallBackToCount` — pinning both sides of the naming threshold. - The navigation and autofill suites that embed the feed were re-recorded too.⚠️ Automatic CI is still disabled (#548), so this is local verification only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilds the backup, restore and backup-password screens on standard SwiftUI form controls.
The problem
These screens had drifted away from the rest of the app. Each was a
ScrollViewof hand-rolled cards:VaultCardModifierwith a coloured border, a tinted icon tile, and a full-width filled button inside the card. The border colour also carried state (green for active, red for error, orange for warning), which is not something iOS does.Nothing else in the app is built that way. Every other screen — settings, tags, item detail, and the note editing flow — uses
Form+Section+FormRow, with explanatory copy in section footers.What changed
BackupCreateViewSectionper capability; descriptions moved to section footersBackupRestoreViewAutoBackupSettingsViewSectioninto its parent form instead of rendering its own cardBackupKeyChangeViewSecureFieldrows; keygen status moves to a footer; the historical-backup caution moves into the Details disclosure groupBackupImportFlowViewBackupCreatePDFViewBackupKeyDecryptorViewBackupGeneratedPDFViewDeviceTransferExportViewDetails worth calling out:
.pickerStyle(.segmented)to the standard form picker, matchingVaultSettingsView.Form-based but placed their primary action inside a section footer as a padded, centredProminentButtonModifierpill. Those are now ordinary button rows in their own section.PlaceholderViewin a section, which is whatDeviceTransferExportViewandBackupKeyDecryptorViewalready did for their generating, error and completed states.BackupImportCodeScannerViewalready matched the house style and is unchanged. NoVaultCardModifierorProminentButtonModifierusage remains anywhere underViews/Backup.Both modifiers themselves are untouched — they are still used by the item preview and detail screens, so only the backup screens stop using them.
One flow change: password entry is presented immediately
Importing an encrypted document used to add a "Decryption Password Needed" section to the top of the Import screen, which the user had to tap to reach the password field. There is nothing to decide at that point — the document is encrypted and the only way forward is the password — so entering
needsPasswordEntrynow presents the sheet directly and the section is gone.This needed a cancel path to be safe.
PayloadStateisEquatable, so leaving the state at.needsPasswordEntryafter a dismissal would make a second import of the same document compare equal to the first, producing no change foronChangeto react to — the sheet could never be re-presented and the user would be stuck on a screen with no route back to password entry. The same equality trap is why.readyalready carries a UUID.cancelPasswordEntry()resets the state so that transition stays observable. It is guarded on.needsPasswordEntry, so a dismissal that follows a successful decode cannot clobber the.readypayload — the sheet'sonDismissfires for both outcomes. Both paths are covered by new tests.Behaviour is otherwise unchanged
Every action, sheet,
task, navigation path, file importer, publisher subscription, toolbar item and ordering is preserved. In particularloadBackupPassword()still runs before any import sheet is presented — that is now done once in a shared row builder rather than repeated at each call site.Reviewed against
MANIFESTO.md, since backups are explicitly governed by it. Aside from the password-entry step above, which removes a tap without removing a decision, this is presentation-only: no bulk operation was added (C1), no enumeration UI (C5), no default weakened (C7), and nothing changed about what a backup payload contains or what a recipient can preview without decrypting (C10).One copy change: the override warning no longer opens with a "⚠️ Warning!" prefix, since the red row and footer already carry that weight. The text still states that on-device data will be lost if it is not in the backup, and the action still routes through the full import flow rather than destroying anything immediately. Flagging it as the only non-visual change in the diff.
Snapshot test fix
BackupKeyChangeViewSnapshotTestshad a pre-existing defect, found while re-recording.The scenario loop built the view once and reused it across all six colour-scheme × type-size combinations. The view resets
permissionStateto.undeterminedinonDisappear, so the first snapshot tore down the state the remaining five depended on — they silently captured the locked screen instead of the authenticated one.layoutAuthenticated.*was byte-identical tolayout.*for five of six scenarios. Verified againstHEADbefore the change, so this predates this PR.Each scenario now builds its own view. All six authenticated snapshots now differ from their locked counterparts, so the authenticated layout — the part this PR rewrites most heavily — is actually covered.
Verification
Local, Xcode 27.0 RC1 (
27A266a), iPhone 18 Pro Max / iOS 27.0:xcodebuild build-for-testing—TEST BUILD SUCCEEDED, no warnings (-warnings-as-errorsis on package-wide)-parallel-testing-enabled NO—TEST EXECUTE SUCCEEDED, 2852 passed, 0 failures, across both configurations (Default and TSAN). Up 4 from the pre-change 2848 — the two new cancel-path tests, counted once per configuration.make format+make lint— clean🤖 Generated with Claude Code