TalerWallet1App.swift (8070B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * Main app entry point 7 * 8 * @author Jonathan Buchanan 9 * @author Marc Stibane 10 */ 11 import BackgroundTasks 12 import SwiftUI 13 import os.log 14 import SymLog 15 16 @main 17 struct TalerWallet1App: App { 18 #if TALER_NIGHTLY 19 @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 20 #endif 21 private let symLog = SymLogV() 22 @Environment(\.scenePhase) private var phase 23 @AppStorage("pasteAutomatically") var pasteAutomatically: Bool = false 24 @AppStorage("preferredColorScheme") var preferredColorScheme: Int = 0 25 #if DEBUG 26 @AppStorage("developerMode") var developerMode: Bool = true 27 #else 28 @AppStorage("developerMode") var developerMode: Bool = false 29 #endif 30 31 @StateObject private var viewState = ViewState.shared // popToRootView() 32 @StateObject private var viewState2 = ViewState2.shared // popToRootView() 33 @State private var pastedString: String? = nil 34 @State private var pastedWebURL: String? = nil // TODO: debounce 35 36 private let walletCore = WalletCore.shared 37 private let controller = Controller.shared 38 private let model = WalletModel.shared 39 private let debugViewC = DebugViewC.shared 40 let logger = Logger(subsystem: "net.taler.gnu", category: "Main App") 41 private let biometricService = BiometricService.shared 42 43 func scheduleAppRefresh() { 44 let request = BGAppRefreshTaskRequest(identifier: "net.taler.refresh") 45 request.earliestBeginDate = .now.addingTimeInterval(4 * 3600) 46 try? BGTaskScheduler.shared.submit(request) 47 } 48 49 @MainActor 50 func post(_ pastedURL: URL) { 51 Task { 52 let pasteType = PasteType(pastedURL: pastedURL) 53 let userinfo = [NOTIFICATIONPASTE: pasteType] 54 NotificationCenter.default.post(name: .PasteAction, object: nil, userInfo: userinfo) 55 } 56 } 57 58 func inspectPasteboard(playSound: Bool) { 59 let pasteboard = UIPasteboard.general 60 // We are only interested in URLs 61 if !pasteboard.hasURLs { return } 62 63 // users get asked whether they want to paste, and only if they agree we go to completion 64 pasteboard.detectPatterns(for: [UIPasteboard.DetectionPattern.probableWebURL], 65 inItemSet: nil, 66 completionHandler: { result in 67 switch result { 68 case .success(let detectedPatterns): 69 // A pattern detection is completed, 70 // regardless of whether the pasteboard has patterns we care about. 71 // So we have to check if the detected patterns contains our patterns. 72 73 if detectedPatterns.contains([UIPasteboard.DetectionPattern.probableWebURL]) { 74 // Will match if the pasteboard string has a URL within it 75 self.pastedWebURL = pasteboard.string 76 if let string = pasteboard.string { 77 if self.pastedString != string { 78 self.pastedString = string 79 if let pastedURL = string.toURL { 80 let scheme = pastedURL.scheme 81 if scheme?.lowercased() == "taler" { 82 // print(string) 83 post(pastedURL) 84 return 85 } 86 } 87 } 88 } 89 } else { 90 // We won't be retrieving the value, so we won't get a notification banner 91 self.pastedWebURL = nil 92 if playSound { 93 controller.playSound(0) // tell the user we didn't find anything 94 } 95 } 96 case .failure(let error): 97 // This never gets called 98 self.pastedWebURL = error.localizedDescription 99 } 100 }) 101 } 102 103 var body: some Scene { 104 let colorScheme = preferredColorScheme < 0 ? ColorScheme.dark 105 : preferredColorScheme > 0 ? ColorScheme.light 106 : nil // use the current scheme 107 let mainView = MainView(logger: logger, stack: CallStack("App")) 108 .preferredColorScheme(colorScheme) 109 .environmentObject(debugViewC) // change viewID / sheetID 110 .environmentObject(viewState) // popToRoot 111 .environmentObject(viewState2) // popToRoot 112 .environmentObject(controller) 113 .environmentObject(model) 114 .environmentObject(biometricService) 115 .addKeyboardVisibilityToEnvironment() 116 /// external events are taler:// or payto:// URLs passed to this app 117 /// we handle them in .onOpenURL in MainView.swift 118 .handlesExternalEvents(preferring: ["*"], allowing: ["*"]) 119 .task { 120 #if DEBUG 121 let testing = true 122 let delay: TimeInterval = 0.001 123 #else 124 let testing = false 125 let delay: TimeInterval = 2.25 126 #endif 127 try! await controller.initWalletCore(model, stage: developerMode, 128 setTesting: testing, delay: delay) // will (and should) crash on failure 129 } 130 if #available(iOS 16.4, *) { 131 return WindowGroup { 132 mainView 133 } 134 .onChange(of: phase) { newPhase in 135 switch newPhase { 136 case .active: 137 logger.log("❗️.onChange() ==> Active") 138 if pasteAutomatically { 139 inspectPasteboard(playSound: false) 140 } 141 case .background: 142 logger.log("❗️.onChange() ==> Background)") 143 // scheduleAppRefresh() 144 default: break 145 } 146 } 147 .backgroundTask(.appRefresh("net.taler.refresh")) { 148 // symLog.log("backgroundTask running") 149 //#if 0 150 // let request = URLRequest(url: URL(string: "your_backend")!) 151 // guard let data = try? await URLSession.shared.data(for: request).0 else { 152 // return 153 // } 154 // 155 // let decoder = JSONDecoder() 156 // guard let products = try? decoder.decode([Product].self, from: data) else { 157 // return 158 // } 159 // 160 // if !products.isEmpty && !Task.isCancelled { 161 // await notifyUser(for: products) 162 // } 163 //#endif 164 } 165 } else { 166 // Fallback on earlier versions 167 return WindowGroup { 168 mainView 169 } 170 } 171 172 } 173 } 174 // MARK: - 175 struct PasteType: Hashable { 176 let pastedURL: URL 177 } 178 179 final class ViewState : ObservableObject { 180 static let shared = ViewState() 181 @Published var rootViewId = UUID() 182 let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState") 183 184 public func popToRootView(_ stack: CallStack?) -> Void { 185 logger.info("popToRootView") 186 rootViewId = UUID() // setting a new ID will cause 1st NavStack popToRootView behaviour 187 } 188 189 private init() { } 190 } 191 192 final class ViewState2 : ObservableObject { 193 static let shared = ViewState2() 194 @Published var rootViewId = UUID() 195 let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState2") 196 197 public func popToRootView(_ stack: CallStack?) -> Void { 198 logger.info("popToRootView") 199 rootViewId = UUID() // setting a new ID will cause 2nd NavStack popToRootView behaviour 200 } 201 202 private init() { } 203 }