taler-ios

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

TalerWallet1App.swift (8093B)


      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 > 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                 .addKeyboardVisibilityToEnvironment()
    118                     /// external events are taler:// (or in the future 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, setTesting: testing, delay: delay)     // will (and should) crash on failure
    130                 }
    131         if #available(iOS 16.4, *) {
    132             return WindowGroup {
    133                 mainView
    134             }
    135             .onChange(of: phase) { newPhase in
    136                 switch newPhase {
    137                     case .active:
    138                         logger.log("❗️.onChange() ==> Active")
    139                         if pasteAutomatically {
    140                             inspectPasteboard(playSound: false)
    141                         }
    142                     case .background:
    143                         logger.log("❗️.onChange() ==> Background)")
    144 #if TALER_NIGHTLY
    145 //                      scheduleAppRefresh()
    146 #endif
    147                     default: break
    148                 }
    149             }
    150 #if TALER_NIGHTLY
    151             .backgroundTask(.appRefresh("net.taler.refresh")) {
    152 //                symLog.log("backgroundTask running")
    153 //#if 0
    154 //                let request = URLRequest(url: URL(string: "your_backend")!)
    155 //                guard let data = try? await URLSession.shared.data(for: request).0 else {
    156 //                    return
    157 //                }
    158 //                
    159 //                let decoder = JSONDecoder()
    160 //                guard let products = try? decoder.decode([Product].self, from: data) else {
    161 //                    return
    162 //                }
    163 //                
    164 //                if !products.isEmpty && !Task.isCancelled {
    165 //                    await notifyUser(for: products)
    166 //                }
    167 //#endif
    168             }
    169 #endif
    170         } else {
    171             // Fallback on earlier versions
    172             return WindowGroup {
    173                 mainView
    174             }
    175         }
    176 
    177     }
    178 }
    179 // MARK: -
    180 struct PasteType: Hashable {
    181     let pastedURL: URL
    182 }
    183 
    184 final class ViewState : ObservableObject {
    185     static let shared = ViewState()
    186     @Published var rootViewId = UUID()
    187     let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState")
    188 
    189     public func popToRootView(_ stack: CallStack?) -> Void {
    190         logger.info("popToRootView")
    191         rootViewId = UUID() // setting a new ID will cause 1st NavStack popToRootView behaviour
    192     }
    193 
    194     private init() { }
    195 }
    196 
    197 final class ViewState2 : ObservableObject {
    198     static let shared = ViewState2()
    199     @Published var rootViewId = UUID()
    200     let logger = Logger(subsystem: "net.taler.gnu", category: "ViewState2")
    201 
    202     public func popToRootView(_ stack: CallStack?) -> Void {
    203         logger.info("popToRootView")
    204         rootViewId = UUID() // setting a new ID will cause 2nd NavStack popToRootView behaviour
    205     }
    206 
    207     private init() { }
    208 }