Skip to content

Commit 0867083

Browse files
authored
v1.4.0 — conflict-notification controls, relay reliability, contributions (#11)
* feat: stable conflict-notification id + unchanged-count suppression (#10) 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. * feat: in-app "Conflict Notifications" toggle (#10) 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. * fix: decouple Cloud Relay health from alert authorization (#10) 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. * fix: background-sync reliability — single-flight, deadline budget, error-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. * fix: address self-review findings (#10) 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). * feat: one-time contributions + region-correct Cloud Relay pricing 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). * chore: release v1.4.0 (build 25) 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. * chore: set DEVELOPMENT_TEAM in project.yml for regenerate-safe signing 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. * fix: address CodeRabbit review (conflict snapshot, alert tri-state, zh 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. * refactor: remove dead code and polish UI consistency 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 * chore(l10n): add missing keys, drop orphans, keep en/de/zh in sync - 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 * fix(l10n): localize the dashboard "Last sync" relative time 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 * feat(l10n): add Spanish (es) as a fully supported language 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). * refactor(l10n): refresh Support/Cloud Relay copy and polish de/es/zh 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). * fix(l10n): positional args in zh "additional files synced" string 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). * fix: address CodeRabbit (locale-aware conflict date, local signing override) - 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). --------- Co-authored-by: psimaker <psimaker@users.noreply.github.com>
1 parent 9c36fbd commit 0867083

37 files changed

Lines changed: 1897 additions & 428 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,28 @@ All notable changes to VaultSync are documented here.
44

55
---
66

7+
## [1.4.0] — 2026-05-30
8+
9+
### Added
10+
11+
- **Turn off conflict notifications** ([#10](https://github.com/psimaker/vaultsync/issues/10)) — A new Settings → Notifications toggle mutes the sync-conflict banner without touching anything else. It gates only the banner, so Cloud Relay wake-ups and background sync keep working, and turning it off no longer requires disabling iOS notifications for the whole app. Defaults on; existing installs keep their current behaviour.
12+
- **Support VaultSync** — An optional "Support VaultSync" section in Settings offers two one-time contributions (Small and Big). They unlock nothing — VaultSync stays fully functional without them — and you can contribute as often as you like. Localized for English, German, Spanish, and Simplified Chinese.
13+
- **Spanish localization** — VaultSync is now fully localized in Spanish (`es`), joining English, German, and Simplified Chinese across the app and the home-screen widget.
14+
15+
### Changed
16+
17+
- **Cloud Relay price shown correctly per region** — The subscribe button and the subscription details previously mixed the localized App Store price with a hard-coded "$0.99/month", so non-US storefronts saw two different currencies. Both now show a single price taken straight from StoreKit (for example "0,99 € / month" or "A$1.99 / month"), so the displayed price is always correct for the user's storefront.
18+
- **Clearer Support and Cloud Relay wording** — The Settings → Support footer now frames a contribution around keeping VaultSync independent, open-source (MPL-2.0), and ad-free — it still unlocks nothing. The Cloud Relay description explains the silent-push wake-up more plainly and honestly: changes on your server wake the app the moment they happen so incoming sync feels instant, and the relay only sends a wake-up signal — it never sees your notes. The German, Spanish, and Simplified Chinese translations were polished for terminology and punctuation consistency in the same pass.
19+
20+
### Fixed
21+
22+
- **Conflict notifications no longer spam the screen** ([#10](https://github.com/psimaker/vaultsync/issues/10)) — The conflict banner was posted with a fresh identifier on every background sync that reached idle, so iOS never coalesced them: the same "28 conflicts" message re-lit the screen over and over and drained battery. VaultSync now uses a stable notification that replaces itself in place, only alerts (with sound) when the conflict count actually grows, refreshes quietly when it shrinks, and clears when the last conflict is resolved.
23+
- **Disabling notifications no longer reports Cloud Relay as broken** ([#10](https://github.com/psimaker/vaultsync/issues/10)) — Silent push wake-ups do not need alert permission, but the app used to flag APNs/relay as "failed" purely because notifications were turned off, cascading a misleading "relay broken" message across diagnostics. Relay health is now judged from the things that actually matter (subscription, APNs token, provisioning, and a recent silent-push trigger), and the alert-permission state is shown as a separate, informational row.
24+
- **More reliable background sync** — Two background wake-ups that fired at the same time could tear down each other's sync mid-transfer; a single-flight guard now prevents that. The silent-push time budget is measured from the start of the run so long setups can't overrun iOS's window and get future wake-ups throttled. And a folder stuck in an error state no longer spins out the full background deadline or raises a misleading "Background Sync Timed Out".
25+
- **Localized conflict notification text** — The conflict banner body was English-only on German and Simplified Chinese devices; it is now translated.
26+
27+
---
28+
729
## [1.3.2] — 2026-05-23
830

931
### Fixed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ VaultSync is also not a magic always-on Syncthing daemon for iOS. Apple’s back
9999

100100
---
101101

102-
## What’s New — v1.3.2
102+
## What’s New — v1.4.0
103103

104-
> **Skip on iPhone now actually skips returning conflicts** — Tapping "Always skip on this iPhone" in the conflict resolver previously added only the original file's path to the ignore list, so a fresh sync-conflict copy with a new timestamp would arrive from the desktop and the conflict reappeared. Skip now also covers future conflict copies of the same file, removes any existing copies on disk, and groups the pair as a single row in Sync Filters.
104+
> **Quiet conflict notifications — and a switch to turn them off** ([#10](https://github.com/psimaker/vaultsync/issues/10)) — VaultSync no longer re-posts the same "N conflicts" banner on every background sync. It now replaces a single notification in place, only alerts when the count actually grows, and adds a Settings toggle so you can mute conflict banners without disabling iOS notifications — which no longer makes Cloud Relay look "broken". Background sync is more reliable too, the Cloud Relay price now displays correctly in every region, and you can optionally support development with a one-time contribution. This release also adds full **Spanish** localization, alongside English, German, and Simplified Chinese.
105105
106106
See [CHANGELOG.md](CHANGELOG.md) for full details.
107107

docs/setup.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ cd ../ios
4444
xcodegen generate
4545
```
4646

47+
> **Code signing (device builds only):** No development team is committed to the
48+
> repo. To build on a physical device, copy `ios/Signing.local.xcconfig.example`
49+
> to `ios/Signing.local.xcconfig`, set your `DEVELOPMENT_TEAM`, and re-run
50+
> `xcodegen generate`. That file is gitignored, so your team never lands in the
51+
> repo and persists across regenerations. Simulator builds need no team — build
52+
> with `CODE_SIGNING_ALLOWED=NO`.
53+
4754
### 6. Open and build in Xcode
4855

4956
```bash

ios/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ build/
1010
*.xcuserstate
1111
*.xcuserdata
1212
xcuserdata/
13+
14+
# Local signing override (your DEVELOPMENT_TEAM) — see Signing.local.xcconfig.example
15+
Signing.local.xcconfig

ios/Signing.local.xcconfig.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Local, per-developer signing — NOT committed (see ios/.gitignore).
2+
//
3+
// 1. Copy this file to "Signing.local.xcconfig" in the same folder.
4+
// 2. Set your Apple Developer Team ID below.
5+
// 3. Run `xcodegen generate`. Your team now persists across regenerations.
6+
//
7+
// Simulator-only builds don't need this; leave it unset and build with
8+
// CODE_SIGNING_ALLOWED=NO.
9+
10+
DEVELOPMENT_TEAM = ABCDE12345

ios/Signing.xcconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Base signing configuration for VaultSync (committed — contains no team).
2+
//
3+
// Automatic signing is the default. Your Apple Developer Team ID lives in the
4+
// gitignored Signing.local.xcconfig (copy it from Signing.local.xcconfig.example).
5+
// The optional include below is silently skipped when that file is absent, so a
6+
// fresh clone or CI can `xcodegen generate` and build for the Simulator without
7+
// forcing any developer team onto contributors.
8+
CODE_SIGN_STYLE = Automatic
9+
10+
#include? "Signing.local.xcconfig"

ios/VaultSync.storekit

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"identifier" : "VAULTSYNC_STOREKIT",
3+
"nonRenewingSubscriptions" : [],
4+
"products" : [
5+
{
6+
"displayPrice" : "2.99",
7+
"familyShareable" : false,
8+
"internalID" : "1001",
9+
"localizations" : [
10+
{
11+
"description" : "A one-time contribution to support VaultSync development. Unlocks nothing.",
12+
"displayName" : "Small Contribution",
13+
"locale" : "en_US"
14+
}
15+
],
16+
"productID" : "eu.vaultsync.app.contribution.small",
17+
"referenceName" : "Small Contribution",
18+
"type" : "Consumable"
19+
},
20+
{
21+
"displayPrice" : "9.99",
22+
"familyShareable" : false,
23+
"internalID" : "1002",
24+
"localizations" : [
25+
{
26+
"description" : "A larger one-time contribution to support VaultSync development. Unlocks nothing.",
27+
"displayName" : "Big Contribution",
28+
"locale" : "en_US"
29+
}
30+
],
31+
"productID" : "eu.vaultsync.app.contribution.big",
32+
"referenceName" : "Big Contribution",
33+
"type" : "Consumable"
34+
}
35+
],
36+
"settings" : {
37+
"_locale" : "en_US",
38+
"_storefront" : "USA"
39+
},
40+
"subscriptionGroups" : [
41+
{
42+
"id" : "2001",
43+
"localizations" : [],
44+
"name" : "Cloud Relay",
45+
"subscriptions" : [
46+
{
47+
"adHocOffers" : [],
48+
"codeOffers" : [],
49+
"displayPrice" : "0.99",
50+
"familyShareable" : false,
51+
"groupNumber" : 1,
52+
"internalID" : "2002",
53+
"introductoryOffer" : null,
54+
"localizations" : [
55+
{
56+
"description" : "Silent push wake-ups for faster server-to-iPhone sync.",
57+
"displayName" : "Cloud Relay",
58+
"locale" : "en_US"
59+
}
60+
],
61+
"productID" : "eu.vaultsync.app.relay.monthly",
62+
"recurringSubscriptionPeriod" : "P1M",
63+
"referenceName" : "Cloud Relay",
64+
"subscriptionGroupID" : "2001",
65+
"type" : "RecurringSubscription"
66+
}
67+
]
68+
}
69+
],
70+
"version" : {
71+
"major" : 4,
72+
"minor" : 0
73+
}
74+
}

ios/VaultSync/App/AppDelegate.swift

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import UIKit
2-
import UserNotifications
32
import os
43

54
private let logger = Logger(subsystem: "eu.vaultsync.app", category: "appdelegate")
@@ -13,7 +12,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
1312
application.registerForRemoteNotifications()
1413
logger.info("Registered for remote notifications")
1514
logger.debug("Custom URL routing is handled by VaultSyncApp.onOpenURL; AppDelegate remains dedicated to push and background delivery")
16-
Task { await refreshNotificationAuthorizationState() }
15+
// NOTE: We deliberately do NOT flag APNs/relay as failed based on alert
16+
// authorization. Silent (content-available) pushes — the relay's wake
17+
// mechanism — are delivered regardless of UNAuthorizationStatus. The
18+
// live alert-permission state is surfaced as informational in Relay
19+
// Diagnostics instead (see SubscriptionManager.alertBannerStatus).
1720
return true
1821
}
1922

@@ -60,7 +63,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
6063
switch result {
6164
case .synced:
6265
completionHandler(.newData)
63-
case .alreadyIdle, .noFoldersConfigured:
66+
case .alreadyIdle, .noFoldersConfigured, .settledWithFolderError:
6467
completionHandler(.noData)
6568
case .noBookmarkAccess, .bridgeStartFailed, .notIdleBeforeDeadline, .failed:
6669
completionHandler(.failed)
@@ -81,12 +84,4 @@ class AppDelegate: NSObject, UIApplicationDelegate {
8184

8285
return base
8386
}
84-
85-
private func refreshNotificationAuthorizationState() async {
86-
let settings = await UNUserNotificationCenter.current().notificationSettings()
87-
guard settings.authorizationStatus == .denied else { return }
88-
APNsRegistrationStore.markFailed(
89-
reason: L10n.tr("Notifications are disabled for VaultSync. Enable them in iOS Settings > Notifications > VaultSync, then retry APNs registration.")
90-
)
91-
}
9287
}

ios/VaultSync/App/VaultSyncApp.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ struct VaultSyncApp: App {
1616
private static let foregroundRescanThreshold: TimeInterval = 5
1717

1818
init() {
19+
// Conflict banners default ON. Registered defaults are per-process and
20+
// not persisted, so the background handler still relies on its own
21+
// `?? true` fallback — this only keeps foreground `bool(forKey:)` reads
22+
// consistent before the user ever touches the toggle.
23+
UserDefaults.standard.register(
24+
defaults: [BackgroundSyncService.conflictNotificationsEnabledKey: true]
25+
)
1926
BackgroundSyncService.registerTasks()
2027
logger.info("VaultSync starting")
2128
Task.detached(priority: .utility) {
@@ -50,6 +57,7 @@ struct VaultSyncApp: App {
5057
.onChange(of: scenePhase) { _, newPhase in
5158
switch newPhase {
5259
case .active:
60+
BackgroundSyncService.setSceneActive(true)
5361
BackgroundSyncService.endBackgroundAssertion()
5462
BackgroundSyncService.cancelContinuedProcessing()
5563
if !SyncBridgeService.isRunning() {
@@ -75,6 +83,7 @@ struct VaultSyncApp: App {
7583
lastBackgroundedAt = nil
7684
case .background:
7785
lastBackgroundedAt = Date()
86+
BackgroundSyncService.setSceneActive(false)
7887

7988
// Release the foreground lifecycle lock so silent-push and
8089
// BGAppRefresh handlers can manage Syncthing when the process

ios/VaultSync/Models/RelayProvisionStatus.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ enum APNsRegistrationStore {
6666
static let tokenDidChangeNotification = Notification.Name("APNsDeviceTokenDidChange")
6767

6868
struct Snapshot: Equatable, Sendable {
69-
let status: APNsRegistrationStatus
70-
let failureReason: String?
7169
let updatedAt: Date?
7270
let lastSuccessAt: Date?
7371
let lastFailureAt: Date?
@@ -89,16 +87,7 @@ enum APNsRegistrationStore {
8987

9088
static func snapshot() -> Snapshot {
9189
let defaults = UserDefaults.standard
92-
let status = current()
93-
let failureReason: String?
94-
if case .failed(let reason) = status {
95-
failureReason = reason
96-
} else {
97-
failureReason = nil
98-
}
9990
return Snapshot(
100-
status: status,
101-
failureReason: failureReason,
10291
updatedAt: defaults.object(forKey: updatedAtKey) as? Date,
10392
lastSuccessAt: defaults.object(forKey: lastSuccessAtKey) as? Date,
10493
lastFailureAt: defaults.object(forKey: lastFailureAtKey) as? Date

0 commit comments

Comments
 (0)