feat(ui): radical redesign — design tokens, TabView shell, actionable onboarding & Relay funnel#24
Conversation
…et parity Phase 1 of the radical redesign (Vault-OS identity on a Calm-Native foundation). Foundation only — no navigation/structure changes yet. - Theme.swift becomes the shared design-token layer (compiled into app + widget): P3 / dark-aware brand accent, a six-meaning semantic status palette, and an 8pt spacing + continuous-radius scale, replacing the previous two literals. - Add a canonical SyncStatus registry (symbol + color + localized label) keyed by genuine sync state, incl. a distinct "starting" state. Uses String(localized:) so it resolves in both the app and the widget bundle (no L10n dependency). - Fill AccentColor with the refined teal so the app-wide tint is the brand instead of the leaked system blue; also set an explicit .tint on the WindowGroup. - Widget: decode the snapshot status through SyncStatus.fromWire — unknown values now resolve to "attention", never silently to the green "all good" branch (closes the documented widget-lies bug). CTA retinted off system blue. - Add SyncStatus.fromWire contract tests (unknown -> attention regression guard). Build + full test suite green on iOS 26.5 simulator.
…tionCard, MonoField) Phase 2a — the reusable building blocks every later phase consumes, each replacing a pattern the audit found hand-rebuilt across many views: - StatusBadge / StatusRow: status as icon + text label (never color alone), driven by the SyncStatus registry. - DetailRow: the title+value row duplicated across five views. - ActionCard: icon + title + plain-language message + a real >=44pt primary action, replacing the prose "go to Settings" remediation blocks. - MonoField: copyable monospaced field (Device IDs, .stignore globs, docker run) with haptic + confirmation — the Vault OS "monospace-as-identity" signature. - vaultCard(): one card surface (continuous radius + optional status-accent bar) replacing the three divergent card looks. Additive and app-only. Build green; not yet wired into screens.
Phase 2b (partial) — replace ad-hoc .green/.orange/.blue status colors with the semantic tokens so status reads consistently and resolves correctly in dark mode: - PendingSharesView: attention/info tokens for the failure vs "Ready" states; bumped the status badge off .caption2. - SetupChecklistView: success/attention tokens for the progress tint and the per-item status color. - RelayServerSetupView: success token for the "helper running" banner; removed the dead `private let teal` the audit flagged. ContentView and OnboardingView are intentionally left for their Phase 3/4 rebuilds. Build green.
- Add SyncStatusHeader: a pinned material header (safeAreaInset) that states one
glanceable truth, driven by a new canonical `overallStatus` (SyncStatus) for
glyph + color while keeping the contextual wording in syncStatusText. Adopts
Liquid Glass automatically on iOS 26.
- Give the hub a real navigation title ("VaultSync") instead of the empty one.
- Remove the now-redundant in-list status row (folded into the header) and its
duplicate syncStatusIcon/syncStatusColor; relocate stale/background warnings.
- Retint the whole hub onto semantic status tokens (devices connected, errors,
folder errors, conflict badge, vault state, "Shared With", device connectivity),
killing the .green/.orange/.red/.blue + hand-rolled slate-opacity usage.
- Raise actionable/remediation text off .caption2 to the .footnote floor.
Build green. No navigation restructure yet (TabView is 3b).
…s (Phase 3b) - Replace the single overloaded scrolling hub with a TabView: a "Sync" tab (status header + issues + Obsidian + pending shares + vaults) and a "Devices" tab (paired peers + Add Device), giving the app the HIG-correct bottom-tab shell and thumb-zone navigation it lacked. - Keep all @State, sheets, alerts and onChange handlers in ContentView, attached at the shell level so cross-tab triggers (e.g. "Add Device" from a Sync-tab issue) still present correctly. The scenePhase-coupled Syncthing lifecycle in VaultSyncApp is untouched. - Add-Device sheet grows with Dynamic Type ([.medium, .large] instead of medium-only). The dedicated Relay tab + cross-linked funnel is Phase 4. Build + tests green.
…se 2b) Retint the remaining views so status reads consistently and resolves correctly in dark mode app-wide: - SyncActivityView: error/info tokens for event severity. - DeviceDetailView: success/inactive for connectivity. - LineDiffView: unify the diff colors onto status tokens — fixes the mixed-hue bug (Color.green background vs systemGreen foreground) the audit flagged, and gives dark-mode-correct, contrast-validated add/remove highlights. - SettingsView + RelayDiagnosticsView + CloudRelayUpsellView: success/attention/ error tokens for relay subscription, delivery, health, APNs, and error states. Only OnboardingView remains on raw colors; it is rebuilt in Phase 4. Build green.
Rebuild onboarding from prose-that-points-elsewhere into a real pager whose steps
launch the actual task — the audit's highest-leverage single change ("setup is
described but not doable" / "the checklist lies").
- TabView(.page) pager: a Welcome page (value pitch) + a live "Let's get your
vault synced" page with three actionable step cards.
- Each step runs the real task inline and self-verifies against live manager
state, turning green when its prerequisite is met:
- Connect Obsidian folder -> FolderPicker
- Add your computer/server -> Device ID form + QR scanner
- Sync your first vault -> info + auto-green when a folder arrives
- Page dots + a persistent bottom bar (Continue -> Open VaultSync); tokens/radii
throughout; Add-Device sheet grows with Dynamic Type.
- Animate the onboarding->hub swap in VaultSyncApp (was an abrupt no-transition flip).
Welcome page visually verified in the simulator; build green.
Replace the three disconnected relay altitudes (paywall / Docker wall / engineer dump) with one progressively-disclosed Relay tab, and unify the subscribe UI. - SubscribePlanPicker: the single canonical subscription picker — yearly first and highlighted as "Best value", monthly below, restore, and prominent price / term / auto-renew disclosure with Terms of Use + Privacy Policy links (App Store 3.1.2(a)). Replaces the two previously duplicated, opposite-ordered blocks. - RelayHomeView: the new Relay tab. Not subscribed -> pitch + SubscribePlanPicker. Subscribed -> a status badge + a three-step spine (Subscribe -> run the server helper -> verify delivery) that links into RelayServerSetupView and RelayDiagnosticsView, finally cross-linking the funnel that had no links. - ContentView: add the Relay tab (Sync / Devices / Relay); the dashboard relay row and the one-time aha-moment now select the tab instead of presenting a sheet. - Delete CloudRelayUpsellView (superseded by the Relay tab). - SettingsView: list yearly before monthly to match the canonical order. Build + tests green.
…tus motion (Phase 5) - ConflictDiffView: replace the tiny .caption2 bottom-bar icons (which looked like a fake tab bar) with a real bottom action bar of full-width >=44pt buttons. ALL three actions now confirm before mutating files — "Keep Both" previously mutated with no confirmation. Keep This / Keep Other are destructive-styled and confirmed; Keep Both confirms with a non-destructive message. Labels are now localized; the diff legend uses the semantic status tokens. - ContentView: the Vault Detail rescan no longer fakes completion with a fixed 2s Task.sleep — the busy state now reflects the folder's real "scanning" engine state. - SyncStatusHeader: add contentTransition(.symbolEffect(.replace)) so the status glyph morphs (e.g. syncing arrows -> checkmark) on completion; .pulse while syncing stays, both reduce-motion-safe. Build + tests green.
…l (Phase 6) - Widget: add VoiceOver support (it had zero) — decorative status glyphs are hidden and each widget family is exposed as one combined, readable element (status + last sync + files + action). - L10n.tr: add a `value: key` fallback so a missing translation renders readable English instead of the bare key, matching the widget's helper. - Add ios/scripts/design-token-lint.sh — fails if views reintroduce raw status colors (.red/.green/.orange/.blue) or hardcoded literals instead of Theme.swift tokens — and wire it as a fast CI gate before the macOS build. The guardrail immediately caught two missed .orange usages in ContentView's Obsidian section, now retinted to the attention token. Lint passes; build green. Note: the bulk de/es/zh translation of the newly added (and the pre-existing 44 unlocalized) strings is a separate translation pass — they are now all extractable keys and fall back to English meanwhile.
…tings relay removed
Addresses review feedback on the Relay tab content:
- The Relay tab now has two deliberately different shapes:
- Not subscribed: a decluttered, focused pitch (single hero + one honest
framing line) instead of the busy icon-title-paragraph + three benefit rows.
Framing repositions Relay as a tiny private wake-up on top of the already-free
P2P sync — explicitly "not cloud storage" — so it doesn't read as an Obsidian
Sync competitor.
- Subscribed: an operational control center. Activation is the real lever, so
when wake-ups aren't arriving the server-setup step is front and center;
otherwise a calm "active" status. Diagnostics, server setup, and Manage
Subscription all live here now.
- SubscribePlanPicker: Monthly is listed first (low-commitment entry, avoids
annual sticker shock); Yearly follows, framed as "Save N%" (computed from
StoreKit prices) rather than a scary annual total. Adds a no-device guard — with
no paired homeserver it guides to the Devices tab instead of letting the user
pay for nothing to wake.
- Delete the entire Cloud Relay section from Settings; the subscribe guard, manage
subscription, server setup, and diagnostics it held are absorbed by the Relay
tab. The tab refreshes relay diagnostics on appear.
Build + full test suite + design-token lint green.
The "already syncs free / not cloud storage" reassurance line is no longer always-on in the pitch — it's now a subtle "How is this private?" info button that reveals the text in a popover, decluttering the hero further. Dropped the hero's accessibility-combine so the button stays independently actionable for VoiceOver. Build + design-token lint green.
…ology - Add the 36 redesign keys that were English-only (tab labels Sync/Relay, Relay-tab pitch + subscribed states, onboarding setup steps, plan picker, conflict "keep both" copy) to all four catalogs with real de/es/zh translations. Parity restored: 583 keys each. - Fix ConflictDiffView's primary "Keep This Device's Version" button: it used a curly apostrophe so it never matched the straight-apostrophe catalog key and fell back to English in every language despite existing translations. - Unify "wake-up" terminology: de -> "Weck-Signal" (was Wecksignal/Aufweck- vorgänge/Weckrufe), es -> "señal de activación" (was señal de aviso/ activaciones). Fix es "silent push" (push silencioso), "Health Endpoint" (Endpoint de salud), and de Synchronisierung/Synchronisation drift. - zh: normalize quote marks to full-width "" (were 「」 / straight ASCII).
… kit adoption Correctness - ContentView: the dashboard "Cloud Relay active" claim was driven solely by isRelaySubscribed (a StoreKit entitlement check), so a subscribed-but-unprovisioned device showed green "active" while nothing was delivered. Gate it on relayDeliveryLikelyWorking; otherwise route the user to finish server setup. - ContentView: fold the folder-state glyph/color mapping onto the SyncStatus registry (one source of truth) — fixes the divergent error glyph (exclamationmark.circle vs the canonical xmark.octagon) and the non-semantic .vaultAccent fill; unknown states stay neutral rather than a false "attention". Dead code / duplication - Extract the byte-for-byte-duplicated Add-Device form (ContentView + Onboarding) into a shared AddDeviceSheet. - ContentView: drop the unused @Environment colorScheme; collapse the teal/vaultTeal alias onto vaultAccent; unify the empty-device strings. - SettingsView: remove the dead "Error" alert and its unused alertMessage/showAlert state (left over from the relay-section removal). - SetupChecklistView: dedup the optional-badge across both Dynamic-Type branches and remove the unreachable statusText("") branch. Design-system adoption (the kit was 0-call-site dead code) - Wire StatusRow (device rows), DetailRow (vault detail), ActionCard (pending-shares banner), MonoField (Device ID), and .vaultCard() (pending-shares + checklist surfaces) into the views that had hand-rebuilt them. - DesignSystem: MonoField "copied" state now reverts after 1.5s instead of latching forever; vaultCard's accent strip is clipped to the card shape so it follows the leading corners instead of being rounded on all four of its own. Tokens / color - RelayDiagnosticsView: the .green/.red/.blue returned from the status-color helpers evaded the design-token lint (return-position blind spot) — map them onto statusSuccess/statusError/statusInfo. - Tokenize stray radii/spacing (checklist, pending shares, relay command box); LineDiffView uses .foregroundStyle over the deprecated .foregroundColor. Accessibility - OnboardingView: the step card combined its children AND overrode the label, hiding both the description and the action button from VoiceOver — combine only the text, keep the button as its own element. - DeviceDetailView: stop overriding the Status row's label so LabeledContent keeps the "Status" field name. - SubscribePlanPicker: fold the "best value / save N%" badge into the recommended card's VoiceOver label; hide the in-progress spinner. - PendingSharesView: drop the redundant "Applying share" spinner label.
…rn blind spot
- RelayHomeView: the "How is this private?" popover showed only a truncated first
line ("Dein Vault synchronisiert bereits kostenl…"). It sized the text to a
single line; force multi-line wrapping at a fixed width with fixedSize(vertical)
so the popover grows to the full text.
- design-token-lint.sh: also flag return-position / ternary / switch-expression
status colors (return .red, cond ? .green : .red, case .x: .orange) — the blind
spot that let helper functions evade the lint. Tightening it immediately caught
two more missed colors in SyncIssuesView's severity helper, now retinted to
statusError / statusAttention.
Build + full test suite + (tightened) design-token lint green.
The relay declines triggers with HTTP 400/401/403 when a device's subscription is expired, cancelled, or not yet provisioned. doTrigger classified those as fatal, so fireTrigger exited the process — and with `restart: unless-stopped` that turned any normal subscription lapse into a crash-restart loop. Treat the relay's subscription-gating codes (400/401/402/403) as a recoverable subscriptionInactiveError: log and keep running, and re-check on a slow cadence so delivery resumes automatically once the subscription is active again, without hammering the relay. 404 stays fatal (wrong RELAY_URL); other 4xx and 5xx remain transient retries. fireTrigger now returns a triggerOutcome enum instead of (delivered, fatal) bools. Add unit and end-to-end runService tests (survival, auto-resume, fatal exit) and document the trigger error contract in docs/relay-spec.md.
…older The "Obsidian Vaults" section rendered syncthingManager.folders directly, so a whole-directory sync (one folder rooted at the Obsidian directory, labelled "Obsidian") showed a single "Obsidian" row instead of the vaults inside it. VaultManager.detectedVaults already discovered the real vaults (subfolders with .obsidian/) but was only used to pick an empty-state message. Derive the displayed list from the detected vaults mapped onto whichever Syncthing folder syncs them: a folder rooted at the Obsidian directory expands into one row per vault; per-vault folders still map 1:1. Path comparison is symlink/trailing-slash tolerant. Conflicts are attributed per vault by folder-relative path so a shared folder's conflicts no longer appear duplicated across vault rows. The detail view is vault-centric (name + subpath) with a note that sync filters and devices apply to the whole directory. Adds VaultConflictAttributionTests for the path-prefix boundary logic.
zh:
- Monthly subscription period rendered "1 月" (reads as "January").
Change month singular 月 -> 个月.
- Standardize 重扫 -> 重新扫描 (9x; Apple-standard "rescan").
- QR Code -> 二维码 (Scan button, camera-permission body, InfoPlist).
- "Present" 已提供 -> 存在 (APNs-token diagnostic; antonym of 缺失).
- "immediate" 即时 -> 目前 in "No immediate relay problems detected".
- Restore 前 ("ago") in "...more than %d %@ ago", parallel to the sibling.
- Symmetric Keep This / Keep Other (both on 版本).
- Full-width colon in "%@: %@".
de:
- Healthy/Unhealthy "Gesund/Ungesund" (medical sense) ->
"Funktionsfähig/Nicht funktionsfähig" for a service-health status
(incl. the "...is not healthy" body line).
- "No folders syncing yet" calque -> "Es werden noch keine Ordner synchronisiert".
- Unify contribution wording to "Unterstützung".
- "Last Trigger Received" -> noun-phrase label "Letzter empfangener Trigger".
es:
- "Last wake-up" -> consistent "señal de activación".
- "Best value" "La mejor opción" -> "Mejor valor".
Add missing keys Starting / Paused (SyncStatus badge) and Node modules
(Go-bridge detected-pattern label) to all four catalogs — these were
rendering the English fallback in de/es/zh. Parity restored: 587 keys each.
Catalogs (all four, parity 587 -> 549): - Drop 38 unused string keys verified to have zero Swift references (neither L10n.tr/fmt nor bare SwiftUI string literals): the removed onboarding-overview page (11 onboarding.overview.* keys + 2 onboarding.accessibility hints) and ~25 superseded Cloud Relay redesign strings (Subscribe Monthly/Yearly, Make incoming sync instant, Not Now, Finish setup, the long pitch variants, etc.). Swift (verified no callers; test-only symbols left intact): - Theme: Color.vaultViolet (never wired), VaultSpacing.xxl (unused token). - SubscriptionManager: relayProductID + availableProduct (legacy single-product aliases; UI reads monthlyProduct/yearlyProduct directly). - VaultManager: stopAccess() (+ its now-orphaned // MARK: - Lifecycle). Builds clean for iphonesimulator.
Shorten translation values for width-constrained controls so they no longer truncate/wrap on iPhone (English keys unchanged): de — tight .controlSize(.small) SyncIssuesView buttons + SyncStatus pills: - "Run Foreground Rescan": "Erneuten Scan im Vordergrund ausführen" -> "Im Vordergrund scannen" - "Add or Reconnect Device": "Gerät hinzufügen oder neu verbinden" -> "Gerät verbinden" - "Rescan Failed Vaults": "Fehlgeschlagene Vaults erneut scannen" -> "Fehler-Vaults neu scannen" - "Sync Error" -> "Sync-Fehler", "Sync Issue" -> "Sync-Problem", "Needs Attention" -> "Aktion nötig" (status pills; also widget catalog) es: - "Add or Reconnect Device" -> "Añadir o reconectar" - "Run Foreground Rescan" -> "Reescanear ahora" - "Timed out" -> "Sin respuesta" - "Reconnect Obsidian Directory" (alert action) -> "Reconectar Obsidian" zh: - "Accept First Pending Share" -> "接受待处理共享" - "Add or Reconnect Device" -> "添加或重连设备"
|
Warning Review limit reached
More reviews will be available in 5 minutes and 14 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughThis PR unifies the iOS app's design system by centralizing colors, spacing, and status semantics into a reusable token library, introduces a comprehensive component toolkit, refactors major views to use the new system, and adds subscription-inactive retry handling to the relay backend with corresponding tests. ChangesiOS Design System & Relay Subscription Redesign
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ios/VaultSync/en.lproj/Localizable.strings (1)
513-516:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReplace the stale “Settings → Cloud Relay” route text.
These strings still send users to Settings, but this redesign moved relay subscription/setup into the dedicated Relay tab and removed the old Settings entry. That leaves the activation funnel pointing at a dead path.
Suggested copy update
-"Enable Cloud Relay in Settings if you want changes pushed the moment they happen." +"Enable Cloud Relay on the Relay tab if you want changes pushed the moment they happen." -"Set up the server helper from Settings → Cloud Relay → Set Up Your Server." +"Set up the server helper from the Relay tab → Set Up Your Server." -"Optional: turn on Cloud Relay later for instant updates — you’ll find it in Settings." +"Optional: turn on Cloud Relay later for instant updates — you’ll find it on the Relay tab."Please mirror the same route fix in the localized copies too.
Also applies to: 557-557
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/VaultSync/en.lproj/Localizable.strings` around lines 513 - 516, The displayed route "Settings → Cloud Relay" is stale; update the four strings containing that phrase (the entries beginning "Enable Cloud Relay in Settings...", "Cloud Relay — finish server setup", "You’re subscribed... vaultsync-notify helper...", and "Set up the server helper from Settings → Cloud Relay → Set Up Your Server.") to point to the new Relay tab instead (e.g., change "Settings → Cloud Relay → Set Up Your Server" to "Relay → Set Up Your Server" or "Relay tab → Set Up Your Server" as appropriate), and apply the same replacement to the other occurrence noted (line 557) and all localized copies so the activation funnel points to the Relay tab rather than Settings.
🧹 Nitpick comments (2)
notify/relay.go (1)
259-267: 💤 Low valueConsider using
errors.AsinisFatalfor consistency.
isSubscriptionInactivecorrectly useserrors.As, butisFataluses a direct type assertion. While errors aren't wrapped today, this inconsistency could cause subtle bugs if error wrapping is added later. Low priority since the current code works.func isFatal(err error) bool { - _, ok := err.(*fatalError) - return ok + var e *fatalError + return errors.As(err, &e) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@notify/relay.go` around lines 259 - 267, The isFatal function currently uses a direct type assertion against *fatalError; change it to use errors.As like isSubscriptionInactive does so wrapped errors are detected too: in isFatal, declare a variable var fe *fatalError and return errors.As(err, &fe) instead of the direct assertion, keeping behavior consistent with isSubscriptionInactive and ensuring future wrapped fatalError values are recognized.ios/VaultSync/Views/DesignSystem.swift (1)
69-91: ⚡ Quick winAdjust
StatusRowaccessibility: current trailing is non-interactive; guard against future interactive trailing
ios/VaultSync/Views/DesignSystem.swiftsets.accessibilityElement(children: .combine)on the wholeStatusRow, but its current call site (ios/VaultSync/Views/ContentView.swift) passestrailingasText(...)only, so there are no interactive descendants being flattened today.- Because
StatusRowaccepts generictrailing: () -> Trailing, consider switching to.accessibilityElement(children: .contain)(or constraining/documentingtrailingto non-interactive views) to prevent futureButton/Link/etc. from becoming hard to reach with VoiceOver.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/VaultSync/Views/DesignSystem.swift` around lines 69 - 91, The StatusRow view currently flattens all children with .accessibilityElement(children: .combine), which can hide interactive descendants passed via the generic trailing: () -> Trailing closure; update StatusRow to use .accessibilityElement(children: .contain) instead (replace the .combine call) so any future interactive trailing content (e.g., Button, Link) remains reachable by VoiceOver, or alternatively document/constrain trailing to non-interactive views if you prefer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Line 72: In the design-lint job's checkout step, replace the floating action
ref "actions/checkout@v6" with the pinned full commit SHA for that release and
add a with block setting persist-credentials: false; specifically update the
checkout step used in the design-lint job (the actions/checkout invocation) to
reference the exact commit SHA and include persist-credentials: false to prevent
the default GitHub token from being persisted.
In `@ios/VaultSync/Views/AddDeviceSheet.swift`:
- Around line 48-50: The Button's disabled check currently uses deviceID.isEmpty
which allows whitespace-only IDs; update the Button(s) (the "Add" Button and any
similar controls around the same block) to disable when
deviceID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty and also add an
early-guard at the start of addDevice() to return if the trimmed ID is empty
(use the same trimming check) so mutations cannot be attempted with
whitespace-only input.
In `@ios/VaultSync/Views/DesignSystem.swift`:
- Around line 30-40: StatusBadge currently hides the icon and replaces its
accessibilityLabel with text, which loses the status semantic
(paused/attention/error) for VoiceOver; update the StatusBadge accessibility so
when text is provided it preserves the status meaning by exposing both the
visible text and the status.label (e.g. set accessibilityLabel to "\(text) —
\(status.label)" or keep accessibilityLabel as text and call
accessibilityValue(status.label)) so VoiceOver announces the badge text and the
status; modify the StatusBadge view (references: StatusBadge, status.label,
text, .accessibilityHidden, .accessibilityLabel, .accessibilityElement) to
include the status semantics alongside the override text.
In `@ios/VaultSync/Views/SubscribePlanPicker.swift`:
- Around line 155-163: The yearlySavingsText computed property currently returns
"Best value" when percent <= 0; change this so that when there is no positive
discount it returns nil instead of L10n.tr("Best value"). Specifically, in
yearlySavingsText (using subscriptionManager.monthlyProduct and
subscriptionManager.yearlyProduct and the percent computation), replace the
guard that currently does "guard percent > 0 else { return L10n.tr("Best value")
}" with a guard that returns nil for non-positive percent so the badge is only
shown when percent > 0.
- Around line 43-63: The Restore Purchases button is only disabled by
isRestoring but can be tapped while subscriptionManager.purchaseInProgress is
true; update the .disabled modifier to disable when either isRestoring or
subscriptionManager.purchaseInProgress, and add a guard at the start of the Task
(or before setting isRestoring) to return early if
subscriptionManager.purchaseInProgress or isRestoring is true so
subscriptionManager.restorePurchases() is not invoked during an ongoing
purchase; reference the isRestoring state, the
subscriptionManager.purchaseInProgress flag, and the
subscriptionManager.restorePurchases() call to locate where to add the checks.
In `@ios/VaultSync/zh-Hans.lproj/Localizable.strings`:
- Line 113: The Chinese translation for the localization key "Last successful
sync was more than %d %@ ago." is ungrammatical; update the value in
zh-Hans.lproj Localizable.strings (the string whose key is "Last successful sync
was more than %d %@ ago.") to a natural Chinese phrase that correctly orders the
placeholders and connector words, for example use a form like "距今上次成功同步已超过
%d%@。" or "上次成功同步距今已超过 %d%@。" so the numeric (%d) and unit (%@) render together
correctly and the sentence reads naturally in the UI.
In `@ios/VaultSyncWidget/VaultSyncWidget.swift`:
- Around line 42-52: buttonLabel currently branches on the raw wire string
(`status == "syncing"`) which misses aliases; change the branching to use the
canonical decoded status on the model (use `entry.snapshot.syncStatus` /
`SyncStatus.fromWire(status)`) instead of the raw `status` string so aliases
like `"scanning"` are handled; update the logic in `buttonLabel` (and the nearby
lines ~249-253) to compare against the canonical enum/case (e.g. the `.syncing`
case or an `isSyncing` helper on `SyncStatus`) and remove reliance on the raw
string.
---
Outside diff comments:
In `@ios/VaultSync/en.lproj/Localizable.strings`:
- Around line 513-516: The displayed route "Settings → Cloud Relay" is stale;
update the four strings containing that phrase (the entries beginning "Enable
Cloud Relay in Settings...", "Cloud Relay — finish server setup", "You’re
subscribed... vaultsync-notify helper...", and "Set up the server helper from
Settings → Cloud Relay → Set Up Your Server.") to point to the new Relay tab
instead (e.g., change "Settings → Cloud Relay → Set Up Your Server" to "Relay →
Set Up Your Server" or "Relay tab → Set Up Your Server" as appropriate), and
apply the same replacement to the other occurrence noted (line 557) and all
localized copies so the activation funnel points to the Relay tab rather than
Settings.
---
Nitpick comments:
In `@ios/VaultSync/Views/DesignSystem.swift`:
- Around line 69-91: The StatusRow view currently flattens all children with
.accessibilityElement(children: .combine), which can hide interactive
descendants passed via the generic trailing: () -> Trailing closure; update
StatusRow to use .accessibilityElement(children: .contain) instead (replace the
.combine call) so any future interactive trailing content (e.g., Button, Link)
remains reachable by VoiceOver, or alternatively document/constrain trailing to
non-interactive views if you prefer.
In `@notify/relay.go`:
- Around line 259-267: The isFatal function currently uses a direct type
assertion against *fatalError; change it to use errors.As like
isSubscriptionInactive does so wrapped errors are detected too: in isFatal,
declare a variable var fe *fatalError and return errors.As(err, &fe) instead of
the direct assertion, keeping behavior consistent with isSubscriptionInactive
and ensuring future wrapped fatalError values are recognized.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7592fca3-50f3-4999-a984-a9ec1429e137
📒 Files selected for processing (40)
.github/workflows/ci.ymldocs/relay-spec.mdios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Resources/Assets.xcassets/AccentColor.colorset/Contents.jsonios/VaultSync/Resources/L10n.swiftios/VaultSync/Resources/Theme.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Services/VaultManager.swiftios/VaultSync/Views/AddDeviceSheet.swiftios/VaultSync/Views/CloudRelayUpsellView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/PendingSharesView.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/SetupChecklistView.swiftios/VaultSync/Views/SubscribePlanPicker.swiftios/VaultSync/Views/SyncActivityView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/InfoPlist.stringsios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSyncTests/SyncStatusTests.swiftios/VaultSyncTests/VaultConflictAttributionTests.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSyncWidget/de.lproj/Localizable.stringsios/scripts/design-token-lint.shnotify/main.gonotify/main_test.gonotify/relay.gonotify/relay_test.go
💤 Files with no reviewable changes (3)
- ios/VaultSync/Views/CloudRelayUpsellView.swift
- ios/VaultSync/Services/SubscriptionManager.swift
- ios/VaultSync/Services/VaultManager.swift
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/*
⚙️ CodeRabbit configuration file
**/*: VaultSync syncs private Obsidian notes through Syncthing. Treat data loss,
privacy leaks, security regressions, and broken sync behavior as high priority.
Do not nitpick formatting unless it affects maintainability, correctness, or public API clarity.
Flag any accidental logging, telemetry, crash reporting, or network transfer of note contents,
vault paths, filenames with private context, API keys, APNs tokens, relay keys, or security-scoped bookmark data.
Files:
ios/VaultSyncWidget/de.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/InfoPlist.stringsios/VaultSyncTests/VaultConflictAttributionTests.swiftdocs/relay-spec.mdios/VaultSync/Views/SyncActivityView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/App/VaultSyncApp.swiftios/scripts/design-token-lint.shios/VaultSync/Views/AddDeviceSheet.swiftios/VaultSync/Resources/L10n.swiftios/VaultSyncTests/SyncStatusTests.swiftios/VaultSync/Resources/Assets.xcassets/AccentColor.colorset/Contents.jsonios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Views/PendingSharesView.swiftios/VaultSync/Views/SubscribePlanPicker.swiftnotify/relay.goios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSync/Resources/Theme.swiftios/VaultSync/Views/SetupChecklistView.swiftnotify/main.gonotify/main_test.gonotify/relay_test.goios/VaultSync/Views/OnboardingView.swiftios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/Views/SettingsView.swiftios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSync/Views/ContentView.swift
**/*.swift
📄 CodeRabbit inference engine (Custom checks)
For Swift background execution changes, pass if work is bounded, cancellation-aware, handles expiration callbacks, and records errors without leaking private vault data. Fail only when background work can continue unbounded, miss cleanup, or violate iOS background execution constraints.
**/*.swift: Follow Swift API Design Guidelines for Swift code
Use Swift strict concurrency where applicable
Files:
ios/VaultSyncTests/VaultConflictAttributionTests.swiftios/VaultSync/Views/SyncActivityView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/AddDeviceSheet.swiftios/VaultSync/Resources/L10n.swiftios/VaultSyncTests/SyncStatusTests.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Views/PendingSharesView.swiftios/VaultSync/Views/SubscribePlanPicker.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSync/Resources/Theme.swiftios/VaultSync/Views/SetupChecklistView.swiftios/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/ContentView.swift
ios/**/*.swift
📄 CodeRabbit inference engine (README.md)
ios/**/*.swift: Use SwiftUI for iOS user interface development
Background execution must use BGAppRefreshTask and BGContinuedProcessingTask for iOS 26+
Files:
ios/VaultSyncTests/VaultConflictAttributionTests.swiftios/VaultSync/Views/SyncActivityView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/AddDeviceSheet.swiftios/VaultSync/Resources/L10n.swiftios/VaultSyncTests/SyncStatusTests.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Views/PendingSharesView.swiftios/VaultSync/Views/SubscribePlanPicker.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSync/Resources/Theme.swiftios/VaultSync/Views/SetupChecklistView.swiftios/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/ContentView.swift
⚙️ CodeRabbit configuration file
ios/**/*.swift: Focus on Swift 6 strict concurrency, Sendable/MainActor correctness, Task cancellation,
retain cycles, memory pressure, SwiftUI observation state, StoreKit/APNs flows, and iOS background execution limits.
Pay special attention to BGAppRefreshTask and BGContinuedProcessingTask behavior, expiration handling,
bounded work, and cleanup when the app is suspended or terminated.
Files:
ios/VaultSyncTests/VaultConflictAttributionTests.swiftios/VaultSync/Views/SyncActivityView.swiftios/VaultSync/Views/DeviceDetailView.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/AddDeviceSheet.swiftios/VaultSync/Resources/L10n.swiftios/VaultSyncTests/SyncStatusTests.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/RelayHomeView.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Views/PendingSharesView.swiftios/VaultSync/Views/SubscribePlanPicker.swiftios/VaultSync/Views/RelayServerSetupView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/DesignSystem.swiftios/VaultSync/Resources/Theme.swiftios/VaultSync/Views/SetupChecklistView.swiftios/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/ContentView.swift
**/*.md
⚙️ CodeRabbit configuration file
**/*.md: Review public documentation for technical accuracy, privacy/security claims, App Store-facing wording,
setup correctness, and consistency with the free app plus optional Cloud Relay subscription model.
Files:
docs/relay-spec.md
.github/workflows/**/*.yml
⚙️ CodeRabbit configuration file
.github/workflows/**/*.yml: Review CI for correct Go test tags, Xcode/iOS simulator assumptions, secret scoping,
dependency integrity, permissions, Docker publishing safety, and unnecessary privilege escalation.
Files:
.github/workflows/ci.yml
**/*.go
📄 CodeRabbit inference engine (README.md)
Follow standard Go conventions for Go code
Files:
notify/relay.gonotify/main.gonotify/main_test.gonotify/relay_test.go
notify/**
📄 CodeRabbit inference engine (README.md)
Cloud Relay authentication uses Syncthing Device ID, which the sidecar reads automatically — no relay key needed
Files:
notify/relay.gonotify/main.gonotify/main_test.gonotify/relay_test.go
notify/**/*.go
⚙️ CodeRabbit configuration file
notify/**/*.go: Review goroutine lifecycle, context cancellation, HTTP timeouts, signal handling, debounce behavior,
Syncthing REST API polling, relay API calls, error classification, and API-key handling.
Flag leaked request bodies, note metadata, Syncthing API keys, relay keys, or APNs-related secrets.
Files:
notify/relay.gonotify/main.gonotify/main_test.gonotify/relay_test.go
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-31T20:28:45.570Z
Learning: vaultsync-notify must be stateless except for configuration; no persistent storage required
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-31T20:28:45.570Z
Learning: Identity in the relay system is based on Syncthing Device IDs; no user accounts or API keys required
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-31T20:28:45.570Z
Learning: The central relay must be horizontally scalable with stateless request handling and a shared database
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-31T20:28:45.570Z
Learning: vaultsync-notify works identically regardless of cloud vs self-hosted relay; the gate for features is the central relay accepting provisioned Device IDs, not feature gates in the container
🪛 zizmor (1.25.2)
.github/workflows/ci.yml
[warning] 72-72: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 72-72: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🔇 Additional comments (47)
ios/VaultSyncWidget/VaultSyncWidget.swift (1)
149-152: LGTM!Also applies to: 158-158, 192-192, 201-201, 209-209, 224-224, 231-231
ios/VaultSync/App/VaultSyncApp.swift (1)
56-57: LGTM!ios/VaultSync/Views/ContentView.swift (13)
18-18: LGTM!Also applies to: 30-36
39-95: LGTM!
97-155: LGTM!
168-168: LGTM!
232-244: LGTM!Also applies to: 246-284, 287-287, 291-291, 316-316, 323-323
332-344: LGTM!Also applies to: 349-349, 360-360, 369-369, 375-375
405-430: LGTM!
467-468: LGTM!Also applies to: 472-472, 496-496
629-634: LGTM!Also applies to: 640-701
703-740: LGTM!
742-761: LGTM!
765-779: LGTM!Also applies to: 818-818, 824-824, 863-863, 880-901
935-935: LGTM!Also applies to: 950-958
ios/VaultSync/Views/OnboardingView.swift (8)
12-18: LGTM!Also applies to: 23-25
29-68: LGTM!
75-84: LGTM!
86-116: LGTM!
119-181: LGTM!
183-233: LGTM!
235-279: LGTM!
281-352: LGTM!ios/VaultSync/Views/SetupChecklistView.swift (4)
12-12: LGTM!Also applies to: 21-22
68-68: LGTM!Also applies to: 71-71, 81-81, 84-84
104-104: LGTM!Also applies to: 107-107, 109-109
122-148: LGTM!docs/relay-spec.md (1)
151-163: LGTM!ios/scripts/design-token-lint.sh (1)
1-37: LGTM!notify/main.go (1)
83-86: LGTM!The outcome-based classification is a clean design that correctly separates:
- Transient failures (retry on debounce cadence)
- Subscription-inactive (slow recheck, no crash)
- Fatal misconfiguration (exit 1)
Timer lifecycle is handled correctly—when entering the
debounceChcase the timer has already fired, so the reassignment at lines 301–302 or 310–311 doesn't leak. Shutdown uses a fresh timeout context, and the package-levelinactiveRecheckIntervalfor test injection is documented appropriately.Also applies to: 173-179, 181-181, 228-237, 286-312, 486-544
notify/main_test.go (1)
267-502: LGTM!Good regression coverage for the crash-loop scenario. I appreciate that tests modifying
inactiveRecheckIntervalare intentionally serial to avoid races on the package-level variable, andt.Cleanupensures servers are properly closed.notify/relay.go (1)
82-119: LGTM!The status-code classification is sensible—treating 400/401/402/403 as subscription-state (self-resolving) rather than fatal prevents crash loops while still failing fast on genuine misconfigurations (404). The
ProbeTriggerchange correctly treats an inactive subscription as "endpoint reachable" for the doctor diagnostic.Also applies to: 159-165, 203-226, 242-267
notify/relay_test.go (1)
1-240: LGTM!Comprehensive test coverage for the status-code classification. The
triggerStubhelper is clean, and the table-driven tests inTestDoTriggerClassifiesStatusCodesmake the expected behavior explicit.TestTriggerRetriesTransientThenSucceedsdirectly exercises the retry loop, which is a good complement to the classification-only tests.ios/VaultSync/zh-Hans.lproj/InfoPlist.strings (1)
2-2: LGTM!ios/VaultSyncWidget/de.lproj/Localizable.strings (1)
3-3: LGTM!ios/VaultSync/Views/RelayHomeView.swift (1)
25-186: LGTM!ios/VaultSync/Views/RelayServerSetupView.swift (1)
31-35: LGTM!Also applies to: 93-103
ios/VaultSync/Views/SettingsView.swift (1)
17-19: LGTM!Also applies to: 164-168
ios/VaultSync/Views/RelayDiagnosticsView.swift (1)
31-38: LGTM!Also applies to: 47-49, 83-85, 124-125, 163-165, 349-380
ios/VaultSync/Views/PendingSharesView.swift (1)
18-24: LGTM!Also applies to: 73-138
ios/VaultSync/Views/SyncIssuesView.swift (1)
60-66: LGTM!ios/VaultSync/Views/ConflictDiffView.swift (1)
93-97: LGTM!Also applies to: 109-110, 151-190, 226-228, 322-323
ios/VaultSync/Views/ConflictListView.swift (1)
5-8: LGTM!Also applies to: 15-18, 72-79
ios/VaultSync/Views/DeviceDetailView.swift (1)
16-23: LGTM!Also applies to: 37-37
ios/VaultSync/Views/LineDiffView.swift (1)
34-39: LGTM!Also applies to: 117-118, 125-126
ios/VaultSync/Views/SyncActivityView.swift (1)
19-19: LGTM!ios/VaultSyncTests/VaultConflictAttributionTests.swift (1)
10-17: LGTM!Also applies to: 19-42
…ADME 1.5.0 hasn't shipped yet, so the redesign folds into the existing entry rather than a new version (no bump). Added the redesign highlights (design system, tabbed home, do-it onboarding, dedicated Cloud Relay tab, never-color-only status, conflict affordance, honest progress), plus the notify sidecar crash-loop fix, the widget false-"all good" fix, and the de/es/zh translation pass. Updated the now-stale "Settings → Cloud Relay" references to the new Relay tab and refreshed the README "What's New" to lead with the redesign.
- Stale activation route: in-app strings + all four catalogs no longer send users to "Settings → Cloud Relay" (removed in the redesign) — they now point to the Relay tab (checklist, onboarding, and the relay-unreachable / auth remediations). - SubscribePlanPicker: yearlySavingsText returns nil instead of a false "Best value" when the yearly plan has no real discount; Restore Purchases is disabled and guarded while a purchase is in progress. - AddDeviceSheet: disable Add and guard addDevice() against whitespace-only IDs. - StatusBadge: expose the status meaning as accessibilityValue when its label is overridden, so VoiceOver keeps the severity. - Widget: buttonLabel branches on the canonical decoded status (handles "scanning"). - zh: fix the ungrammatical "more than %d %@ ago" stale-sync phrasing. Deliberately skipped nitpicks: errors.As in notify isFatal (no wrapping today), StatusRow .combine->.contain (combine is correct for the current non-interactive trailing), and CI checkout SHA-pin/persist-credentials (matches the repo's existing unpinned convention — better as a separate repo-wide change). Key parity 549 across en/de/es/zh; build + full test suite + design-token lint green.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
What & why
Radically redesigns the iOS app UI — a "Vault OS" identity on a Calm-Native foundation (the direction a 16-agent design jury converged on). It turns a structurally-uniform-but-visually-improvised SwiftUI app (two color literals, no design system, prose-only setup, a fractured relay flow) into a coherent, tokenized, navigable, localized experience.
20 focused commits; iOS build + full test suite + the design-token lint and the notify Go suite all green; key screens verified live in the simulator; review passes hardened correctness, a11y, translations, and dead code.
Highlights
Theme.swift(P3, automatic dark mode): one brand accent + a six-meaning semantic status palette + spacing/radius scale, replacing the previous twoColorliterals. New canonicalSyncStatusregistry (symbol + color + label).AccentColoris filled so the app-wide tint is the brand, not the leaked system blue.StatusBadge,StatusRow,DetailRow,ActionCard,MonoField,SyncStatusHeader,vaultCard) now actually adopted across the views that hand-rebuilt those patterns.scenePhase-coupled Syncthing lifecycle is untouched.AddDeviceSheet.SubscribePlanPicker(monthly-first low-commitment entry, yearly framed as "Save N%" from StoreKit prices, prominent price/term/auto-renew + Terms & Privacy links, and a no-device guard) addresses the recorded App Store 3.1.2(a) risk. The old Settings → Cloud Relay section is deleted.4xxfor an inactive subscription no longer kills the helper (it crash-looped underrestart: unless-stopped) — it logs and stays alive.ios/scripts/design-token-lint.sh, wired as a fast CI gate, fails if views reintroduce raw status colors (including return-position / ternary / switch-expression forms) instead of the tokens.Component(s)
Testing
cd go && make patch && go test -tags noassets ./bridgecd notify && go test ./...— green.xcodebuild test— build + full suite green on the iOS 26.5 simulator;ios/scripts/design-token-lint.shpasses.Vault OS Foundation UI Redesign
Overview
This PR radically redesigns the iOS app UI around a shared design-token system ("Vault OS" identity) and reorganizes the information architecture into a three-tab shell. The changes unify sync, device, and relay states with semantic colors and icons (never color-only), introduce reusable components for consistency, consolidate relay features into a dedicated tab, and fix a notify sidecar crash-loop on relay subscription errors.
User-Visible Changes
TabViewwith persistent "Sync," "Devices," and "Relay" tabs, plus a persistent sync-status header (Liquid-Glass style) showing overall status, icon, and subtitle.vaultAccent) is filled and used consistently.SubscribePlanPicker(App Store framing, yearly savings badge). Relay state gated on actual delivery confirmation; no longer shows false "all good" states.Sync Behavior & State Management
SyncStatusregistry unifies statuses (synced, syncing, starting, attention, error, paused) with stable wire encoding, symbol names, semantic colors, and localized labels. Defaults toattentionfor unknown values (safer than silent/green).status?.state == "scanning").pathPrefixparameter onConflictListView.Background & Widget Behavior
accessibilityElement(children: .combine)and explicit VoiceOver labels on status icons.400/401/402/403) from fatal misconfiguration (404), implementing exponential backoff for transient failures and a slower recheck cadence when subscription is inactive.Privacy & Configuration
ios/scripts/design-token-lint.sh) added to CI to prevent regressions (bans raw.red/.green/.orange/.blueliterals in views).Test Coverage
SyncStatusTestscovering wire decoding, round-tripping, urgency logic, and invariants.VaultConflictAttributionTestsvalidating vault-to-conflict path matching and edge cases.notifyintegration tests:TestRunServiceSurvivesInactiveSubscription,TestRunServiceResumesAfterSubscriptionActivates,TestRunServiceExitsOnFatalTrigger.relay_test.gosuite covering HTTP status classification, retry logic, and outcome mapping.Localization
Components Introduced
StatusBadge: Icon + label for sync status.StatusRow: Title/subtitle with optional status glyph and trailing content.DetailRow: Title/value with optional monospaced rendering.ActionCard: Status + message + optional primary action.MonoField: Tap-to-copy monospaced field with haptics and "copied" feedback.SyncStatusHeader: Busy spinner or status icon with title/subtitle.vaultCard()modifier: Consistent card surface with optional accent strip.AddDeviceSheet: Consolidated device-add workflow (QR scan, name entry, add/reset).SubscribePlanPicker: Canonical relay subscription picker (App Store framing, device guard).RelayHomeView: Unified relay pitch and control center.