BackupView.swift (8798B)
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 // the error was already shown (viewHandles: false) - back to the list of backups 51 withAnimation { 52 self.isRestoring = false 53 self.progress = 0 54 } 55 } 56 } 57 } 58 59 private func createBackup(asJSON: Bool) { 60 created = true 61 Task { 62 let dateString = Date().iso 63 let stem = String(BACKUP + dateString) 64 if let backupFile = try? await model.exportDbToFile(stem: stem, asJSON: asJSON) { 65 filename = backupFile 66 await viewDidLoad() 67 controller.hapticFeedback(.medium) 68 } 69 } 70 } 71 72 private var dismissAlertButton: some View { 73 Button("Cancel", role: .cancel) { 74 showRestoreAlert = false 75 withAnimation { selectedBackup = nil } 76 } 77 } 78 private var restoreButton: some View { 79 Button("Restore", role: .destructive) { // TODO: WalletColors().errorColor 80 // didReset = true 81 82 if !isRestoring, 83 let selectedBackup, 84 let docDirUrl = URL.docDirUrl { 85 showRestoreAlert = false 86 withAnimation { 87 self.isRestoring = true 88 } 89 // .path doesn't percent-encode e.g. "Taler-… 2.json" into "Taler-…%202.json" 90 let path = docDirUrl.appendingPathComponent(selectedBackup).path(withSlash: false) 91 symLog.log("❗️Restore \(path)❗️") 92 restoreBackup(path) 93 withAnimation { self.selectedBackup = nil } 94 } 95 } 96 } 97 98 func finishAction() { 99 // withAnimation { 100 // _ = dismissTop(stack.push()) 101 // } 102 } 103 104 @MainActor 105 private func viewDidLoad() async { 106 let fm = FileManager.default 107 if let docDirUrl = URL.docDirUrl { 108 if let contentsOfDocDir = try? fm.contentsOfDirectory(at: docDirUrl, 109 includingPropertiesForKeys: nil, 110 options: []) { 111 let filenames = contentsOfDocDir.map { $0.lastPathComponent } 112 backups = filenames.filter { $0.hasPrefix(BACKUP) }.sorted(by: >) 113 } 114 } 115 } 116 117 var body: some View { 118 #if PRINT_CHANGES 119 let _ = Self._printChanges() 120 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 121 #endif 122 let buttonTitle1 = String(localized: "Create JSON backup", comment: "button") 123 let buttonTitle2 = String(localized: "Create sqlite backup", comment: "button") 124 125 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.") 126 127 let restoreHint = Text("To restore your digital money, tap on the backup you want to restore from the list below.") 128 129 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.") 130 let hasBackups = !backups.isEmpty 131 let busyRestore = isRestoring || isFinished || isCancelled 132 List { 133 if !busyRestore { 134 Section { 135 backupHint 136 .listRowSeparator(.hidden) 137 FeedbackButton(buttonTitle1, disabled: created) { createBackup(asJSON: true) } 138 .padding(.horizontal) 139 .listRowSeparator(.hidden) 140 FeedbackButton(buttonTitle2, disabled: created) { createBackup(asJSON: false) } 141 .padding(.horizontal) 142 .listRowSeparator(.hidden) 143 restoreHint 144 restoreHint2 145 } 146 .talerFont(.body) 147 } 148 Section { 149 if busyRestore { 150 Text("Restoring the backup:") 151 .talerFont(.title) 152 ProgressView(value: progress) 153 if isFinished || isCancelled { 154 Text(isCancelled ? "Cancelled" : "Finished after \(finishedMS) ms.") 155 .talerFont(.title) 156 .task { 157 DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 158 finishAction() 159 } 160 } 161 } else { 162 Text("Please don't close the app now, or we need to start over next time.") 163 .talerFont(.body) 164 } 165 } else if hasBackups { 166 ForEach(backups, id: \.self) { file in 167 let isSelected = file == selectedBackup 168 HStack { 169 Text(file) 170 .padding(.vertical, 2) 171 Spacer() 172 } 173 .background { 174 Color.primary.opacity(isSelected ? 0.12 : INVISIBLE) 175 } 176 .onTapGesture { 177 withAnimation { selectedBackup = file } 178 showRestoreAlert = true 179 } 180 } 181 } else { 182 Text("No backups yet...") 183 .talerFont(.body) 184 } 185 186 } header: { 187 Text("Available for restore:") 188 .talerFont(.title3) 189 } 190 } 191 .listStyle(myListStyle.style).anyView 192 .task { await viewDidLoad() } 193 .navigationTitle(navTitle) 194 .onChange(of: selectedBackup) { selected in 195 if selected != nil { 196 showRestoreAlert = true 197 } 198 } 199 .alert("Overwrite Wallet", 200 isPresented: $showRestoreAlert, 201 actions: { dismissAlertButton 202 restoreButton }, 203 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.") }) 204 .onAppear() { 205 DebugViewC.shared.setViewID(VIEW_BACKUP, stack: stack.push()) 206 } 207 .onNotification(.DatabaseMaintenance) { notification in 208 if let payload = notification.userInfo?[NOTIFICATIONPAYLOAD] as? WalletCore.Payload { 209 if let error = payload.error { 210 model.setError(WalletBackendError.walletCoreError(error)) 211 } else if let percent = payload.completionPercent { 212 withAnimation { 213 progress = Double(percent) / 100 214 } 215 } 216 } 217 } 218 } // body 219 }