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