taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }