taler-ios

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

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 }