v1.4.0 — conflict-notification controls, relay reliability, contributions#11
Conversation
) The conflict banner was posted with a fresh UUID identifier on every silent push that reached idle, so iOS never coalesced them — each was a new banner that lit the screen even when the count was identical ("28 conflicts" over and over), draining battery. - Stable "sync-conflict" identifier: a re-post replaces the existing banner instead of stacking a new one. - Persist the last-notified count; only alert (with sound) when the count rises, suppress entirely when unchanged, refresh quietly when it falls, clear when it hits zero. - Re-baseline suppression from the foreground poll so a banner the user has already seen is not re-alerted, and a brand-new conflict after a full resolve still alerts instead of being read as a decrease. - Localize the previously hard-coded English notification body (en/de/zh-Hans). Decision logic extracted to a pure conflictNotificationAction(...) with tests.
Adds a dedicated Settings → Notifications toggle that gates only the conflict banner. Cloud Relay silent-push wake-ups are untouched, so users can silence conflict spam without disabling iOS notifications (which the app otherwise misreads as the relay being broken — addressed separately). - Gate read at the top of notifyConflictsIfAny(), before the per-folder disk scan, so turning banners off also skips that recurring I/O. - Default ON via `object(forKey:) as? Bool ?? true` so existing installs (key absent after upgrade) are NOT silently muted — never `bool(forKey:)`. - Toggle lives in its own Notifications section, independent of the Cloud Relay IAP block; @AppStorage-backed, default registered at launch. - Strings localized for en/de/zh-Hans.
Turning off iOS notifications to silence the conflict banner made the app report Cloud Relay as broken: refreshNotificationAuthorizationState() forced APNs status to .failed purely because alert authorization was .denied. But silent (content-available) pushes — the relay's wake mechanism — are delivered regardless of alert authorization, so this was a false negative that scared users off the exact workaround they needed, and cascaded a red "relay broken" narrative across diagnostics, provisioning hints, and the APNs retry button. - AppDelegate: stop marking APNs/relay failed on alert-denied entirely. Genuine APNs problems still surface via didFailToRegisterForRemoteNotifications. - SubscriptionManager: expose alertAuthorizationDenied (informational) and a composite relayDeliveryLikelyWorking signal built from subscription + token + provisioned + (recent silent-push trigger OR healthy endpoint) — independent of alert authorization. - RelayDiagnostics: show alert-banner permission as an informational row with a clarifying caption, a positive "delivering wake-ups" line, and reworded hints that no longer imply silent push needs notification banners. - Strings localized for en/de/zh-Hans.
…ror-idle Three independent reliability fixes surfaced while auditing #10: - Single-flight guard around performBackgroundSync: two concurrent background wake-ups (silent push + BGAppRefresh, or two pushes) could have one task's expiration/cleanup stop the bridge mid-sync of the other, silently aborting a transfer. The second caller now just nudges a rescan and returns. - Absolute deadline from sync start instead of "now": the silent-push setup waits (folder availability, wake-evidence, optional forced restart) already consume part of iOS's ~30s content-available budget. Budgeting from start keeps total wall-clock under budget so overruns don't throttle future wakes. - Error-state folders no longer spin the full deadline: a folder stuck in "error" can never reach idle, so the wait loop used to burn the whole budget and raise a misleading "Background Sync Timed Out". allFoldersSettledOrErrored breaks early and the outcome is classified as settled, not timed out. Idle/settlement logic extracted to a pure folderSettlement(...) with tests. Note: cross-mechanism races with BGContinuedProcessing are out of scope here (tracked for a follow-up); this covers the performBackgroundSync re-entry path.
Adversarial review of the branch surfaced three real bugs and three worth-fixing nits, all corrected here: - L1 (medium): the foreground 2s poll keeps running during the ~30s post- background grace window and was silently re-baselining the suppression count, so a conflict arriving in that window would be read as "unchanged" by the next silent push and never alert. Added a scene-active flag; the poll only re-baselines while the scene is genuinely active, freezing the baseline once backgrounded so such conflicts stay a genuine rise. - L2 (medium): classifying an error-settled run as .synced made the widget show green "idle" while a folder was actually errored. Added a dedicated SyncResult.settledWithFolderError (isSuccessful=false so the widget shows error, shouldSurfaceIssue=false so it still does NOT raise a misleading "Timed Out" issue). - L5 (low): the fast already-idle path skipped notifyConflictsIfAny entirely; now it notifies before cleanup (a quick conflict could otherwise slip through). - R1: reconcile now skips the write/IPC when the count is unchanged (was hitting usernotificationsd every 2s tick in the zero-conflict steady state). - R2: removed the now-dead `import UserNotifications` from AppDelegate. - R3: split the relay signal into relayDeliveryConfirmed (recent trigger proves end-to-end delivery → "delivering wake-ups") vs relayDeliveryLikelyWorking (endpoint reachable only → "looks reachable"). Also gates notifyConflictsIfAny on !sceneActive so a silent push arriving while the app is open doesn't post a banner over the in-app conflict UI (also removes the foreground-reconcile vs background-notify race, L4). Build/test: fixed a ShapeStyle ternary type error in the new alert-banner row (Color.secondary/Color.green) caught by xcodebuild. App + widget compile clean under Swift 6 strict concurrency; all unit tests pass (23 XCTest + 46 Swift Testing across 10 suites, including the new suppression and folder-settlement suites).
Adds a "Support VaultSync" section to Settings with two one-time, repeatable
contributions (StoreKit consumables) that unlock nothing — they only let users
support development, and can be given as often as they like.
- TipJarManager: loads the two consumables, sorts cheapest→most expensive,
purchases and finishes the transaction (consumable = nothing to unlock, so
finishing is the fulfillment). SubscriptionManager's existing updates loop
finishes any contribution that arrives out-of-band (e.g. approved Ask to Buy).
- SettingsView: own section, per-row localized displayPrice, spinner while
purchasing, a thank-you alert, and graceful unavailable/loading states.
Also fixes the inconsistent Cloud Relay price display: the subscribe button and
the App-Store-Review price line previously mixed StoreKit's localized
displayPrice with a hard-coded "$0.99/month", so non-US storefronts showed two
different currencies. Both now use a single StoreKit-derived
`relayPriceText` ("0,99 € / month", "A$1.99 / month", …) — never a hard-coded
amount.
New product IDs to create in App Store Connect (Consumable):
eu.vaultsync.app.contribution.small (base US $2.99)
eu.vaultsync.app.contribution.big (base US $9.99)
Adds VaultSync.storekit (test-only config, wired via project.yml) so IAP is
actually testable in local Debug builds — the project had none. Strings
localized for en/de/zh-Hans.
App + widget compile under Swift 6; all tests pass (23 XCTest + 46 Swift Testing).
Bump app + widget marketing version to 1.4.0 and build to 25. Add the 1.4.0 CHANGELOG entry (conflict-notification controls + reliability, region-correct Cloud Relay pricing, one-time contributions) and refresh the README "What's New" highlight.
|
Warning Review limit reached
More reviews will be available in 41 minutes and 5 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 (6)
📝 WalkthroughWalkthroughAdds user-controlled conflict-banner toggle and stable conflict-notification logic; serializes background sync wake-ups with a single-flight guard; classifies folder settlement and distinguishes a settled-with-folder-error result; adds StoreKit tip-jar products and a TipJarManager plus localized Cloud Relay pricing; updates UI wiring, multiple localizations (de/en/zh/es), tests, and release metadata. ChangesConflict notifications, relay diagnostics, and contribution system
🎯 4 (Complex) | ⏱️ ~60 minutes
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The .xcodeproj is gitignored and regenerated by xcodegen, so signing set in the Xcode UI is wiped on every `xcodegen generate`. Pin the team (QWTAK63B7C) and automatic signing in project.yml so it persists across regenerations for the app, widget, and test targets.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ios/VaultSync/App/VaultSyncApp.swift (1)
57-107:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
.inactivestill leavessceneActivelatched to the previous value.The new notification baseline logic assumes “foreground-active” means
.activeonly, but this switch only clearssceneActiveon.background. During.inactive, background banner posting stays gated off and the foreground poll can still advance the suppression baseline, which creates a small timing window where a new conflict is treated as already surfaced.Small fix
.onChange(of: scenePhase) { _, newPhase in switch newPhase { case .active: BackgroundSyncService.setSceneActive(true) BackgroundSyncService.endBackgroundAssertion() BackgroundSyncService.cancelContinuedProcessing() if !SyncBridgeService.isRunning() { // Syncthing may have been stopped by a BGTask expiration handler. // Reset Swift-side state so start() works. if syncthingManager.isRunning { syncthingManager.resetForRestart() } vaultManager.restoreAccess() syncthingManager.start() } else if BackgroundSyncService.shouldRescanOnForeground( now: Date(), lastBackgroundedAt: lastBackgroundedAt, threshold: Self.foregroundRescanThreshold ) { // Bridge is still alive but the app spent enough time in // the background that the user likely edited the vault // from another app (e.g. Obsidian). The iOS FSWatcher // doesn't reliably see cross-sandbox writes, so trigger // a fresh scan to pick them up immediately. syncthingManager.triggerForegroundSync() } lastBackgroundedAt = nil + case .inactive: + BackgroundSyncService.setSceneActive(false) case .background: lastBackgroundedAt = Date() BackgroundSyncService.setSceneActive(false)🤖 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/App/VaultSyncApp.swift` around lines 57 - 107, The switch on scenePhase leaves sceneActive true during .inactive; update the onChange handling so .inactive clears the foreground latch by calling BackgroundSyncService.setSceneActive(false) (and any minimal teardown needed to match .background behavior, e.g., releaseForegroundLifecycleLock() if appropriate) — add a .inactive case in the same switch (near the .active/.background cases) that clears sceneActive so the notification baseline logic and suppression polling behave correctly.
🤖 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 `@ios/VaultSync/Services/BackgroundSyncService.swift`:
- Around line 951-966: currentConflictCount() can return 0 on decode failures
which causes notifyConflictsIfAny() to take the .clear branch and remove
delivered notifications incorrectly; change the flow so decode/parse failures
are treated as "unreadable" rather than zero: make currentConflictCount() return
a Result/optional or throw on decode errors (or add a separate
currentConflictCountIsValid flag), update
conflictNotificationAction(currentCount:lastNotifiedCount:) to accept/recognize
an invalid/unreadable state and map that state to .suppress (or a new .unknown
state that does not clear notifications), and update notifyConflictsIfAny() to
avoid calling UNUserNotificationCenter.removeDeliveredNotifications or resetting
lastNotifiedConflictCountKey when the count is invalid/unreadable (use
identifiers: currentConflictCount(), conflictNotificationAction(),
notifyConflictsIfAny(), lastNotifiedConflictCountKey,
conflictNotificationIdentifier to find the places to change).
In `@ios/VaultSync/Views/RelayDiagnosticsView.swift`:
- Around line 129-133: The diagnostics row currently uses
subscriptionManager.alertAuthorizationDenied (a Bool) which treats any
non-denied state as "Allowed"; update RelayDiagnosticsView to drive the UI from
real UNNotificationSettings instead: add a computed property on
SubscriptionManager (e.g., notificationAlertStatus or a tri-state enum like
.allowed/.denied/.unknown) that calls
UNUserNotificationCenter.current().getNotificationSettings and evaluates
settings.authorizationStatus == .authorized AND settings.alertSetting ==
.enabled to return .allowed, return .denied for .denied or alertSetting ==
.disabled, otherwise .unknown; then replace the Text and foregroundStyle usage
in RelayDiagnosticsView to switch on that new property and show
"Allowed"/"Denied"/"Unknown" with appropriate colors (Color.green,
Color.secondary, Color.yellow) so the row reflects true alert banner capability
instead of a single Bool.
In `@ios/VaultSync/zh-Hans.lproj/Localizable.strings`:
- Line 524: Update the translation for the string "Show a banner when sync
conflicts are detected. Turning this off does not affect Cloud Relay or
background sync — your vault keeps syncing." so it uses the product's "Vault"
terminology instead of the generic "你的库", e.g. change "你的库会继续同步" to "你的 Vault
会继续同步" or "你的库(Vault)会继续同步" in the Localizable.strings entry to match other
Vault wording.
---
Outside diff comments:
In `@ios/VaultSync/App/VaultSyncApp.swift`:
- Around line 57-107: The switch on scenePhase leaves sceneActive true during
.inactive; update the onChange handling so .inactive clears the foreground latch
by calling BackgroundSyncService.setSceneActive(false) (and any minimal teardown
needed to match .background behavior, e.g., releaseForegroundLifecycleLock() if
appropriate) — add a .inactive case in the same switch (near the
.active/.background cases) that clears sceneActive so the notification baseline
logic and suppression polling behave correctly.
🪄 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: 623edf3f-be8d-4145-9906-1a2bbb1ba968
📒 Files selected for processing (16)
CHANGELOG.mdREADME.mdios/VaultSync.storekitios/VaultSync/App/AppDelegate.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSyncTests/BackgroundSyncServiceTests.swiftios/project.yml
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*
⚙️ 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/project.ymlios/VaultSync.storekitios/VaultSync/Services/SubscriptionManager.swiftCHANGELOG.mdREADME.mdios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/Views/SettingsView.swiftios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSyncTests/BackgroundSyncServiceTests.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/BackgroundSyncService.swift
ios/project.yml
⚙️ CodeRabbit configuration file
ios/project.yml: This generates the Xcode project and Info.plist. Review changes for bundle ID,
entitlements, background modes, URL schemes, signing settings, and accidental secret exposure.
Files:
ios/project.yml
**/*.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.
Follow Swift API Design Guidelines for API naming, parameter conventions, and code organization in Swift files
Use Swift strict concurrency where applicable to ensure thread-safe code in Swift
Files:
ios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSyncTests/BackgroundSyncServiceTests.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/BackgroundSyncService.swift
ios/**/*.swift
📄 CodeRabbit inference engine (README.md)
Use SwiftUI for UI development in the iOS app
Implement BGAppRefreshTask and BGContinuedProcessingTask for iOS background execution in the iOS app
Use APNs silent notifications via Cloud Relay for push wake-ups in the iOS app
Files:
ios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSyncTests/BackgroundSyncServiceTests.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/BackgroundSyncService.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/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/App/VaultSyncApp.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSyncTests/BackgroundSyncServiceTests.swiftios/VaultSync/App/AppDelegate.swiftios/VaultSync/Services/BackgroundSyncService.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:
CHANGELOG.mdREADME.md
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-30T06:20:57.301Z
Learning: Use Conventional Commits format for all commit messages
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-30T06:20:57.301Z
Learning: Provide clear PR descriptions in all pull requests
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-30T06:20:57.301Z
Learning: Sync Obsidian vaults directly into Obsidian's iOS sandbox folder
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-30T06:20:57.301Z
Learning: Use Syncthing v2.x via Go/gomobile .xcframework as the sync engine
🔇 Additional comments (11)
ios/VaultSyncTests/BackgroundSyncServiceTests.swift (1)
5-68: LGTM!ios/VaultSync/de.lproj/Localizable.strings (1)
517-562: LGTM!ios/VaultSync/en.lproj/Localizable.strings (1)
518-563: LGTM!CHANGELOG.md (1)
7-24: LGTM!README.md (1)
102-104: LGTM!ios/project.yml (1)
33-35: LGTM!Also applies to: 54-55, 104-105
ios/VaultSync/Services/SubscriptionManager.swift (1)
28-88: LGTM!Also applies to: 255-255
ios/VaultSync/Views/RelayDiagnosticsView.swift (1)
31-39: LGTM!Also applies to: 317-321
ios/VaultSync.storekit (1)
1-74: LGTM!ios/VaultSync/Services/TipJarManager.swift (1)
8-97: LGTM!ios/VaultSync/Views/SettingsView.swift (1)
13-15: LGTM!Also applies to: 22-23, 99-109, 196-197, 234-336
…h wording) - Unreadable conflict snapshot no longer mistaken for "no conflicts": currentConflictCount() returns Int? and a folder-list or per-folder decode failure now suppresses instead of collapsing to 0. The old behaviour removed the delivered banner and reset the baseline on a transient read failure, then re-alerted the still-present conflicts as new on the next successful read. - "Alert Banners" diagnostics row reflects real capability via a tri-state (allowed/denied/unknown) read from UNNotificationSettings — authorized alone isn't enough; a user can keep authorization but turn banners off (alertSetting == .disabled). Replaces the previous denied/allowed Bool. - zh-Hans: use "Vault" instead of the generic "库" in the conflict-notifications toggle footer, matching the product terminology used elsewhere. Skipped the .inactive scene-phase suggestion: .inactive is a transient foreground sub-state (app switcher, Control Center, incoming call) and .background reliably clears the scene-active latch, so clearing it on .inactive would instead post conflict banners over the on-screen app.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Dead code (all verified unreferenced across app, widget and tests): - drop unused bridge wrappers getConfigJSON / setDiscoveryEnabled - drop unused isPresetActive, isFolderSyncing and two folderLabel props - drop never-read APNsRegistrationStore.Snapshot.status/failureReason UI / visual polish (clear-improvement only, no v1.4.0 logic change): - shared Color.vaultTeal/.vaultSlate palette (app + widget); unify the syncing-state colour across dashboard, vault rows and widget - ConflictListView reads live conflictFiles so a resolved conflict vanishes, with an "all resolved" empty state; shared locale-aware conflict-date formatter so the diff header no longer shows the raw Syncthing timestamp - line-diff legend (+/- symbols, colourblind/VoiceOver-safe) and equal-width row backgrounds; ASCII "..." -> "…" - Subscribe spinner, Copy Device ID haptic + confirmation, Restore Purchases progress, pending-contribution notice - consistent APNs buttons (>=44pt), neutral "Unknown" alert-banner colour, drop the misdirecting conflict "Learn how to fix" link - device status "Offline" -> "Disconnected" to match the detail screen - localize the background continued-processing title/subtitle and both conflict-diff pane titles
- add 46 keys that were silently falling back to English in de/zh (conflict resolution, device removal, background-sync diagnostics, relay errors, VoiceOver hints and the new UI feedback strings) - remove 76 orphaned keys left behind by the onboarding/checklist/relay rewrites, incl. the stale hard-coded relay price and the unused QR Code key - de-duplicate three keys whose de/zh copies had silently diverged, keeping the currently-shipping wording (last-wins) - fix German in-progress tense (Synchronisiert -> Wird synchronisiert) and the zh notification-banner term for consistency - result: en/de/zh at 510 keys each, no duplicates, no orphans, matching format specifiers
The bare Text(lastSync, style: .relative) renders only the magnitude
("2 hr", no "ago") and the appended L10n.tr("ago") leaked an untranslated
English "ago" into de/zh. Replace it with a fully localized relative phrase
from a cached RelativeDateTimeFormatter (unitsStyle .full) — "2 hours ago" /
"vor 2 Stunden" / "2 小时前" — rendered through one "Last sync: %@" key.
- new key reuses the existing "Last sync:" translations; the old key is dropped
- scope is this line only; the six other bare .relative usages are left as-is
- trade-off: the phrase is static (no live tick), fine for a last-sync label
- en/de/zh stay at 510 keys each, no orphans/dups, plutil OK; tests green
Add complete Spanish localization for the app (510 keys) and the home-screen widget (16 keys), mirroring the English source exactly, plus per-locale InfoPlist.strings for both targets. - es.lproj/Localizable.strings + InfoPlist.strings for app and widget - Informal tú register; brand terms (Vault, VaultSync, Cloud Relay, Syncthing, Obsidian, APNs) kept untranslated; every format specifier preserved and order-checked against en - Wire es into knownRegions via the project.yml postGenCommand (Base, de, en, es, "zh-Hans") - CHANGELOG 1.4.0 + README note that Spanish is now supported Verified: plutil -lint clean; es-vs-en parity 510/510 app, 16/16 widget (0 missing/extra/duplicate; format specifiers match); xcodebuild test green (23 XCTest + 46 Swift Testing).
Part B — marketing copy (en + de/es/zh; English-key renames + Swift literals): - Support footer: reframe around VaultSync being an independent, open-source (MPL-2.0), ad-free project; keep the "unlocks nothing / fully functional / give as often as you like" honesty verbatim. - Cloud Relay footer: drop the over-stated "instant sync" claim for the honest silent-push framing — changes wake the app the moment they happen so incoming sync feels instant, and the relay only sends a wake-up signal, never sees your notes. - Onboarding relay line: same concrete, honest framing. - Untouched by design: the StoreKit price line "Cloud Relay — %@", the auto-renew/cancel terms line, and the Subscribe button. Part A — consistency/quality (translated values only; English keys unchanged): - de: "ein Banner" gender fix; unify "rate-limitiert"; standardize prose on "Hintergrundsynchronisation"; "Das Relay" (neuter). - es: "Vault syncing" -> "Vault sincronizándose" (state, matches the checklist siblings; disambiguates it from the "Syncing Vault" action). - zh: unify 限流 and 支持 terminology; full-width parens; parallel "%@ 中…" activity-log phrasing. 4-language parity intact (510 app keys each, 0 dups/orphans, format specifiers matched), plutil -lint OK on all 8 strings files, xcodebuild test green (46 tests).
The zh-Hans translation of "%d additional files synced in %@" reordered the folder before the count without positional specifiers, so String(format:) bound the Int count to %@ (treated as an object pointer → crash) and the folder String to %d. Switched to "%2$@ … %1$d" so the args map to the correct positions; en/de/es already follow en's order. Reachable on Chinese devices when one poll syncs more than 6 files in a single folder (rate-limited activity summary).
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ios/VaultSync/Views/ConflictListView.swift (1)
73-83:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRespect locale + 12/24-hour preferences in
formattedConflictDate
conflictDateDisplayhard-codesDateFormatter’sdateFormat = "yyyy-MM-dd HH:mm"(no localized styles/locale), so the output won’t follow the user’s locale formatting or 12/24-hour setting despite the “locale-aware” doc comment.Suggested fix
private static let conflictDateDisplay: DateFormatter = { let f = DateFormatter() - f.dateFormat = "yyyy-MM-dd HH:mm" + f.locale = .autoupdatingCurrent + f.dateStyle = .medium + f.timeStyle = .short return f }()🤖 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/ConflictListView.swift` around lines 73 - 83, The DateFormatter in conflictDateDisplay currently hardcodes dateFormat which ignores user locale and 12/24-hour preferences; update conflictDateDisplay (used by formattedConflictDate) to use localized styles instead of a fixed dateFormat (e.g., set dateStyle and timeStyle such as .medium/.short) and ensure the formatter's locale is Locale.current so output respects the user's locale and 12/24-hour settings; keep the static property and reuse it in formattedConflictDate as before.
🤖 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 `@ios/project.yml`:
- Around line 27-31: The shared ios/project.yml currently hard-codes
DEVELOPMENT_TEAM: QWTAK63B7C in the base settings which forces regenerated
projects to that team; remove this hard-coded DEVELOPMENT_TEAM from
settings.base and instead document or wire signing to a local override or
CI-only mechanism (e.g., per-developer xcconfig or an environment-variable
injected value in CI) so contributors can regenerate without local patches;
update any docs or README to show how to set DEVELOPMENT_TEAM locally and ensure
CODE_SIGN_STYLE remains if needed.
---
Outside diff comments:
In `@ios/VaultSync/Views/ConflictListView.swift`:
- Around line 73-83: The DateFormatter in conflictDateDisplay currently
hardcodes dateFormat which ignores user locale and 12/24-hour preferences;
update conflictDateDisplay (used by formattedConflictDate) to use localized
styles instead of a fixed dateFormat (e.g., set dateStyle and timeStyle such as
.medium/.short) and ensure the formatter's locale is Locale.current so output
respects the user's locale and 12/24-hour settings; keep the static property and
reuse it in formattedConflictDate as before.
🪄 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: eb2f7ffe-3cc9-4b7f-95b8-f9405ebb2f0f
📒 Files selected for processing (30)
CHANGELOG.mdREADME.mdios/VaultSync/App/AppDelegate.swiftios/VaultSync/Models/RelayProvisionStatus.swiftios/VaultSync/Resources/Theme.swiftios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Services/SyncBridgeService.swiftios/VaultSync/Services/SyncthingManager.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Views/IgnorePatternsView.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/OnboardingView.swiftios/VaultSync/Views/RelayDiagnosticsView.swiftios/VaultSync/Views/SettingsView.swiftios/VaultSync/Views/SyncFilterRecommendationSheet.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.stringsios/VaultSync/es.lproj/InfoPlist.stringsios/VaultSync/es.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSyncWidget/de.lproj/Localizable.stringsios/VaultSyncWidget/es.lproj/InfoPlist.stringsios/VaultSyncWidget/es.lproj/Localizable.stringsios/project.yml
💤 Files with no reviewable changes (5)
- ios/VaultSync/Views/SyncFilterRecommendationSheet.swift
- ios/VaultSync/Views/IgnorePatternsView.swift
- ios/VaultSync/Services/SyncthingManager.swift
- ios/VaultSync/Services/SyncBridgeService.swift
- ios/VaultSync/Models/RelayProvisionStatus.swift
✅ Files skipped from review due to trivial changes (6)
- ios/VaultSyncWidget/de.lproj/Localizable.strings
- ios/VaultSyncWidget/es.lproj/InfoPlist.strings
- ios/VaultSync/es.lproj/InfoPlist.strings
- ios/VaultSyncWidget/es.lproj/Localizable.strings
- ios/VaultSync/Resources/Theme.swift
- CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (4)
- README.md
- ios/VaultSync/Views/RelayDiagnosticsView.swift
- ios/VaultSync/App/AppDelegate.swift
- ios/VaultSync/Views/SettingsView.swift
📜 Review details
⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build & Test
🧰 Additional context used
📓 Path-based instructions (4)
**/*.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.
Follow Swift API Design Guidelines for naming, parameter conventions, and overall API structure
Use Swift strict concurrency where applicable to ensure thread-safe code and leverage Swift 6's concurrency features
Files:
ios/VaultSync/Views/OnboardingView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/BackgroundSyncService.swift
ios/**/*.swift
📄 CodeRabbit inference engine (README.md)
Follow SwiftUI conventions for view composition, state management, and layout in iOS UI code
Files:
ios/VaultSync/Views/OnboardingView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/BackgroundSyncService.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/VaultSync/Views/OnboardingView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/BackgroundSyncService.swift
**/*
⚙️ 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/VaultSync/Views/OnboardingView.swiftios/VaultSyncWidget/VaultSyncWidget.swiftios/VaultSync/Views/LineDiffView.swiftios/VaultSync/Views/ConflictDiffView.swiftios/VaultSync/Services/SubscriptionManager.swiftios/VaultSync/Views/ConflictListView.swiftios/VaultSync/Views/SyncIssuesView.swiftios/VaultSync/Services/TipJarManager.swiftios/VaultSync/es.lproj/Localizable.stringsios/project.ymlios/VaultSync/Views/ContentView.swiftios/VaultSync/Services/BackgroundSyncService.swiftios/VaultSync/de.lproj/Localizable.stringsios/VaultSync/zh-Hans.lproj/Localizable.stringsios/VaultSync/en.lproj/Localizable.strings
ios/project.yml
⚙️ CodeRabbit configuration file
ios/project.yml: This generates the Xcode project and Info.plist. Review changes for bundle ID,
entitlements, background modes, URL schemes, signing settings, and accidental secret exposure.
Files:
ios/project.yml
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-30T09:19:16.348Z
Learning: Use Conventional Commits for commit messages to maintain clear project history
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-05-30T09:19:16.348Z
Learning: Provide clear PR descriptions when contributing to explain the purpose, scope, and impact of changes
🔇 Additional comments (7)
ios/VaultSync/Services/TipJarManager.swift (1)
31-33: LGTM!Also applies to: 64-64, 85-86
ios/VaultSync/Views/ConflictDiffView.swift (1)
41-41: LGTM!Also applies to: 56-56, 70-70, 190-232
ios/VaultSync/Views/ContentView.swift (1)
18-29: LGTM!Also applies to: 172-172, 576-576, 776-776
ios/VaultSync/es.lproj/Localizable.strings (1)
1-533: LGTM!ios/VaultSync/zh-Hans.lproj/Localizable.strings (1)
36-36: LGTM!Also applies to: 119-119, 248-249, 356-356, 397-397, 446-446, 481-481, 486-532
ios/VaultSyncWidget/VaultSyncWidget.swift (1)
67-67: LGTM!ios/project.yml (1)
16-16: LGTM!Also applies to: 40-40, 59-60, 106-108, 112-113
…erride) - ConflictListView.conflictDateDisplay hard-coded "yyyy-MM-dd HH:mm" despite its "locale-aware" doc comment, ignoring the user's locale and 12/24-hour setting. Now uses localized dateStyle/timeStyle with an .autoupdatingCurrent locale. - Moved DEVELOPMENT_TEAM out of the committed project.yml so a public-repo contributor's `xcodegen generate` is no longer forced onto the maintainer's team. Signing flows from Signing.xcconfig (committed, no team) which optionally includes a gitignored Signing.local.xcconfig (your DEVELOPMENT_TEAM) via `#include?`. That include is skipped when the file is absent, so fresh clones / CI generate and build for the Simulator unchanged. Copy Signing.local.xcconfig.example to set yours; documented in docs/setup.md. Verified: xcodegen generate succeeds with AND without the local override; no team in the generated pbxproj; build + tests green (23 XCTest + 46 Swift Testing).
v1.4.0
Closes #10, plus background-sync reliability hardening, a region-correct Cloud Relay price display, and an optional tip jar.
✨ Added
eu.vaultsync.app.contribution.small(base US $2.99)eu.vaultsync.app.contribution.big(base US $9.99)🔧 Changed
relayPriceTextinstead of mixing the localizeddisplayPricewith a hard-coded "$0.99/month" (non-US storefronts previously showed two currencies).🐛 Fixed (#10 + reliability)
✅ Verification
xcodebuild teston iPhone 16 sim: BUILD SUCCEEDED, all tests pass (23 XCTest + 46 Swift Testing across 10 suites, including the new suppression and folder-settlement suites).🧪 Notes
VaultSync.storekit(test-only, wired viaproject.yml) so IAP is testable in local Debug builds — the project had none.apns-push-type: background,content-available:1, no alert), which iOS delivers regardless of alert authorization..stignore-aware conflict count + dedupe by original, event-based silent-push wake evidence, BGContinuedProcessing cross-mechanism race.v1.4.0 release: Conflict notification controls, relay reliability, and contribution support
User-visible sync behavior
Background execution and relay reliability
Privacy and security
Test coverage and verification
Miscellaneous