taler-ios

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

commit a9c21083acfae79406fee3cc7c2dfce46be70e41
parent 23a2d97f4e86aa7ef551c71ce477369466b77af7
Author: Marc Stibane <marc@taler.net>
Date:   Sat, 31 Oct 2026 17:59:17 +0100

AddContactView

Diffstat:
ATalerWallet1/Views/TalDir/AddContactView.swift | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 300 insertions(+), 0 deletions(-)

diff --git a/TalerWallet1/Views/TalDir/AddContactView.swift b/TalerWallet1/Views/TalDir/AddContactView.swift @@ -0,0 +1,300 @@ +/* + * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. + * See LICENSE.md + */ +/** + * @author Marc Stibane + */ +import SwiftUI +import taler_swift +import SymLog + +// MARK: - +// Called when scanning an add_contact QR code, or tapping an add_contact talerURI +// Or the user saves their own alias after registering it +struct AddContactView: View { + private let symLog = SymLogV(0) + let stack: CallStack + let url: URL? + let contact: ContactEntry? + let navTitle: String + + @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + + @State private var created: Bool = false + @State private var working: Bool = false + @State private var lookupState: LookupState = .unknown + @State private var selectedType: Int = 0 + @State private var lastType: Int? = nil + @State private var unknownType: String = EMPTYSTRING + @State private var alias: String = EMPTYSTRING + @State private var lastAlias: String = EMPTYSTRING + @State private var mailbox: String = EMPTYSTRING + @State private var mailAddress: String = EMPTYSTRING + @State private var sourceBaseUrl: String = EMPTYSTRING + @State private var petname: String = EMPTYSTRING + @State private var isSelf: Bool = false + @State private var targetUri: String? = nil + @State private var aliasTypes: [TaldirAliasType]? = nil + + private enum FocusField { case alias, mbox, address, source, pet } + @FocusState private var focus: FocusField? + + init(_ stack: CallStack, url: URL? = nil, contact: ContactEntry? = nil, navTitle: String? = nil) { + self.stack = stack + self.url = url + self.contact = contact + self.navTitle = navTitle ?? + String(localized: "TitleAddContact", defaultValue: "Add Contact", comment: "View Title, Button") + } + + private func selectType(_ type: String) { + var typeIsSupported = false + if let aliasTypes { + for index in aliasTypes.indices { + let aliasType = aliasTypes[index] + if aliasType.name == type { + selectedType = index + typeIsSupported = true + break + } + } + } + if !typeIsSupported { + unknownType = type + } + } + + @MainActor + private func viewDidLoad() async { + if aliasTypes == nil { + if let types = try? await model.getSupportedAliasTypes() { + aliasTypes = types + } + } + if let url { + // we only arrive here when the command is .addContact, so no need to test this again + let parts = url.path.split(separator: "/") + if parts.count == 4 { + selectType(parts[0].lowercased()) + alias = parts[1].lowercased() + mailbox = HTTPS + parts[2].lowercased() + mailAddress = parts[3].lowercased() + if let queryParameters = url.queryParameters { + if let baseUrl = queryParameters["sourceBaseUrl"] { + sourceBaseUrl = baseUrl + } + } + // TODO: perform lookup + lookupState = .confirmed(mailAddress, Duration.microseconds(0)) + } + } else if let contact { + selectType(contact.aliasType.lowercased()) + alias = contact.alias.lowercased() + mailbox = contact.mailboxBaseUrl.lowercased() + mailAddress = contact.mailboxAddress.lowercased() + sourceBaseUrl = contact.source.lowercased() + petname = contact.petname + isSelf = contact.isSelf ?? false + // TODO: perform lookup + lookupState = .confirmed(mailAddress, Duration.microseconds(0)) + } + } + + @ViewBuilder func picker(_ aliasTypes: [TaldirAliasType]) -> some View { + AliasPicker(stack: stack.push(), + title: String(localized: "Alias Type:", comment: "Picker title"), + value: $selectedType, + types: aliasTypes + ) { selected in + if selected != lastType { + lookupState = .unknown + } + } + } + + @ViewBuilder func lookupButton(_ aliasType: TaldirAliasType) -> some View { + LookupButton(stack: stack.push(), + alias: alias, + aliasType: aliasType, + lookupState: $lookupState, + focus: { focus = nil } + ) { + lastType = selectedType + lastAlias = alias + + var targetUri: String? = nil + var validFor: Duration? = nil + switch lookupState { + case .registered(let newUri, let newDuration): + targetUri = newUri + validFor = newDuration + case .confirmed(let newUri, let newDuration): + targetUri = newUri + validFor = newDuration + default: break + } + if let targetUri, + let url = URL(string: targetUri) { + if let host = url.host, + let scheme = url.scheme { + mailbox = scheme + "://" + host + } + var path = url.relativePath + if path.hasPrefix("/") { + path.removeFirst() + } + mailAddress = path + sourceBaseUrl = TALDIR + } + } + } + + @ViewBuilder func aliasInput() -> some View { + AliasInput(stack: stack.push(), + text: $alias, + lastAlias: lastAlias, + changeAction: { lookupState = .unknown }) + .focused($focus, equals: .alias) + .onSubmit { focus = .mbox } + } + + @ViewBuilder func mailboxInput() -> some View { + StandardInput(stack: stack.push(), + label: String(localized: "Mailbox:", comment: "TextEdit purpose, a11y"), + placeHolder: MAILBOX, + text: $mailbox) + .focused($focus, equals: .mbox) + .onSubmit { focus = .address } + } + + @ViewBuilder func sourceInput() -> some View { + StandardInput(stack: stack.push(), + label: String(localized: "Source:", comment: "TextEdit purpose, a11y"), + placeHolder: EMPTYSTRING, + text: $sourceBaseUrl) + .focused($focus, equals: .source) + .onSubmit { focus = .pet } + } + + @ViewBuilder func petInput() -> some View { + StandardInput(stack: stack.push(), + label: String(localized: "Nickname:", comment: "TextEdit purpose, a11y"), + placeHolder: EMPTYSTRING, + text: $petname) + .focused($focus, equals: .pet) + .onSubmit { focus = nil } + } + + @ViewBuilder func addContactButton(_ aliasType: TaldirAliasType) -> some View { +// let title = String(localized: "Confirm", comment: "button") + FeedbackButton(navTitle, disabled: working, haptic: false) { + working = true + focus = nil // dismiss keyboard + let newContact = ContactEntry(alias: alias, + aliasType: aliasType.name, + mailboxBaseUrl: mailbox, + mailboxAddress: mailAddress.uppercased(), // TODO: remove this after fix + source: sourceBaseUrl, + petname: petname, + isSelf: isSelf) + Task { + do { + try await model.addContact(newContact) + } catch { // it didn't succeed + // TODO: show error + print("addContact failed") + lookupState = .error + } + self.working = false + dismissTop(stack.push()) + } + } + } + + var body: some View { +#if PRINT_CHANGES + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif + if let aliasTypes { + let type = aliasTypes[selectedType] + List { + Group { + VStack(alignment: .leading, spacing: 6) { + picker(aliasTypes) + aliasInput() + } + switch lookupState { + case .unknown: + if !alias.isEmpty { + Text("Check whether this alias is registered at the Taler Directory:") + lookupButton(type) + .padding(.horizontal) + } else { +#if DEBUG + HStack { + Button("t11") { + petname = "t11" + alias = "t11@fesh.com" + }.padding(.horizontal) + Spacer() + Button("t15") { + petname = "t15" + alias = "t15@fesh.com" + }.padding(.horizontal) + }.buttonStyle(.bordered) +#endif + } + case .unregistered: + Text("This alias is not registered at the Taler Directory.") +// registerButton(type) + .padding() + case .confirmed(let targetUri, let validFor): + if let configuration = controller.mailboxConfiguration, + targetUri.contains(configuration.hAddress) { + Text("This alias, valid for \(validFor.toDays()) days, is already registered to the mailbox on this phone.") + } else { + Text("This alias is registered to:") + } + case .registered(let targetUri, let validFor): + Text("This alias is registered, but not valid yet.") + case .error: + Text("Cannot check this alias...") + } + + VStack(alignment: .leading, spacing: 6) { + mailboxInput() + Text("leave blank to use default mailbox") + .talerFont(.subheadline) + } + + MailAddressInput(stack: stack.push(), + text: $mailAddress, + isFocused: focus == .address) + { focus = .source } + .focused($focus, equals: .address) + + sourceInput() + + petInput() + + SettingsToggle(name: String("Belongs to me"), value: $isSelf, + id1: "isSelf", + description: String("May be on another wallet")) + + addContactButton(type) + } + .talerFont(.title3) + .listRowSeparator(.hidden) + } + .listStyle(myListStyle.style).anyView + .navigationTitle(navTitle) + } else { + LoadingView(stack: stack.push(), scopeInfo: nil, message: "Loading supported alias types") + .task { await viewDidLoad() } + } + } +}