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 }