taler-ios

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

WithdrawTOSView.swift (8284B)


      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.getExchangeTos(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, stack: stack.push())
     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                 return false                // nothing to accept
    139             }
    140             if let acceptedEtag = tos.acceptedEtag {
    141                 if acceptedEtag == tos.currentEtag {
    142                     return false            // user already accepted current ToS
    143                 }
    144             }
    145             return true
    146         }
    147 
    148         var body: some View {
    149             let title = String(localized: "Language:", comment: "title of ToS language selection")
    150             let list = List {
    151                 if tos.tosAvailableLanguages.count > 1 {
    152                     Picker(title, selection: $selectedLanguage) {
    153                         ForEach(tos.tosAvailableLanguages, id: \.self) { code in
    154                             let languageName = Locale.current.localizedString(forLanguageCode: code)
    155                             Text(languageName ?? code)
    156                         }
    157                     }
    158                     .talerFont(.title3)
    159                     .pickerStyle(.menu)
    160                     .onAppear() {
    161                         withAnimation { selectedLanguage = language }
    162                     }
    163                     .onChange(of: selectedLanguage) { selected in
    164                         Task {
    165                             await languageAction(selected)
    166                         }
    167                     }
    168                 }
    169                 if tos.tosStatus == .missingTos {
    170                     Section {
    171                         Text("This payment service has not published any terms of service.")
    172                             .talerFont(.body)
    173                             .foregroundColor(Color(UIColor.label))
    174                     }
    175                     .accessibility(sortPriority: 1)     // read this last
    176                 } else if tos.contentType == MARKDOWN {
    177                     Section {
    178                         let content = MarkdownContent(tos.content)
    179                         Markdown(content)
    180                     }
    181                     .accessibility(sortPriority: 1)     // read this last
    182                 } else {
    183                     let components = tos.content.components(separatedBy: "\n\n")
    184                     ForEach (components, id: \.self) { term0 in
    185                         Section {
    186                             Text(cleanupText(term0))
    187                                 .talerFont(.footnote)
    188                                 .foregroundColor(Color(UIColor.label))
    189                         }
    190                         .accessibility(sortPriority: 1)     // read this last
    191                     } // for
    192                 } // plain text
    193             }.listStyle(myListStyle.style).anyView
    194             list.safeAreaInset(edge: .bottom) {
    195                 if showButton {
    196                         Button(String(localized: "Accept Terms of Service", comment: "Button"), action: acceptAction)
    197                             .buttonStyle(TalerButtonStyle(type: .prominent))
    198                             .accessibility(sortPriority: 2)     // read this second
    199                             .padding(.horizontal)
    200                 }
    201             }
    202         }
    203     }
    204 }