taler-ios

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

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 }