TalerWallet1App.swift (8093B)
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 > 1 ? 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 in the future 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, setTesting: testing, delay: delay) // will (and should) crash on failure 130 } 131 if #available(iOS 16.4, *) { 132 return WindowGroup { 133 mainView 134 } 135 .onChange(of: phase) { newPhase in 136 switch newPhase { 137 case .active: 138 logger.log("❗️.onChange() ==> Active") 139 if pasteAutomatically { 140 inspectPasteboard(playSound: false) 141 } 142 case .background: 143 logger.log("❗️.onChange() ==> Background)") 144 #if TALER_NIGHTLY 145 // scheduleAppRefresh() 146 #endif 147 default: break 148 } 149 } 150 #if TALER_NIGHTLY 151 .backgroundTask(.appRefresh("net.taler.refresh")) { 152 // symLog.log("backgroundTask running") 153 //#if 0 154 // let request = URLRequest(url: URL(string: "your_backend")!) 155 // guard let data = try? await URLSession.shared.data(for: request).0 else { 156 // return 157 // } 158 // 159 // let decoder = JSONDecoder() 160 // guard let products = try? decoder.decode([Product].self, from: data) else { 161 // return 162 // } 163 // 164 // if !products.isEmpty && !Task.isCancelled { 165 // await notifyUser(for: products) 166 // } 167 //#endif 168 } 169 #endif 170 } else { 171 // Fallback on earlier versions 172 return WindowGroup { 173 mainView 174 } 175 } 176 177 } 178 } 179 // MARK: - 180 struct PasteType: Hashable { 181 let pastedURL: URL 182 } 183 184 final class ViewState : ObservableObject { 185 static let shared = ViewState() 186 @Published var rootViewId = UUID() 187 let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState") 188 189 public func popToRootView(_ stack: CallStack?) -> Void { 190 logger.info("popToRootView") 191 rootViewId = UUID() // setting a new ID will cause 1st NavStack popToRootView behaviour 192 } 193 194 private init() { } 195 } 196 197 final class ViewState2 : ObservableObject { 198 static let shared = ViewState2() 199 @Published var rootViewId = UUID() 200 let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState2") 201 202 public func popToRootView(_ stack: CallStack?) -> Void { 203 logger.info("popToRootView") 204 rootViewId = UUID() // setting a new ID will cause 2nd NavStack popToRootView behaviour 205 } 206 207 private init() { } 208 }