Skip to content

Add the Devices section to Mac Settings - #298

Open
sudoHG wants to merge 20 commits into
mainfrom
sudoHG/t6-mac-devices-settings-854916
Open

sudoHG wants to merge 20 commits into
mainfrom
sudoHG/t6-mac-devices-settings-854916

Conversation

@sudoHG

@sudoHG sudoHG commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Closes #288

Summary

  • Settings gains a Devices section between Backup and Data, shown only when the device coordinator is configured (CloudKit builds). Release builds and every E2E scenario except screenshots-devices show nothing.
  • Pair a Device… opens a sheet: QR code (Core Image) with countdown and the "scan only from this window" warning → the device that scanned it, platform shown as "(not verified)", with the 10-second hint while the Mac waits for the reveal → a six-digit field (no one-time-code content type, no truncation) → Confirm checks the code; only an accepted code asks for system authentication, a cancelled/failed prompt keeps the accepted entry and Confirm retries without a new entry. The Mac's code is never shown: the model and presentation never receive it, and the field is cleared once the code is accepted.
  • List rows show name, platform, paired date, last reached and the delivery check state, with Rename, Test Delivery and Remove (inline confirmation). Each unavailable state has its own line; an unusable identity or refused list offers Reset Device Identity… with a confirmation.
  • Step 7: pairing confirmation now uses its own authentication reason, Pair a device with this Mac / 确认将设备与这台 Mac 配对, accepted by the authentication subprocess; the four device keychain conflict messages are translated. Step 8 terms are in docs/glossary.md and checked by a test.

Diff stat

git diff --stat origin/main: 21 files changed, 2376 insertions(+), 6 deletions(-). New: Views/Settings/DevicesSettingsSection.swift, DevicePairingSheet.swift, DevicesSettingsModel.swift, DevicesPresentation.swift, PairingSheetPresentation.swift, VaultViewModel+Devices.swift, Tests/AskKeyTestSupport/E2EDeviceScenario.swift, two App test files. Localizable.xcstrings +672 lines (84 new entries with en, zh-Hans and qps-ploc).

Tests

Local, head 22120f6:

  • swift build: complete.
  • swift test --filter "DevicesSettingsModelTests|DevicesSettingsRenderTests|DeviceCoordinatorPairingTests|DeviceCoordinatorDevicesTests|CloudKitGateTests|AppLanguageCatalogTests|ManagementAuthenticationProcessTests|LocalizationUnificationTests|LocalizationRemediationTests|ReviewLocalizationTests|AgentAccessPolicyMessageTests|LanguageSwitchPickerTitleTests|OnboardingFlowTests|BackupSheetRenderTests|Batch4SettingsLanguageTests|AppLanguageExperienceTests|WorkspaceVisualContractTests": 115 tests, 0 failed, 0 skipped.
Suite Tests Note
DevicesSettingsModelTests 12 new
DevicesSettingsRenderTests 1 new: 9 sheet/section states x 2 languages offscreen; asserts the code field's contentType is nil
DeviceCoordinatorPairingTests 17 reason assertion updated
DeviceCoordinatorDevicesTests 14 unchanged
CloudKitGateTests 7 unchanged
AppLanguageCatalogTests 9 +2 (glossary terms, device keychain conflicts)
ManagementAuthenticationProcessTests 13 +1 (packaged subprocess accepts and describes the pairing reason)
LocalizationUnificationTests 8
LocalizationRemediationTests 11
ReviewLocalizationTests 2
AgentAccessPolicyMessageTests 2
LanguageSwitchPickerTitleTests 1
OnboardingFlowTests 7
BackupSheetRenderTests 1
Batch4SettingsLanguageTests 6
AppLanguageExperienceTests 2
WorkspaceVisualContractTests 2

The Acceptance view-model checks are covered in DevicesSettingsModelTests: section absent when not configured; pairing needs the management session; a wrong code asks for no authentication and counts one attempt; a matching code needs authentication with the pairing reason, a cancel stores nothing, a retry needs no new entry; three wrong codes, expiry and a second responder store nothing; the field appears only after the reveal; no sheet text in any state contains the code; Remove and Reset confirm first; each unavailable state shows its message.

Full swift test, basic-ui-flows and the screenshot suite run in CI (not run locally, per AGENTS.md).

Checks

  • python3 scripts/check_hygiene.py: passed (size=0, test-support=0, debug=0, local-path=0, non-ascii-name=0, multica=0, cjk=0).
  • python3 scripts/check_module_deps.py: passed.
  • CI run and screenshot artifact: added below once CI finishes.

Deviations and questions

  • Vault.devicePairingReason in Sources/AskKeyVault/Vault+Devices.swift still equals the management reason and its comment still says Add the Devices section to Mac Settings #288 will add a dedicated one. The App now passes ManagementAuthenticationAction.pairDevice.reasonKey to the approval signature, so the prompt shows the pairing reason; the Vault only checks the authenticator against its constant, and the App's authenticator accepts any reason. Updating the constant and its comment is outside this issue's Scope (Sources/AskKeyVault/), left for a follow-up.
  • WorkspaceVisualContract.swift has no Settings section and was not changed; the Devices views reuse Theme, GroupedList, the Settings row layout and the backup sheet frame.
  • In the screenshot scenario, the E2E fixture plays the person typing the iPhone's code (through the coordinator, inside the App process) so that the code is never written to a file the runner could read; captures 31–33 come from the real controls.

Settings gains a Devices section between Backup and Data, shown only when
the device coordinator is configured (a build with CloudKit). Pair a
Device… opens a sheet that shows the QR code drawn with Core Image and a
countdown, then the device that scanned it (its platform marked as not
verified) while the Mac waits for the iPhone's reveal, then a six-digit
field for the code the iPhone shows. Confirm checks the entry; a wrong or
malformed entry asks for nothing, and an accepted code asks for system
authentication at once. A cancelled or failed prompt keeps the accepted
entry, and Confirm asks again without a new entry. The field is cleared
once the code is accepted, is not a one-time-code field and does not cut
an entry short. The sheet never shows the Mac's code: no state it reads
carries one. Expiry, three wrong codes, a second responder, cancellation
and start failures each have their own message, with Pair Again where it
helps.

The list shows each device's name, platform, paired date, last reached
and delivery check state, with Rename, Test Delivery and Remove; Remove
and Reset Device Identity… confirm inline first. The section explains
each unavailable state: no Secure Enclave, iCloud signed out, restricted
or unavailable, an account change, an unusable identity or refused list
(with the reset), and the limit of 8.

Pairing confirmation now asks for system authentication with its own
reason, "Pair a device with this Mac", which the authentication
subprocess accepts. Every new string is in the catalog in English and
Simplified Chinese, with the four device keychain conflict messages and
neutral text for push.message.title and push.message.body. The glossary
adds Device, Pair, Pairing code and Delivery check.

The E2E App gets fake device services and a simulated iPhone only in the
screenshots-devices scenario, and the screenshot suite captures the list,
the QR code, waiting, code entry and iCloud signed out in English (30-34)
and Simplified Chinese (35-39).

Closes #288
- The accepted-code step has its own title and message, Confirm Pairing,
  instead of repeating the entry instruction without a field.
- Unavailable Devices actions are dimmed as well as disabled, because the
  shared secondary button styles draw the same when disabled.
- A Mac without a Secure Enclave says so before the first attempt.
- Removing the last device after an account change checks the account
  again, so pairing in the new account can start.
- The device row shows the delivery check on its own line, and none while
  the device can't be reached.
- Each Devices subview subscribes to the language itself, so switching
  language updates the section and the sheet.
- Copy: the signed-out line no longer says pairing can't start while Pair
  a Device stays available; the removal confirmation states the local
  guarantee and asks to remove this Mac on the device if it still shows
  it; Chinese wording fixes for the code warning, the restricted and
  undone messages and the removed-device issue.
- The test-only shownText helper moves to the test target.
The first CI screenshot run clicked Pair a Device… and no sheet appeared;
afterwards the button was enabled again, so the session had been
cancelled. The only path that does that without a click is the sheet
binding's setter. It no longer cancels: every way out of the sheet goes
through its buttons (Esc is Cancel) or the end of the management session.

The QR code is drawn with Core Image's software renderer, which needs no
GPU. The E2E device fixture records each pairing state the sheet reads,
by case name only, in device-transitions.json, so a failed capture shows
where the session went.
- The sheet binding clears a session that has finished when SwiftUI
  reports the sheet dismissed; a live session still ends only through the
  sheet's buttons or the end of the management session.
- On the QR step, Cancel and Pair Again share one row, so Cancel lines up
  with the other steps' buttons.
- The removal confirmation says Ask Key tries to tell the device, which
  then removes this Mac when it next connects.
- E2E: the second CI run again opened no sheet after the click, and no
  pairing state changed. The fixture now records the coordinator's state
  at launch and after every change the section reads; the test checks
  for the sheet first, clicks again or starts pairing through a fixture
  command when the click opened none, takes the captures, and fails at
  the end if the click was not enough.
The third CI run settled where the captures fail: two synthesized clicks
on Pair a Device… started no pairing, while the fixture calling the
coordinator's startPairing() opened the sheet at once, so presentation
works and the button's action does not run under the CI click. The
section now follows the Backup section's layout, whose buttons the same
run clicks successfully: the sheet hangs off the pairing row instead of
the whole group, the button calls a method, and the section refreshes on
appear instead of through a task.

After the pairing, the runner's accessibility queries timed out after
four 600-point scrolls. The test now scrolls only until the device row
is hittable, and attaches the fixture's state record as soon as the
sheet shows and after the delivery check, so it survives a later
timeout.
Diagnostics only, in Tests: before each click the fixture records what
Pair a Device… checks (Secure Enclave, pairing, availability, account,
identity, devices) and the runner records the button's enabled, hittable
and frame values. A click that opens no sheet is followed by a click
with the button scrolled higher, then the fixture start; the test fails
at the end unless the first click opened the sheet. After the pairing
the runner waits eight seconds so the state record shows what changes
once the sheet has closed, and a teardown block terminates the App so
a hung accessibility query cannot also hang the teardown.
…sect the Devices row

Identify the Devices status lines and the Settings page for the E2E, which
now finds them by identifier.
A sheet bound to a Binding(get:set:) over the model left Pair a Device...
deaf to clicks on macOS 26 until the section redrew (#298).

TEMP #298 diagnostics, revert before merge: the English E2E runs without the
sheet; view changes are still printed.
The Devices section kept an optional observable model in @State, created
in its initializer. On macOS 26, Pair a Device... then ignored clicks until
the section redrew (#298). The model now lives as long as the App's device
coordinator and is passed in, as the Backup section receives the vault; a
section that appears again closes a rename or confirmation left open.

The Devices E2E clicks Pair a Device... itself and has no fixture path that
starts a session, so the macOS 26 runner catches the regression. It finds
the Devices texts by identifier: after the code field had focus, whole-window
snapshots hung on the text cursor's remote element on the runner.

This removes the temporary #298 diagnostics from 0f547e7, f9fee03 and
b81ac28 and the diagnostic fixture commands.
ADR 0013 lets the unpair notice be lost or expire, so the removal
confirmation now says only that Ask Key tries to tell the device.
On macOS 26, Pair a Device... ignored clicks until the Devices section
redrew; passing the model in from a stable holder did not change that. The
cause is unknown (#298). Clicks worked in every run after a redraw, so the
section rebuilds its controls once, after it has loaded. Nothing redraws
repeatedly, and the controls look the same before and after.
….. does on a click

The button action, the model's canPair and its inputs, and each return of
the coordinator's startPairing are recorded in the E2E App; the screenshot
test prints the trace after its click.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add the Devices section to Mac Settings

1 participant