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,7 @@ struct SettingsView: View {
@State private var showingPinDisableAlert = false
@State private var showingRestartAlert = false
@State private var didLoadCloudSync = false
@State private var backupToRestore: BackupRecord?
init(iapService: IAPService) {
self.iapService = iapService
@@ -41,6 +42,9 @@ struct SettingsView: View {
// Data Section
dataSection
if viewModel.backupsEnabled {
backupsSection
}
// Security Section
securitySection
@@ -86,6 +90,26 @@ struct SettingsView: View {
} message: {
Text("This will permanently delete all your investment data. This action cannot be undone.")
}
.confirmationDialog(
"Restore Backup",
isPresented: Binding(
get: { backupToRestore != nil },
set: { if !$0 { backupToRestore = nil } }
),
titleVisibility: .visible
) {
Button("Restore", role: .destructive) {
if let backup = backupToRestore {
viewModel.restoreBackup(backup)
backupToRestore = nil
}
}
Button("Cancel", role: .cancel) {
backupToRestore = nil
}
} message: {
Text("This will replace your current data with the selected backup.")
}
.alert("Success", isPresented: .constant(viewModel.successMessage != nil)) {
Button("OK") {
viewModel.successMessage = nil
@@ -141,6 +165,9 @@ struct SettingsView: View {
}
.onAppear {
didLoadCloudSync = true
if viewModel.backupsEnabled {
viewModel.refreshBackups()
}
}
}
}
@@ -303,8 +330,25 @@ struct SettingsView: View {
if didLoadCloudSync {
showingRestartAlert = true
}
viewModel.refreshBackups()
}
Toggle(
isOn: Binding(
get: { viewModel.backupsEnabled },
set: { viewModel.setBackupsEnabled($0) }
)
) {
HStack(spacing: 8) {
Text("Enable Backups")
if !viewModel.isPremium {
Text("Premium")
.font(.caption.weight(.semibold))
.foregroundColor(.appWarning)
}
}
}
Button {
if viewModel.canExport {
viewModel.showingExportOptions = true
@@ -359,6 +403,66 @@ struct SettingsView: View {
}
}
// MARK: - Backups Section
private var backupsSection: some View {
Section {
Picker("Keep Backups", selection: $viewModel.backupRetentionCount) {
Text("5").tag(5)
Text("10").tag(10)
Text("20").tag(20)
}
.onChange(of: viewModel.backupRetentionCount) { _, newValue in
viewModel.updateBackupRetention(newValue)
}
Button {
viewModel.createBackupNow()
} label: {
HStack {
Label("Create Backup Now", systemImage: "arrow.clockwise")
Spacer()
if viewModel.isBackupInProgress {
ProgressView()
}
}
}
.disabled(viewModel.isBackupInProgress || viewModel.isRestoreInProgress)
if viewModel.backups.isEmpty {
Text("No backups yet.")
.font(.caption)
.foregroundColor(.secondary)
} else {
ForEach(viewModel.backups) { backup in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(backup.date.formatted(date: .abbreviated, time: .shortened))
.font(.subheadline)
Text("\(backup.location.rawValue) · \(formatBytes(backup.size))")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Button("Restore") {
backupToRestore = backup
}
.disabled(viewModel.isRestoreInProgress)
}
.contextMenu {
Button("Share Backup") {
viewModel.shareItem = SettingsViewModel.ShareItem(url: backup.url)
}
}
}
}
} header: {
Text("Backups")
} footer: {
Text("Backups are stored locally and in iCloud (when enabled).")
}
}
// MARK: - Security Section
private var securitySection: some View {
@@ -589,6 +693,12 @@ struct SettingsView: View {
}
return "Portfolio Journal"
}
private func formatBytes(_ bytes: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
return formatter.string(fromByteCount: bytes)
}
}
// MARK: - Activity View