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