taler-ios

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

TalerWallet1App.swift (8145B)


      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 < 0 ? 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 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, stage: developerMode,
    130                                               setTesting: testing, delay: delay)     // will (and should) crash on failure
    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 //#if 0
    155 //                let request = URLRequest(url: URL(string: "your_backend")!)
    156 //                guard let data = try? await URLSession.shared.data(for: request).0 else {
    157 //                    return
    158 //                }
    159 //                
    160 //                let decoder = JSONDecoder()
    161 //                guard let products = try? decoder.decode([Product].self, from: data) else {
    162 //                    return
    163 //                }
    164 //                
    165 //                if !products.isEmpty && !Task.isCancelled {
    166 //                    await notifyUser(for: products)
    167 //                }
    168 //#endif
    169             }
    170 #endif
    171         } else {
    172             // Fallback on earlier versions
    173             return WindowGroup {
    174                 mainView
    175             }
    176         }
    177 
    178     }
    179 }
    180 // MARK: -
    181 struct PasteType: Hashable {
    182     let pastedURL: URL
    183 }
    184 
    185 final class ViewState : ObservableObject {
    186     static let shared = ViewState()
    187     @Published var rootViewId = UUID()
    188     let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState")
    189 
    190     public func popToRootView(_ stack: CallStack?) -> Void {
    191         logger.info("popToRootView")
    192         rootViewId = UUID() // setting a new ID will cause 1st NavStack popToRootView behaviour
    193     }
    194 
    195     private init() { }
    196 }
    197 
    198 final class ViewState2 : ObservableObject {
    199     static let shared = ViewState2()
    200     @Published var rootViewId = UUID()
    201     let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState2")
    202 
    203     public func popToRootView(_ stack: CallStack?) -> Void {
    204         logger.info("popToRootView")
    205         rootViewId = UUID() // setting a new ID will cause 2nd NavStack popToRootView behaviour
    206     }
    207 
    208     private init() { }
    209 }