BackupView.swift (8734B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import taler_swift 10 import SymLog 11 12 let BACKUP = "Taler-" 13 let PREFIX = "file://" 14 15 /// This view shows the list of backups 16 struct BackupView: View { 17 private let symLog = SymLogV(0) 18 let stack: CallStack 19 let navTitle: String 20 21 @EnvironmentObject private var model: WalletModel 22 @EnvironmentObject private var controller: Controller 23 @AppStorage("minimalistic") var minimalistic: Bool = false 24 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 25 26 @State private var created: Bool = false 27 @State private var backups: [String] = [] 28 @State private var filename: String = EMPTYSTRING 29 @State private var selectedBackup: String? = nil 30 @State private var showRestoreAlert: Bool = false 31 @State private var progress: Double = 0 32 @State private var isRestoring: Bool = false 33 @State private var isFinished: Bool = false 34 @State private var finishedMS: Int = 0 35 @State private var isCancelled: Bool = false 36 37 private func restoreBackup(_ path: String) { 38 Task { 39 do { 40 let sendTime = Date.now 41 try await model.importDbFromFile(path: path) 42 let timeUsed = Date.now - sendTime 43 self.finishedMS = timeUsed.milliseconds 44 withAnimation { 45 self.isFinished = true 46 self.progress = 1 47 } 48 controller.hapticFeedback(.medium) 49 } catch {} 50 } 51 } 52 53 private func createBackup(asJSON: Bool) { 54 created = true 55 Task { 56 let dateString = Date().iso 57 let stem = String(BACKUP + dateString) 58 if let backupFile = try? await model.exportDbToFile(stem: stem, asJSON: asJSON) { 59 filename = backupFile 60 await viewDidLoad() 61 controller.hapticFeedback(.medium) 62 } 63 } 64 } 65 66 private var dismissAlertButton: some View { 67 Button("Cancel", role: .cancel) { 68 showRestoreAlert = false 69 withAnimation { selectedBackup = nil } 70 } 71 } 72 private var restoreButton: some View { 73 Button("Restore", role: .destructive) { // TODO: WalletColors().errorColor 74 // didReset = true 75 76 if !isRestoring, 77 let selectedBackup, 78 let docDirUrl = URL.docDirUrl { 79 showRestoreAlert = false 80 withAnimation { 81 self.isRestoring = true 82 } 83 // symLog.log("❗️Restore \(selectedBackup)❗️") 84 // if #available(iOS 16.0, *) { 85 // backupPath = docDirUrl.appending(component: selectedBackup) 86 // } 87 let backupPath = docDirUrl.appendingPathComponent(selectedBackup).absoluteString 88 let path = backupPath.deletingPrefix(PREFIX) 89 symLog.log("❗️Restore \(path)❗️") 90 restoreBackup(path) 91 withAnimation { self.selectedBackup = nil } 92 } 93 } 94 } 95 96 func finishAction() { 97 // withAnimation { 98 // _ = dismissTop(stack.push()) 99 // } 100 } 101 102 @MainActor 103 private func viewDidLoad() async { 104 let fm = FileManager.default 105 if let docDirUrl = URL.docDirUrl { 106 if let contentsOfDocDir = try? fm.contentsOfDirectory(at: docDirUrl, 107 includingPropertiesForKeys: nil, 108 options: []) { 109 let filenames = contentsOfDocDir.map { $0.lastPathComponent } 110 backups = filenames.filter { $0.hasPrefix(BACKUP) }.sorted(by: >) 111 } 112 } 113 } 114 115 var body: some View { 116 #if PRINT_CHANGES 117 let _ = Self._printChanges() 118 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 119 #endif 120 let buttonTitle1 = String(localized: "Create JSON backup", comment: "button") 121 let buttonTitle2 = String(localized: "Create sqlite backup", comment: "button") 122 123 let backupHint = Text("Tap 'Create Backup' to make a copy of your digital money. Connect your iPhone to a computer, then use the Files dialog and copy that backup to the computer.") 124 125 let restoreHint = Text("To restore your digital money, tap on the backup you want to restore from the list below.") 126 127 let restoreHint2 = Text("To restore on a different iPhone, first install the Taler Wallet app there. Then connect it to your computer, and use the Files dialog to copy a previously saved backup into the Taler Wallet. Finally open the Backup/Restore dialog there, and select the backup you just copied.") 128 let hasBackups = !backups.isEmpty 129 let busyRestore = isRestoring || isFinished || isCancelled 130 List { 131 if !busyRestore { 132 Section { 133 backupHint 134 .listRowSeparator(.hidden) 135 FeedbackButton(buttonTitle1, disabled: created) { createBackup(asJSON: true) } 136 .padding(.horizontal) 137 .listRowSeparator(.hidden) 138 FeedbackButton(buttonTitle2, disabled: created) { createBackup(asJSON: false) } 139 .padding(.horizontal) 140 .listRowSeparator(.hidden) 141 restoreHint 142 restoreHint2 143 } 144 .talerFont(.body) 145 } 146 Section { 147 if busyRestore { 148 Text("Restoring the backup:") 149 .talerFont(.title) 150 ProgressView(value: progress) 151 if isFinished || isCancelled { 152 Text(isCancelled ? "Cancelled" : "Finished after \(finishedMS) ms.") 153 .talerFont(.title) 154 .task { 155 DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 156 finishAction() 157 } 158 } 159 } else { 160 Text("Please don't close the app now, or we need to start over next time.") 161 .talerFont(.body) 162 } 163 } else if hasBackups { 164 ForEach(backups, id: \.self) { file in 165 let isSelected = file == selectedBackup 166 HStack { 167 Text(file) 168 .padding(.vertical, 2) 169 Spacer() 170 } 171 .background { 172 Color.primary.opacity(isSelected ? 0.12 : INVISIBLE) 173 } 174 .onTapGesture { 175 withAnimation { selectedBackup = file } 176 showRestoreAlert = true 177 } 178 } 179 } else { 180 Text("No backups yet...") 181 .talerFont(.body) 182 } 183 184 } header: { 185 Text("Available for restore:") 186 .talerFont(.title3) 187 } 188 } 189 .listStyle(myListStyle.style).anyView 190 .task { await viewDidLoad() } 191 .navigationTitle(navTitle) 192 .onChange(of: selectedBackup) { selected in 193 if selected != nil { 194 showRestoreAlert = true 195 } 196 } 197 .alert("Overwrite Wallet", 198 isPresented: $showRestoreAlert, 199 actions: { dismissAlertButton 200 restoreButton }, 201 message: { Text("Are you sure you want to overwrite your wallet with this backup?\nThis cannot be reverted, all money which is now still in your wallet will be lost.") }) 202 .onAppear() { 203 DebugViewC.shared.setViewID(VIEW_BACKUP, stack: stack.push()) 204 } 205 .onNotification(.DatabaseMaintenance) { notification in 206 if let payload = notification.userInfo?[NOTIFICATIONPAYLOAD] as? WalletCore.Payload { 207 if let error = payload.error { 208 model.setError(WalletBackendError.walletCoreError(error)) 209 } else if let percent = payload.completionPercent { 210 withAnimation { 211 progress = Double(percent) / 100 212 } 213 } 214 } 215 } 216 } // body 217 }