taler-ios

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

TalerPOSApp.swift (5736B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2026 Bohdan, Volodymyr Potuzhnyi
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 import SwiftUI
     18 
     19 
     20 @main
     21 struct TalerPOSApp: App {
     22     @StateObject private var configManager: ConfigManager
     23     @StateObject private var orderManager: OrderManager
     24     @StateObject private var paymentManager: PaymentManager
     25     @StateObject private var historyManager: HistoryManager
     26     @StateObject private var refundManager: RefundManager
     27     @ObservedObject private var languageManager = LanguageManager.shared
     28 
     29     init() {
     30         let merchantApi = MerchantApi()
     31         let config = ConfigManager(api: merchantApi)
     32         let order = OrderManager()
     33         let payment = PaymentManager(api: merchantApi, configManager: config)
     34         let history = HistoryManager(api: merchantApi, configManager: config)
     35         let refund = RefundManager(api: merchantApi, configManager: config)
     36 
     37         config.addConfigurationReceiver(order)
     38 
     39         _configManager = StateObject(wrappedValue: config)
     40         _orderManager = StateObject(wrappedValue: order)
     41         _paymentManager = StateObject(wrappedValue: payment)
     42         _historyManager = StateObject(wrappedValue: history)
     43         _refundManager = StateObject(wrappedValue: refund)
     44 
     45         merchantApi.onUnauthorized = { [weak config] in
     46             Task { @MainActor in
     47                 guard config?.merchantConfig != nil else { return }
     48                 config?.forgetPassword()
     49                 config?.sessionExpired = true
     50             }
     51         }
     52     }
     53 
     54     private func handleSetupUrl(_ url: URL) {
     55         guard url.scheme == "taler-pos", let host = url.host, !host.isEmpty else {
     56             configManager.configResult = .error("Invalid setup URL: missing scheme or host.")
     57             return
     58         }
     59 
     60         let pathSegments = url.pathComponents.filter { $0 != "/" }
     61         let pathStyleInstance: String? = {
     62             guard pathSegments.count >= 2,
     63                   pathSegments[0].caseInsensitiveCompare("instances") == .orderedSame,
     64                   !pathSegments[1].isEmpty else { return nil }
     65             return pathSegments[1]
     66         }()
     67 
     68         let fragment = url.fragment?.trimmingCharacters(in: CharacterSet(charactersIn: "/").union(.whitespaces)) ?? ""
     69         let params: [String: String]? = fragment.contains("=")
     70             ? Dictionary(uniqueKeysWithValues: fragment.split(separator: "&").compactMap { part in
     71                 let kv = part.split(separator: "=", maxSplits: 1)
     72                 guard kv.count == 2 else { return nil }
     73                 return (String(kv[0]), String(kv[1]).removingPercentEncoding ?? String(kv[1]))
     74             })
     75             : nil
     76 
     77         let instance = pathStyleInstance ?? params?["username"]
     78         let token: String? = {
     79             if pathStyleInstance != nil {
     80                 let decoded = fragment.removingPercentEncoding ?? fragment
     81                 return decoded.isEmpty ? nil : decoded
     82             }
     83             return params?["password"]
     84         }()
     85 
     86         guard let instance, !instance.isEmpty, let token, !token.isEmpty else {
     87             configManager.configResult = .error("Invalid setup URL: missing instance or credentials.")
     88             return
     89         }
     90 
     91         var components = URLComponents()
     92         components.scheme = "https"
     93         components.host = host
     94         components.path = "/instances/\(instance)"
     95         guard let merchantUrl = components.url?.absoluteString else {
     96             configManager.configResult = .error("Invalid setup URL: could not build merchant URL.")
     97             return
     98         }
     99 
    100         configManager.savePassword = true
    101         configManager.fetchConfig(url: merchantUrl, token: token, save: true)
    102     }
    103 
    104     var body: some Scene {
    105         WindowGroup {
    106             ContentView()
    107                 .environment(\.locale, languageManager.locale)
    108                 .id(languageManager.selectedLanguage)
    109                 .environmentObject(configManager)
    110                 .environmentObject(orderManager)
    111                 .environmentObject(paymentManager)
    112                 .environmentObject(historyManager)
    113                 .environmentObject(refundManager)
    114                 .onAppear {
    115                     if let scenario = ScreenshotController.activeScenario {
    116                         ScreenshotController.applyLocaleIfNeeded()
    117                         ScreenshotController.applyFixture(
    118                             configManager: configManager,
    119                             orderManager: orderManager,
    120                             paymentManager: paymentManager,
    121                             historyManager: historyManager,
    122                             refundManager: refundManager,
    123                             scenario: scenario
    124                         )
    125                     } else {
    126                         configManager.restoreCachedRuntimeConfig()
    127                         if configManager.isConfigured {
    128                             configManager.refreshConfigInBackground()
    129                         }
    130                     }
    131                 }
    132                 .onOpenURL { url in
    133                     handleSetupUrl(url)
    134                 }
    135         }
    136     }
    137 }