TalerWallet1App.swift (7517B)
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(-1, Bundle.main.bundleName) 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 .environment(\.fromSheet, false) 118 .addKeyboardVisibilityToEnvironment() 119 /// external events are taler:// (or in the future payto://) URLs passed to this app 120 /// we handle them in .onOpenURL in MainView.swift 121 .handlesExternalEvents(preferring: ["*"], allowing: ["*"]) 122 .task { 123 #if DEBUG 124 let testing = true 125 let delay: TimeInterval = 0.001 126 #else 127 let testing = false 128 let delay: TimeInterval = 2.25 129 #endif 130 try? await controller.initWalletCore(setTesting: testing, delay: delay) 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 } 155 #endif 156 } else { 157 // Fallback on earlier versions 158 return WindowGroup { 159 mainView 160 } 161 } 162 } 163 } 164 // MARK: - 165 struct PasteType: Hashable { 166 let pastedURL: URL 167 } 168 169 final class ViewState : ObservableObject { 170 static let shared = ViewState() 171 @Published var rootViewId = UUID() 172 let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState") 173 174 public func popToRootView(_ stack: CallStack?) -> Void { 175 logger.info("popToRootView") 176 rootViewId = UUID() // setting a new ID will cause 1st NavStack popToRootView behaviour 177 } 178 179 private init() { } 180 } 181 182 final class ViewState2 : ObservableObject { 183 static let shared = ViewState2() 184 @Published var rootViewId = UUID() 185 let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState2") 186 187 public func popToRootView(_ stack: CallStack?) -> Void { 188 logger.info("popToRootView") 189 rootViewId = UUID() // setting a new ID will cause 2nd NavStack popToRootView behaviour 190 } 191 192 private init() { } 193 }