taler-ios

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

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 }