taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }