taler-ios

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

WithdrawTOSView.swift (8029B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-26 Taler Systems S.A.
      3  * See LICENSE.md
      4  */
      5 /**
      6  * @author Marc Stibane
      7  */
      8 import SwiftUI
      9 import SymLog
     10 import MarkdownUI
     11 
     12 struct WithdrawTOSView: View {
     13     private let symLog = SymLogV(0)
     14     let stack: CallStack
     15     let exchangeBaseUrl: String?
     16     let viewID: Int         // either VIEW_WITHDRAW_TOS or SHEET_WITHDRAW_TOS
     17     let acceptAction: (() async -> Void)?
     18 
     19     @Environment(\.dismiss) var dismiss     // pop back once
     20     @EnvironmentObject private var model: WalletModel
     21     @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
     22 
     23     @State var exchangeTOS: ExchangeTermsOfService?
     24 
     25     let navTitle = String(localized: "Terms of Service")
     26 
     27     @MainActor
     28     func loadToS(_ language: String) async {
     29         if let exchangeBaseUrl {
     30             let acceptedFormat: [String] = [MARKDOWN, PLAINTEXT]      // MARKDOWN, HTML, PLAINTEXT
     31             if let someTOS = try? await model.loadExchangeTermsOfService(exchangeBaseUrl,
     32                                                          acceptedFormat: acceptedFormat,
     33                                                          acceptLanguage: language) {
     34                 exchangeTOS = someTOS
     35             }
     36         } else {
     37             // TODO: Yikes! No baseURL
     38         }
     39     }
     40 
     41     @MainActor
     42     func viewDidLoad() async {
     43         if let exchangeTOS, let exchangeBaseUrl {
     44             _ = try? await model.setExchangeTOSAccepted(exchangeBaseUrl)
     45             if acceptAction != nil {
     46                 await acceptAction!()
     47             } else { // just go back - caller will reload
     48                 dismiss()
     49             }
     50         } else {
     51             // TODO: error
     52         }
     53     }
     54 
     55     var body: some View {
     56         let languageCode = Locale.preferredLanguageCode
     57 //        let languageName = Locale.current.localizedString(forLanguageCode: languageCode)
     58         if let exchangeTOS, let exchangeBaseUrl {
     59             Content(symLog: symLog,
     60                        tos: exchangeTOS,
     61                myListStyle: $myListStyle,
     62                   language: languageCode,
     63             languageAction: loadToS) {
     64                 Task { await viewDidLoad() }
     65             }
     66             .navigationTitle(navTitle)
     67             .refreshable {
     68                 try? await model.updateExchange(exchangeBaseUrl: exchangeBaseUrl, force: true)
     69                 await loadToS(languageCode)
     70             }
     71             .onAppear() {
     72                 if viewID > SHEET_WITHDRAWAL {
     73                     DebugViewC.shared.setSheetID(SHEET_WITHDRAW_TOS)
     74                 } else {
     75                     DebugViewC.shared.setViewID(VIEW_WITHDRAW_TOS, stack: stack.push())
     76                 }
     77             }
     78         } else {
     79             let fallback = String(localized: "No payment service", comment: "loading")
     80             LoadingView(stack: stack.push(), scopeInfo: nil,
     81                         message: exchangeBaseUrl?.trimURL ?? fallback)
     82                 .task {
     83                     await loadToS(languageCode)
     84                 }
     85         }
     86     }
     87 }
     88 // MARK: -
     89 extension WithdrawTOSView {
     90 
     91     static func cleanupText(_ term0: String) -> String {
     92         let term1 = term0.replacingOccurrences(of: "\n     ", with: " ")    // newline + 5 blanks
     93         let term2 = term1.replacingOccurrences(of: "\n    ", with: " ")     // newline + 4 blanks
     94         let term3 = term2.replacingOccurrences(of: "\n   ", with: " ")      // newline + 3 blanks
     95         let term4 = term3.replacingOccurrences(of: "\n  ", with: " ")       // newline + 2 blanks
     96         let term5 = term4.replacingOccurrences(of: "\n ", with: " ")        // newline + 1 blank
     97         let term6 = term5.replacingOccurrences(of: "\n", with: " ")         // remove all other linebreaks
     98         let term7 = term6.replacingOccurrences(of: " ====", with: "\n====") // add them back for underscoring
     99         let term8 = term7.replacingOccurrences(of: " ----", with: "\n----") // special for "Highlights:"
    100         let term9 = term8.replacingOccurrences(of: " ****", with: "\n****") // special for "Terms of Service:"
    101         return term9
    102     }
    103 
    104     struct plaintextToSV: View {
    105         let plaintext: String
    106 
    107         var body: some View {
    108             let components = plaintext.components(separatedBy: "\n\n")
    109             ForEach (components, id: \.self) { term0 in
    110                 Section {
    111                     Text(cleanupText(term0))
    112                         .talerFont(.footnote)
    113                         .foregroundColor(Color(UIColor.label))
    114                 }
    115             } // for
    116         }
    117     }
    118 
    119     struct Content: View {
    120         let symLog: SymLogV
    121         var tos: ExchangeTermsOfService
    122         @Binding var myListStyle: MyListStyle
    123         let language: String
    124         var languageAction: (String) async -> ()
    125         var acceptAction: () -> ()
    126 
    127         @Environment(\.colorScheme) private var colorScheme
    128         @Environment(\.colorSchemeContrast) private var colorSchemeContrast
    129 #if DEBUG
    130         @AppStorage("developerMode") var developerMode: Bool = true
    131 #else
    132         @AppStorage("developerMode") var developerMode: Bool = false
    133 #endif
    134         @State private var selectedLanguage = Locale.preferredLanguageCode
    135 
    136         var showButton: Bool {
    137             if tos.tosStatus == .missingTos {
    138                 // TODO: wallet-core should accept receiving money when there are no ToS
    139                 return developerMode        // no devMode ==> false (don't show button)
    140             }
    141             if let acceptedEtag = tos.acceptedEtag {
    142                 if acceptedEtag == tos.currentEtag {
    143                     return false            // user already accepted current ToS
    144                 }
    145             }
    146             return true
    147         }
    148 
    149         var body: some View {
    150             let title = String(localized: "Language:", comment: "title of ToS language selection")
    151             List {
    152                 if tos.tosAvailableLanguages.count > 1 {
    153                     Picker(title, selection: $selectedLanguage) {
    154                         ForEach(tos.tosAvailableLanguages, id: \.self) { code in
    155                             let languageName = Locale.current.localizedString(forLanguageCode: code)
    156                             Text(languageName ?? code)
    157                         }
    158                     }
    159                     .talerFont(.title3)
    160                     .pickerStyle(.menu)
    161                     .onAppear() {
    162                         withAnimation { selectedLanguage = language }
    163                     }
    164                     .onChange(of: selectedLanguage) { selected in
    165                         Task {
    166                             await languageAction(selected)
    167                         }
    168                     }
    169                 }
    170                 if tos.contentType == MARKDOWN {
    171                     Section {
    172                         let content = MarkdownContent(tos.content)
    173                         Markdown(content)
    174                     }
    175                 } else {
    176                     let components = tos.content.components(separatedBy: "\n\n")
    177                     ForEach (components, id: \.self) { term0 in
    178                         Section {
    179                             Text(cleanupText(term0))
    180                                 .talerFont(.footnote)
    181                                 .foregroundColor(Color(UIColor.label))
    182                         }
    183                     } // for
    184                 } // plain text
    185                 if showButton {
    186                     Section {
    187                         Button(String(localized: "Accept Terms of Service", comment: "Button"), action: acceptAction)
    188                             .buttonStyle(TalerButtonStyle(type: .prominent))
    189                             .padding(.horizontal)
    190                     } header: {
    191                         Text("Accept", comment: "Heading for Accept ToS button")
    192                             .talerFont(.title3)
    193                             .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast))
    194                     }
    195                 }
    196             }.listStyle(myListStyle.style).anyView
    197         }
    198     }
    199 }