Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ All notable changes to VaultSync are documented here.

---

## [1.4.0] — 2026-05-30

### Added

- **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.
- **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.
- **Spanish localization** — VaultSync is now fully localized in Spanish (`es`), joining English, German, and Simplified Chinese across the app and the home-screen widget.

### Changed

- **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.
- **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.

### Fixed

- **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.
- **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.
- **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".
- **Localized conflict notification text** — The conflict banner body was English-only on German and Simplified Chinese devices; it is now translated.

---

## [1.3.2] — 2026-05-23

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ VaultSync is also not a magic always-on Syncthing daemon for iOS. Apple’s back

---

## What’s New — v1.3.2
## What’s New — v1.4.0

> **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.
> **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.

See [CHANGELOG.md](CHANGELOG.md) for full details.

Expand Down
7 changes: 7 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ cd ../ios
xcodegen generate
```

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

### 6. Open and build in Xcode

```bash
Expand Down
3 changes: 3 additions & 0 deletions ios/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ build/
*.xcuserstate
*.xcuserdata
xcuserdata/

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

DEVELOPMENT_TEAM = ABCDE12345
10 changes: 10 additions & 0 deletions ios/Signing.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Base signing configuration for VaultSync (committed — contains no team).
//
// Automatic signing is the default. Your Apple Developer Team ID lives in the
// gitignored Signing.local.xcconfig (copy it from Signing.local.xcconfig.example).
// The optional include below is silently skipped when that file is absent, so a
// fresh clone or CI can `xcodegen generate` and build for the Simulator without
// forcing any developer team onto contributors.
CODE_SIGN_STYLE = Automatic

#include? "Signing.local.xcconfig"
74 changes: 74 additions & 0 deletions ios/VaultSync.storekit
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"identifier" : "VAULTSYNC_STOREKIT",
"nonRenewingSubscriptions" : [],
"products" : [
{
"displayPrice" : "2.99",
"familyShareable" : false,
"internalID" : "1001",
"localizations" : [
{
"description" : "A one-time contribution to support VaultSync development. Unlocks nothing.",
"displayName" : "Small Contribution",
"locale" : "en_US"
}
],
"productID" : "eu.vaultsync.app.contribution.small",
"referenceName" : "Small Contribution",
"type" : "Consumable"
},
{
"displayPrice" : "9.99",
"familyShareable" : false,
"internalID" : "1002",
"localizations" : [
{
"description" : "A larger one-time contribution to support VaultSync development. Unlocks nothing.",
"displayName" : "Big Contribution",
"locale" : "en_US"
}
],
"productID" : "eu.vaultsync.app.contribution.big",
"referenceName" : "Big Contribution",
"type" : "Consumable"
}
],
"settings" : {
"_locale" : "en_US",
"_storefront" : "USA"
},
"subscriptionGroups" : [
{
"id" : "2001",
"localizations" : [],
"name" : "Cloud Relay",
"subscriptions" : [
{
"adHocOffers" : [],
"codeOffers" : [],
"displayPrice" : "0.99",
"familyShareable" : false,
"groupNumber" : 1,
"internalID" : "2002",
"introductoryOffer" : null,
"localizations" : [
{
"description" : "Silent push wake-ups for faster server-to-iPhone sync.",
"displayName" : "Cloud Relay",
"locale" : "en_US"
}
],
"productID" : "eu.vaultsync.app.relay.monthly",
"recurringSubscriptionPeriod" : "P1M",
"referenceName" : "Cloud Relay",
"subscriptionGroupID" : "2001",
"type" : "RecurringSubscription"
}
]
}
],
"version" : {
"major" : 4,
"minor" : 0
}
}
17 changes: 6 additions & 11 deletions ios/VaultSync/App/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import UIKit
import UserNotifications
import os

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

Expand Down Expand Up @@ -60,7 +63,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
switch result {
case .synced:
completionHandler(.newData)
case .alreadyIdle, .noFoldersConfigured:
case .alreadyIdle, .noFoldersConfigured, .settledWithFolderError:
completionHandler(.noData)
case .noBookmarkAccess, .bridgeStartFailed, .notIdleBeforeDeadline, .failed:
completionHandler(.failed)
Expand All @@ -81,12 +84,4 @@ class AppDelegate: NSObject, UIApplicationDelegate {

return base
}

private func refreshNotificationAuthorizationState() async {
let settings = await UNUserNotificationCenter.current().notificationSettings()
guard settings.authorizationStatus == .denied else { return }
APNsRegistrationStore.markFailed(
reason: L10n.tr("Notifications are disabled for VaultSync. Enable them in iOS Settings > Notifications > VaultSync, then retry APNs registration.")
)
}
}
9 changes: 9 additions & 0 deletions ios/VaultSync/App/VaultSyncApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ struct VaultSyncApp: App {
private static let foregroundRescanThreshold: TimeInterval = 5

init() {
// Conflict banners default ON. Registered defaults are per-process and
// not persisted, so the background handler still relies on its own
// `?? true` fallback — this only keeps foreground `bool(forKey:)` reads
// consistent before the user ever touches the toggle.
UserDefaults.standard.register(
defaults: [BackgroundSyncService.conflictNotificationsEnabledKey: true]
)
BackgroundSyncService.registerTasks()
logger.info("VaultSync starting")
Task.detached(priority: .utility) {
Expand Down Expand Up @@ -50,6 +57,7 @@ struct VaultSyncApp: App {
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .active:
BackgroundSyncService.setSceneActive(true)
BackgroundSyncService.endBackgroundAssertion()
BackgroundSyncService.cancelContinuedProcessing()
if !SyncBridgeService.isRunning() {
Expand All @@ -75,6 +83,7 @@ struct VaultSyncApp: App {
lastBackgroundedAt = nil
case .background:
lastBackgroundedAt = Date()
BackgroundSyncService.setSceneActive(false)

// Release the foreground lifecycle lock so silent-push and
// BGAppRefresh handlers can manage Syncthing when the process
Expand Down
11 changes: 0 additions & 11 deletions ios/VaultSync/Models/RelayProvisionStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ enum APNsRegistrationStore {
static let tokenDidChangeNotification = Notification.Name("APNsDeviceTokenDidChange")

struct Snapshot: Equatable, Sendable {
let status: APNsRegistrationStatus
let failureReason: String?
let updatedAt: Date?
let lastSuccessAt: Date?
let lastFailureAt: Date?
Expand All @@ -89,16 +87,7 @@ enum APNsRegistrationStore {

static func snapshot() -> Snapshot {
let defaults = UserDefaults.standard
let status = current()
let failureReason: String?
if case .failed(let reason) = status {
failureReason = reason
} else {
failureReason = nil
}
return Snapshot(
status: status,
failureReason: failureReason,
updatedAt: defaults.object(forKey: updatedAtKey) as? Date,
lastSuccessAt: defaults.object(forKey: lastSuccessAtKey) as? Date,
lastFailureAt: defaults.object(forKey: lastFailureAtKey) as? Date
Expand Down
10 changes: 10 additions & 0 deletions ios/VaultSync/Resources/Theme.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SwiftUI

/// Shared brand palette. Single source of truth so the app and the widget
/// render the same accent colors instead of redefining the RGB per file.
extension Color {
/// Brand teal — active / in-progress sync accent.
static let vaultTeal = Color(red: 0 / 255, green: 137 / 255, blue: 123 / 255)
/// Brand slate — muted/inactive accent.
static let vaultSlate = Color(red: 38 / 255, green: 50 / 255, blue: 56 / 255)
}
Loading
Loading