taler-ios

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

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 }