AliasView.swift (10965B)
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 taler_swift 10 import SymLog 11 12 // MARK: - 13 /// This view shows the Alias registration t15@fesh.com 14 struct AliasView: View { 15 private let symLog = SymLogV(0) 16 let stack: CallStack 17 let navTitle: String 18 19 @EnvironmentObject private var model: WalletModel 20 @EnvironmentObject private var controller: Controller 21 @AppStorage("minimalistic") var minimalistic: Bool = false 22 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 23 24 @State private var working: Bool = false 25 @State private var lookupState: LookupState = .unknown 26 @State private var selectedType: Int = 0 27 @State private var lastType: Int? = nil 28 @State private var alias: String = EMPTYSTRING 29 @State private var lastAlias: String = EMPTYSTRING 30 @State private var challenge: String = EMPTYSTRING 31 @State private var aliasTypes: [TaldirAliasType]? = nil 32 @State private var validFor: Duration? = nil 33 @State private var addContact: Bool = false 34 // @ScaledMetric var frameHeight: CGFloat = 40 // relative to fontSize 35 36 private enum FocusField { case alias, code } 37 @FocusState private var focus: FocusField? 38 39 @MainActor 40 private func viewDidLoad() async { 41 if aliasTypes == nil { 42 if let types = try? await model.getSupportedAliasTypes() { 43 aliasTypes = types 44 } } } 45 46 @ViewBuilder func registerButton(_ aliasType: TaldirAliasType, 47 configuration: MailboxConfiguration) -> some View { 48 let hasTarget = lookupState.hasTarget 49 let title = hasTarget ? String(localized: "Unregister Alias", comment: "button") 50 : String(localized: "Register Alias", comment: "button") 51 FeedbackButton(title, disabled: working, haptic: false) { 52 working = true 53 focus = nil // dismiss keyboard 54 let seconds: UInt64 = hasTarget ? 0 : 60*60*24*365 // 1 year 55 let microSeconds = seconds * 1_000_000 56 let targetUri = hasTarget ? EMPTYSTRING 57 : configuration.hAddress 58 let register = TaldirRegistration(alias: alias, 59 aliasType: aliasType.name, 60 mailboxAddress: targetUri, 61 duration: Duration.microseconds(microSeconds)) 62 Task { 63 do { 64 if let result = try await model.registerAlias(register) { 65 validFor = result 66 } else { 67 print("register succeeded, but did not return a validity period") 68 } 69 lookupState = .registered(targetUri, Duration.microseconds(0)) 70 } catch { // it didn't succeed 71 // TODO: show error 72 print("register failed") 73 lookupState = .error 74 } 75 self.working = false 76 } 77 } 78 } 79 80 @ViewBuilder func confirmButton(_ aliasType: TaldirAliasType, _ targetUri: String) -> some View { 81 let title = String(localized: "Confirm", comment: "button") 82 FeedbackButton(title, disabled: working, haptic: false) { 83 working = true 84 focus = nil // dismiss keyboard 85 let register = TaldirRegistration(alias: alias, 86 aliasType: aliasType.name, 87 mailboxAddress: targetUri, 88 challenge: challenge.uppercased()) 89 Task { 90 do { 91 try await model.completeRegisterAlias(register) 92 challenge = EMPTYSTRING 93 lookupState = targetUri.isEmpty ? .unregistered 94 : .confirmed(targetUri, Duration.microseconds(0)) 95 } catch { // it didn't succeed 96 // TODO: show error 97 print("confirm failed") 98 lookupState = .error 99 } 100 self.working = false 101 } 102 } 103 } 104 105 @ViewBuilder func addContactButton(_ aliasType: TaldirAliasType, address: String) -> some View { 106 let buttonTitle = String(localized: "Add to Contacts", comment: "button") 107 let contact = ContactEntry(alias: alias, 108 aliasType: aliasType.name, 109 mailboxAddress: address, 110 isSelf: true) 111 let destination = AddContactView(stack.push(), contact: contact) 112 let actions = Group { 113 NavLink($addContact) { destination } 114 } 115 FeedbackButton(buttonTitle, disabled: working, haptic: false) { 116 focus = nil // dismiss keyboard 117 addContact = true 118 } 119 .background(actions) 120 } 121 122 @ViewBuilder func picker(_ aliasTypes: [TaldirAliasType]) -> some View { 123 AliasPicker(stack: stack.push(), 124 title: String(localized: "Alias Type:", comment: "Picker title"), 125 value: $selectedType, 126 types: aliasTypes 127 ) { selected in 128 if selected != lastType { 129 lookupState = .unknown 130 } 131 } 132 } 133 134 @ViewBuilder func codeInput() -> some View { 135 StandardInput(stack: stack.push(), 136 label: String(localized: "Code:", comment: "TextEdit purpose, a11y"), 137 placeHolder: EMPTYSTRING, 138 text: $challenge) 139 .focused($focus, equals: .code) 140 .onSubmit { 141 focus = nil 142 print("TODO: start confirmation") 143 } 144 } 145 146 @ViewBuilder func lookupButton(_ aliasType: TaldirAliasType) -> some View { 147 LookupButton(stack: stack.push(), 148 alias: alias, 149 aliasType: aliasType, 150 lookupState: $lookupState, 151 focus: { focus = nil } 152 ) { 153 lastType = selectedType 154 lastAlias = alias 155 } 156 } 157 158 @ViewBuilder func aliasInput() -> some View { 159 AliasInput(stack: stack.push(), 160 text: $alias, 161 lastAlias: lastAlias, 162 changeAction: { lookupState = .unknown }) 163 .focused($focus, equals: .alias) 164 .onSubmit { focus = nil } 165 } 166 167 var body: some View { 168 if let aliasTypes, 169 let configuration = controller.mailboxConfiguration { 170 let type = aliasTypes[selectedType] 171 let contacts = controller.contacts 172 List { 173 Group { 174 VStack(alignment: .leading, spacing: 6) { 175 picker(aliasTypes) 176 aliasInput() 177 } 178 switch lookupState { 179 case .unknown: 180 if !alias.isEmpty { 181 Text("Check whether this alias is already registered:") 182 lookupButton(type) 183 .padding(.horizontal) 184 } 185 case .unregistered: 186 Text("This alias is not registered yet. Do you want to register it now?") 187 registerButton(type, configuration: configuration) 188 .padding() 189 case .confirmed(let targetUri, let validFor): 190 if targetUri.contains(configuration.hAddress) { 191 Text("This alias, valid for \(validFor.toDays()) days, is already registered to the mailbox on this phone.") 192 if contacts.firstIndex(where: {$0.alias == alias}) == nil { 193 Text("But it is not in your contacts list. Add it now?") 194 addContactButton(type, address: configuration.hAddress) 195 } 196 197 Text("If you no longer want to use it, you can unregister it now.") 198 registerButton(type, configuration: configuration) 199 .padding(.horizontal) 200 } else { 201 Text("This alias is registered to another mailbox:") 202 Text(targetUri.description) 203 } 204 case .registered(let targetUri, let validFor): 205 // TODO: let the user register again to get another mail/SMS 206 switch type.name { 207 case "email": 208 Text("Check your email inbox for a confirmation email.") 209 case "phone": 210 Text("Check your messages for a confirmation SMS.") 211 default: 212 Text("Check \(type.localized) for a confirmation code.") 213 } 214 codeInput() 215 if challenge.isEmpty { 216 Text("You can also confirm the registration directly on the website, then come back here and check whether it was successful:") 217 lookupButton(type) 218 .padding(.horizontal) 219 } else { 220 confirmButton(type, targetUri) 221 .padding(.horizontal) 222 } 223 case .error: 224 Text("Cannot check this alias...") 225 // Text(error.0) 226 // .foregroundColor(.red) 227 // Text(error.1) 228 // .foregroundColor(.red) 229 } 230 } 231 .talerFont(.title3) 232 .listRowSeparator(.hidden) 233 234 // TODO: once we have services that charge money for TalDir, we should show the remaining time 235 // if let validFor { 236 // Text("Valid for \(validFor.toDays()) days") 237 // } 238 } 239 .listStyle(myListStyle.style).anyView 240 .navigationTitle(navTitle) 241 .onAppear() { 242 DebugViewC.shared.setViewID(VIEW_ALIAS, stack: stack.push()) 243 } 244 } else { 245 LoadingView(stack: stack.push(), scopeInfo: nil, message: "Loading supported alias types") 246 .task { await viewDidLoad() } 247 } 248 } // body 249 }