AddContactView.swift (11869B)
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 // Called when scanning an add_contact QR code, or tapping an add_contact talerURI 14 // Or the user saves their own alias after registering it 15 struct AddContactView: View { 16 private let symLog = SymLogV(0) 17 let stack: CallStack 18 let url: URL? 19 let contact: ContactEntry? 20 let navTitle: String 21 22 @EnvironmentObject private var model: WalletModel 23 @EnvironmentObject private var controller: Controller 24 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 25 26 @State private var created: Bool = false 27 @State private var working: Bool = false 28 @State private var lookupState: LookupState = .unknown 29 @State private var selectedType: Int = 0 30 @State private var lastType: Int? = nil 31 @State private var unknownType: String = EMPTYSTRING 32 @State private var alias: String = EMPTYSTRING 33 @State private var lastAlias: String = EMPTYSTRING 34 @State private var mailbox: String = EMPTYSTRING 35 @State private var mailAddress: String = EMPTYSTRING 36 @State private var sourceBaseUrl: String = EMPTYSTRING 37 @State private var petname: String = EMPTYSTRING 38 @State private var isSelf: Bool = false 39 @State private var targetUri: String? = nil 40 @State private var aliasTypes: [TaldirAliasType]? = nil 41 42 private enum FocusField { case alias, mbox, address, source, pet } 43 @FocusState private var focus: FocusField? 44 45 init(_ stack: CallStack, url: URL? = nil, contact: ContactEntry? = nil, navTitle: String? = nil) { 46 self.stack = stack 47 self.url = url 48 self.contact = contact 49 self.navTitle = navTitle ?? 50 String(localized: "TitleAddContact", defaultValue: "Add Contact", comment: "View Title, Button") 51 } 52 53 private func selectType(_ type: String) { 54 var typeIsSupported = false 55 if let aliasTypes { 56 for index in aliasTypes.indices { 57 let aliasType = aliasTypes[index] 58 if aliasType.name == type { 59 selectedType = index 60 typeIsSupported = true 61 break 62 } 63 } 64 } 65 if !typeIsSupported { 66 unknownType = type 67 } 68 } 69 70 @MainActor 71 private func viewDidLoad() async { 72 if aliasTypes == nil { 73 if let types = try? await model.getSupportedAliasTypes() { 74 aliasTypes = types 75 } 76 } 77 if let url { 78 // we only arrive here when the command is .addContact, so no need to test this again 79 let parts = url.path.split(separator: "/") 80 if parts.count == 4 { 81 selectType(parts[0].lowercased()) 82 alias = parts[1].lowercased() 83 mailbox = HTTPS + parts[2].lowercased() 84 mailAddress = parts[3].lowercased() 85 if let queryParameters = url.queryParameters { 86 if let baseUrl = queryParameters["sourceBaseUrl"] { 87 sourceBaseUrl = baseUrl 88 } 89 } 90 // TODO: perform lookup 91 lookupState = .confirmed(mailAddress, Duration.microseconds(0)) 92 } 93 } else if let contact { 94 selectType(contact.aliasType.lowercased()) 95 alias = contact.alias.lowercased() 96 mailbox = contact.mailboxBaseUrl.lowercased() 97 mailAddress = contact.mailboxAddress.lowercased() 98 sourceBaseUrl = contact.source.lowercased() 99 petname = contact.petname 100 isSelf = contact.isSelf ?? false 101 // TODO: perform lookup 102 lookupState = .confirmed(mailAddress, Duration.microseconds(0)) 103 } 104 } 105 106 @ViewBuilder func picker(_ aliasTypes: [TaldirAliasType]) -> some View { 107 AliasPicker(stack: stack.push(), 108 title: String(localized: "Alias Type:", comment: "Picker title"), 109 value: $selectedType, 110 types: aliasTypes 111 ) { selected in 112 if selected != lastType { 113 lookupState = .unknown 114 } 115 } 116 } 117 118 @ViewBuilder func lookupButton(_ aliasType: TaldirAliasType) -> some View { 119 LookupButton(stack: stack.push(), 120 alias: alias, 121 aliasType: aliasType, 122 lookupState: $lookupState, 123 focus: { focus = nil } 124 ) { 125 lastType = selectedType 126 lastAlias = alias 127 128 var targetUri: String? = nil 129 var validFor: Duration? = nil 130 switch lookupState { 131 case .registered(let newUri, let newDuration): 132 targetUri = newUri 133 validFor = newDuration 134 case .confirmed(let newUri, let newDuration): 135 targetUri = newUri 136 validFor = newDuration 137 default: break 138 } 139 if let targetUri, 140 let url = URL(string: targetUri) { 141 if let host = url.host, 142 let scheme = url.scheme { 143 mailbox = scheme + "://" + host 144 } 145 var path = url.relativePath 146 if path.hasPrefix("/") { 147 path.removeFirst() 148 } 149 mailAddress = path 150 sourceBaseUrl = TALDIR 151 } 152 } 153 } 154 155 @ViewBuilder func aliasInput() -> some View { 156 AliasInput(stack: stack.push(), 157 text: $alias, 158 lastAlias: lastAlias, 159 changeAction: { lookupState = .unknown }) 160 .focused($focus, equals: .alias) 161 .onSubmit { focus = .mbox } 162 } 163 164 @ViewBuilder func mailboxInput() -> some View { 165 StandardInput(stack: stack.push(), 166 label: String(localized: "Mailbox:", comment: "TextEdit purpose, a11y"), 167 placeHolder: MAILBOX, 168 text: $mailbox) 169 .focused($focus, equals: .mbox) 170 .onSubmit { focus = .address } 171 } 172 173 @ViewBuilder func sourceInput() -> some View { 174 StandardInput(stack: stack.push(), 175 label: String(localized: "Source:", comment: "TextEdit purpose, a11y"), 176 placeHolder: EMPTYSTRING, 177 text: $sourceBaseUrl) 178 .focused($focus, equals: .source) 179 .onSubmit { focus = .pet } 180 } 181 182 @ViewBuilder func petInput() -> some View { 183 StandardInput(stack: stack.push(), 184 label: String(localized: "Nickname:", comment: "TextEdit purpose, a11y"), 185 placeHolder: EMPTYSTRING, 186 text: $petname) 187 .focused($focus, equals: .pet) 188 .onSubmit { focus = nil } 189 } 190 191 @ViewBuilder func addContactButton(_ aliasType: TaldirAliasType) -> some View { 192 // let title = String(localized: "Confirm", comment: "button") 193 FeedbackButton(navTitle, disabled: working, haptic: false) { 194 working = true 195 focus = nil // dismiss keyboard 196 let newContact = ContactEntry(alias: alias, 197 aliasType: aliasType.name, 198 mailboxBaseUrl: mailbox, 199 mailboxAddress: mailAddress.uppercased(), // TODO: remove this after fix 200 source: sourceBaseUrl, 201 petname: petname, 202 isSelf: isSelf) 203 Task { 204 do { 205 try await model.addContact(newContact) 206 } catch { // it didn't succeed 207 // TODO: show error 208 print("addContact failed") 209 lookupState = .error 210 } 211 self.working = false 212 dismissTop(stack.push()) 213 } 214 } 215 } 216 217 var body: some View { 218 #if PRINT_CHANGES 219 let _ = Self._printChanges() 220 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 221 #endif 222 if let aliasTypes { 223 let type = aliasTypes[selectedType] 224 List { 225 Group { 226 VStack(alignment: .leading, spacing: 6) { 227 picker(aliasTypes) 228 aliasInput() 229 } 230 switch lookupState { 231 case .unknown: 232 if !alias.isEmpty { 233 Text("Check whether this alias is registered at the Taler Directory:") 234 lookupButton(type) 235 .padding(.horizontal) 236 } else { 237 #if DEBUG 238 HStack { 239 Button("t11") { 240 petname = "t11" 241 alias = "t11@fesh.com" 242 }.padding(.horizontal) 243 Spacer() 244 Button("t15") { 245 petname = "t15" 246 alias = "t15@fesh.com" 247 }.padding(.horizontal) 248 }.buttonStyle(.bordered) 249 #endif 250 } 251 case .unregistered: 252 Text("This alias is not registered at the Taler Directory.") 253 // registerButton(type) 254 .padding() 255 case .confirmed(let targetUri, let validFor): 256 if let configuration = controller.mailboxConfiguration, 257 targetUri.contains(configuration.hAddress) { 258 Text("This alias, valid for \(validFor.toDays()) days, is already registered to the mailbox on this phone.") 259 } else { 260 Text("This alias is registered to:") 261 } 262 case .registered(let targetUri, let validFor): 263 Text("This alias is registered, but not valid yet.") 264 case .error: 265 Text("Cannot check this alias...") 266 } 267 268 VStack(alignment: .leading, spacing: 6) { 269 mailboxInput() 270 Text("leave blank to use default mailbox") 271 .talerFont(.subheadline) 272 } 273 274 MailAddressInput(stack: stack.push(), 275 text: $mailAddress, 276 isFocused: focus == .address) 277 { focus = .source } 278 .focused($focus, equals: .address) 279 280 sourceInput() 281 282 petInput() 283 284 SettingsToggle(name: String("Belongs to me"), value: $isSelf, 285 id1: "isSelf", 286 description: String("May be on another wallet")) 287 288 addContactButton(type) 289 } 290 .talerFont(.title3) 291 .listRowSeparator(.hidden) 292 } 293 .listStyle(myListStyle.style).anyView 294 .navigationTitle(navTitle) 295 } else { 296 LoadingView(stack: stack.push(), scopeInfo: nil, message: "Loading supported alias types") 297 .task { await viewDidLoad() } 298 } 299 } 300 }