taler-ios

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

TalerWallet1App.swift (8070B)


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