Skip to content

feat(apple): redesign pairing onboarding - #1

Merged
QuintinShaw merged 11 commits into
mainfrom
feat/pairing-onboarding
Jul 19, 2026
Merged

feat(apple): redesign pairing onboarding#1
QuintinShaw merged 11 commits into
mainfrom
feat/pairing-onboarding

Conversation

@QuintinShaw

Copy link
Copy Markdown
Owner

Summary

  • Redesign the iOS pairing onboarding flow in JoinView: full-screen scan experience, explicit camera activation instead of auto-start, simplified pairing choices, and reduced scan latency.

Validation

  • Branch reviewed by an independent adversarial review pass before merge (in progress).
  • SwiftUI change only; server and daemon untouched.

Risk

  • UI-only change to the pairing entry flow; no protocol or storage changes. Rollback is a revert of four commits.

Deep-link and cold-start joins run with destination == nil, so the
existing error/joining state only rendered inside the scanner/manual
sheets and was invisible for links opened via onOpenURL or
pendingJoinPayload. Add an inline progress/error status to the base
JoinView shown whenever no sheet is on screen, disable the action
buttons while a join is in flight, and stop present() from resetting
error state while a join is still running.
The onCode closure's `guard !joining` could never block a second
callback: UIViewControllerRepresentable.makeCoordinator runs once, so
Coordinator captured this closure (and the joining value baked into
it) only on the sheet's first render. Reentrancy is actually enforced
synchronously by Coordinator's `fired` flag plus beginJoin's own
`guard !joining`, which runs in the same actor turn as this callback.
Drop the dead guard and document why it's safe.
firstMatch(of:) only ever looked at one shape match per transcript
candidate, so a rejected false-positive on a candidate permanently
hid any real code recognized later in that same candidate. Switch to
matches(of:) and walk all matches, skipping ones already rejected.
.textInputAutocapitalization(.characters) on the manual code field can
uppercase a fully-typed sitrep://join link (e.g. SITREP://JOIN?...),
but canSubmit and resolveJoin compared scheme/host case-sensitively via
URLComponents, so such input could never validate or resolve. Lowercase
only the scheme/host comparison; leave the rest of the string, and the
extracted query values, in their original case.
ScannerJoinView and ManualJoinView already set
interactiveDismissDisabled(joining) to block swipe-to-dismiss during a
join, but their toolbar close buttons stayed tappable, letting the
user dismiss the sheet mid-join through the button instead.
performJoin's blanket catch treated every failure — timeouts, offline,
5xx — the same as a server-rejected code: it showed "code invalid or
expired" and permanently blacklisted the code via rejectAndResume,
deadlocking a perfectly valid code against a flaky connection. Only an
explicit APIError.badStatus in the 4xx range now blacklists the code;
everything else (transport errors, timeouts, 5xx) shows a
network-check message and resumes scanning via the new
ScannerControl.resume()/Coordinator.resume(), which re-arms the
scanner without adding the code to `rejected`.
ScannerJoinView only checked DataScannerViewController.isSupported, so
a denied/restricted camera permission fell through to a black camera
layer with no explanation or way out, and startScanning() failures
were silently swallowed with try?.

Check AVCaptureDevice.authorizationStatus(for: .video) before showing
the scanner: request access when .notDetermined, and show an explicit
"open Settings" screen instead of the camera view when denied or
restricted (refreshed on scenePhase becoming active, so returning from
Settings revives the flow without reopening the sheet). Route
startScanning() through a Coordinator.start() that reports failures
via a new onScanningError callback instead of using try?, rendering a
readable error state rather than a dead camera preview.
@QuintinShaw
QuintinShaw merged commit 19aea3f into main Jul 19, 2026
@QuintinShaw
QuintinShaw deleted the feat/pairing-onboarding branch July 19, 2026 00:48
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.

1 participant