Add premium backups with retention and iCloud support

This commit is contained in:
alexandrev-tibco
2026-02-01 11:23:41 +01:00
parent f97f8026bc
commit b5ba6c47a8
4 changed files with 418 additions and 1 deletions
@@ -20,6 +20,8 @@ class SettingsViewModel: ObservableObject {
@Published var showingExportOptions = false
@Published var showingImportSheet = false
@Published var showingResetConfirmation = false
@Published var isBackupInProgress = false
@Published var isRestoreInProgress = false
@Published var errorMessage: String?
@Published var successMessage: String?
@@ -29,6 +31,12 @@ class SettingsViewModel: ObservableObject {
@Published var totalSnapshots = 0
@Published var totalCategories = 0
// MARK: - Backups
@Published var backupRetentionCount = 10
@Published var backups: [BackupRecord] = []
@Published var backupsEnabled = false
// MARK: - Dependencies
private let iapService: IAPService
@@ -37,6 +45,7 @@ class SettingsViewModel: ObservableObject {
private let categoryRepository: CategoryRepository
private let freemiumValidator: FreemiumValidator
private var cancellables = Set<AnyCancellable>()
private let backupsEnabledKey = "backupsEnabled"
// MARK: - Initialization
@@ -61,7 +70,10 @@ class SettingsViewModel: ObservableObject {
private func setupObservers() {
iapService.$isPremium
.receive(on: DispatchQueue.main)
.assign(to: &$isPremium)
.sink { [weak self] isPremium in
self?.handlePremiumChange(isPremium)
}
.store(in: &cancellables)
iapService.$isFamilyShared
.receive(on: DispatchQueue.main)
@@ -82,6 +94,13 @@ class SettingsViewModel: ObservableObject {
analyticsEnabled = settings.enableAnalytics
currencyCode = settings.currency
inputMode = InputMode(rawValue: settings.inputMode) ?? .simple
backupRetentionCount = loadBackupRetention()
backupsEnabled = loadBackupsEnabled()
if backupsEnabled {
refreshBackups()
} else {
backups = []
}
// Load statistics directly from database to avoid async race conditions
loadStatistics()
@@ -361,6 +380,85 @@ class SettingsViewModel: ObservableObject {
NotificationCenter.default.post(name: .didResetData, object: nil)
}
// MARK: - Backups
func updateBackupRetention(_ count: Int) {
guard backupsEnabled, isPremium else { return }
backupRetentionCount = count
UserDefaults.standard.set(count, forKey: "backupRetentionCount")
refreshBackups()
}
func refreshBackups() {
guard backupsEnabled, isPremium else { return }
let includeICloud = UserDefaults.standard.bool(forKey: "cloudSyncEnabled")
backups = BackupService.shared.listAllBackups(includeICloud: includeICloud)
}
func createBackupNow() {
guard backupsEnabled, isPremium else { return }
guard !isBackupInProgress else { return }
isBackupInProgress = true
errorMessage = nil
Task {
let includeICloud = UserDefaults.standard.bool(forKey: "cloudSyncEnabled")
let records = BackupService.shared.createBackup(
retentionCount: backupRetentionCount,
includeICloud: includeICloud
)
await MainActor.run {
backups = records
isBackupInProgress = false
successMessage = "Backup saved"
}
}
}
func restoreBackup(_ backup: BackupRecord) {
guard backupsEnabled, isPremium else { return }
guard !isRestoreInProgress else { return }
isRestoreInProgress = true
errorMessage = nil
Task {
let content: String
do {
content = try String(contentsOf: backup.url, encoding: .utf8)
} catch {
await MainActor.run {
isRestoreInProgress = false
errorMessage = "Failed to read backup file."
}
return
}
await MainActor.run {
resetAllData()
}
let allowMultiple = iapService.isPremium
let result = await ImportService.shared.importDataAsync(
content: content,
format: .json,
allowMultipleAccounts: allowMultiple,
defaultAccountName: Account.defaultAccountName,
progress: { _ in }
)
await MainActor.run {
isRestoreInProgress = false
if result.errors.isEmpty {
successMessage = "Backup restored"
} else {
errorMessage = "Backup restored with warnings."
}
loadSettings()
refreshBackups()
}
}
}
// MARK: - Computed Properties
var appVersion: String {
@@ -405,4 +503,52 @@ class SettingsViewModel: ObservableObject {
return 0
}
}
private func loadBackupRetention() -> Int {
let value = UserDefaults.standard.integer(forKey: "backupRetentionCount")
let options = [5, 10, 20]
return options.contains(value) ? value : 10
}
private func loadBackupsEnabled() -> Bool {
let enabled = UserDefaults.standard.bool(forKey: backupsEnabledKey)
if !isPremium && enabled {
UserDefaults.standard.set(false, forKey: backupsEnabledKey)
return false
}
return isPremium && enabled
}
private func handlePremiumChange(_ isPremium: Bool) {
self.isPremium = isPremium
if isPremium {
backupsEnabled = UserDefaults.standard.bool(forKey: backupsEnabledKey)
if backupsEnabled {
refreshBackups()
}
} else {
if backupsEnabled {
backupsEnabled = false
}
UserDefaults.standard.set(false, forKey: backupsEnabledKey)
backups = []
}
}
func setBackupsEnabled(_ enabled: Bool) {
guard isPremium else {
backupsEnabled = false
UserDefaults.standard.set(false, forKey: backupsEnabledKey)
showingPaywall = true
return
}
backupsEnabled = enabled
UserDefaults.standard.set(enabled, forKey: backupsEnabledKey)
if enabled {
refreshBackups()
} else {
backups = []
}
}
}