taler-ios

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

commit 086e118b04617cab829f2bf9d3c18bdcb6e19951
parent 0566952e5201aa0020cb8850ca58783ef03c3940
Author: Marc Stibane <marc@taler.net>
Date:   Sat, 28 Sep 2024 22:35:01 +0200

w.i.p

Diffstat:
MTalerWallet1/Views/Balances/BalancesListView.swift | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MTalerWallet1/Views/Balances/BalancesSectionView.swift | 55++++++++++++++++++++++++++++---------------------------
MTalerWallet1/Views/Banking/DepositIbanV.swift | 8++++++--
MTalerWallet1/Views/Banking/ManualWithdraw.swift | 7++++++-
MTalerWallet1/Views/HelperViews/View+fixedInnerHeight.swift | 6++++--
MTalerWallet1/Views/Main/MainView.swift | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
MTalerWallet1/Views/Peer2peer/RequestPayment.swift | 5++++-
MTalerWallet1/Views/Peer2peer/SendAmount.swift | 16++++++++++------
MTalerWallet1/Views/Sheets/WithdrawExchangeV.swift | 2+-
MTalerWallet1/Views/Transactions/ManualDetailsWireV.swift | 8+++++++-
10 files changed, 306 insertions(+), 85 deletions(-)

diff --git a/TalerWallet1/Views/Balances/BalancesListView.swift b/TalerWallet1/Views/Balances/BalancesListView.swift @@ -1,7 +1,10 @@ /* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-24 Taler Systems S.A. * See LICENSE.md */ +/** + * @author Marc Stibane + */ import SwiftUI import taler_swift import SymLog @@ -21,8 +24,10 @@ struct BalancesListView: View { @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic @State private var lastReloadedBalances = 0 + @State private var actionSelected: Int? = nil @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used @State private var summary: String = "" + @State private var myExchange: Exchange? = nil /// runs on MainActor if called in some Task {} @discardableResult @@ -40,11 +45,57 @@ struct BalancesListView: View { return nil } + private static func className() -> String {"\(self)"} + var body: some View { #if PRINT_CHANGES let _ = Self._printChanges() let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif + let scope = ScopeInfo(type: ScopeInfo.ScopeInfoType.exchange, + noFees: false, + url: DEMOEXCHANGE, + currency: DEMOCURRENCY) + let sendDest = LazyView { + SendAmount(stack: stack.push("\(Self.className())()"), +// currencyInfo: $currencyInfo, +// amountAvailable: amountAvailable, + available: nil, + amountToTransfer: $amountToTransfer, // with correct currency + summary: $summary, + scopeInfo: scope, + cameraAction: cameraAction) + } + + let requestDest = LazyView { + RequestPayment(stack: stack.push("\(Self.className())()"), +// currencyInfo: $currencyInfo, + amountToTransfer: $amountToTransfer, // with correct currency + summary: $summary, + scopeInfo: scope, + cameraAction: cameraAction) + } + + let depositDest = LazyView { + DepositIbanV(stack: stack.push(), +// currencyInfo: $currencyInfo, + feeLabel: nil, + feeIsNotZero: nil, +// amountAvailable: amountAvailable, +// depositIBAN: $depositIBAN, +// accountHolder: $accountHolder, + amountToTransfer: $amountToTransfer) + } + + let manualWithdrawDest = LazyView { + ManualWithdraw(stack: stack.push(), +// currencyInfo: $currencyInfo, + isSheet: false, + scopeInfo: scope, + exchange: $myExchange, + amountToTransfer: $amountToTransfer) + } + Group { // necessary for .backslide transition (bug in SwiftUI) let count = balances.count if balances.isEmpty { @@ -66,6 +117,20 @@ struct BalancesListView: View { .listStyle(myListStyle.style).anyView } } + .background( Group { + NavigationLink(destination: sendDest, tag: 1, selection: $actionSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden() + NavigationLink(destination: requestDest, tag: 2, selection: $actionSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden() + NavigationLink(destination: depositDest, tag: 3, selection: $actionSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden() + NavigationLink(destination: manualWithdrawDest, tag: 4, selection: $actionSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden() + }) + .onNotification(.SendAction) { actionSelected = 1 } + .onNotification(.RequestAction) { actionSelected = 2 } + .onNotification(.DepositAction) { actionSelected = 3 } + .onNotification(.WithdrawAction) { actionSelected = 4 } #if REFRESHABLE .refreshable { // already async symLog.log("refreshing balances") diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift @@ -117,7 +117,8 @@ extension BalancesSectionView: View { if scopeInfo.type == .exchange { let baseURL = scopeInfo.url?.trimURL ?? String(localized: "Unknown Payment Provider", comment: "exchange url") Text(baseURL) - .talerFont(.headline) + .talerFont(.body) + .foregroundColor(.secondary) // .listRowSeparator(.hidden) } BalanceCellV(stack: stack.push("BalanceCell"), @@ -158,34 +159,34 @@ extension BalancesSectionView: View { reloadOneAction: reloadOneAction) .padding(.leading, ICONLEADING) } - let showSpendingButton = DEMOCURRENCY == currency && !balance.available.isZero - if showSpendingButton { - if !minimalistic && showSpendingHint { - Text("You can spend these \(currencyName) in the Demo shop, or send them to another wallet.") - .talerFont(.body) - .multilineTextAlignment(.leading) - .listRowSeparator(.hidden) - } - let title = String(localized: "LinkTitle_DEMOSHOP", defaultValue: "Spend demo money") - let action = { - showSpendingHint = false - UIApplication.shared.open(URL(string:DEMOSHOP)!, options: [:]) - } - Button(action: action) { - HStack { - ButtonIconBadge(type: .payment, foreColor: .accentColor, done: false) - Spacer() - Text(title) - Spacer() - } - } - .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center)) - .accessibilityHint(String(localized: "Will go to the demo shop website.")) - .listRowSeparator(.hidden) - } +// let showSpendingButton = DEMOCURRENCY == currency && !balance.available.isZero +// if showSpendingButton { +// if !minimalistic && showSpendingHint { +// Text("You can spend these \(currencyName) in the Demo shop, or send them to another wallet.") +// .talerFont(.body) +// .multilineTextAlignment(.leading) +// .listRowSeparator(.hidden) +// } +// let title = String(localized: "LinkTitle_DEMOSHOP", defaultValue: "Spend demo money") +// let action = { +// showSpendingHint = false +// UIApplication.shared.open(URL(string:DEMOSHOP)!, options: [:]) +// } +// Button(action: action) { +// HStack { +// ButtonIconBadge(type: .payment, foreColor: .accentColor, done: false) +// Spacer() +// Text(title) +// Spacer() +// } +// } +// .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center)) +// .accessibilityHint(String(localized: "Will go to the demo shop website.")) +// .listRowSeparator(.hidden) +// } // DepositWithdrawV(stack: stack.push(), // currencyInfo: $currencyInfo, -//// scopeInfo: balance.scopeInfo, +// scopeInfo: balance.scopeInfo, // amountAvailable: balance.available, // amountToTransfer: $amountToTransfer) // does still have the wrong currency diff --git a/TalerWallet1/Views/Banking/DepositIbanV.swift b/TalerWallet1/Views/Banking/DepositIbanV.swift @@ -13,10 +13,10 @@ import SymLog struct DepositIbanV: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo +// @Binding var currencyInfo: CurrencyInfo let feeLabel: String? let feeIsNotZero: Bool? // nil = no fees at all, false = no fee for this tx - let amountAvailable: Amount? +// let amountAvailable: Amount? // @Binding var depositIBAN: String // @Binding var accountHolder: String @Binding var amountToTransfer: Amount @@ -33,6 +33,10 @@ struct DepositIbanV: View { @State private var transactionStarted: Bool = false @State private var paytoUri: String? = nil @FocusState private var isFocused: Bool + @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) + @State private var currencyName: String = UNKNOWN + @State private var currencySymbol: String = UNKNOWN + @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING) // Update currency when used private func buttonTitle(_ amount: Amount) -> String { let amountWithCurrency = amount.formatted(currencyInfo, isNegative: true, useISO: true) diff --git a/TalerWallet1/Views/Banking/ManualWithdraw.swift b/TalerWallet1/Views/Banking/ManualWithdraw.swift @@ -14,7 +14,7 @@ import SymLog struct ManualWithdraw: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo +// @Binding var currencyInfo: CurrencyInfo let isSheet: Bool // let exchangeBaseUrl: String let scopeInfo: ScopeInfo? @@ -28,6 +28,9 @@ struct ManualWithdraw: View { @State private var withdrawalAmountDetails: WithdrawalAmountDetails? = nil // @State var ageMenuList: [Int] = [] // @State var selectedAge = 0 + @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) + @State private var currencyName: String = UNKNOWN + @State private var currencySymbol: String = UNKNOWN var body: some View { #if PRINT_CHANGES @@ -113,6 +116,8 @@ struct ManualWithdraw: View { stack: stack.push()) } symLog.log("❗️ \(navTitle) onAppear") + print("ManualWithdraw.task❓HideTabBarView") + NotificationCenter.default.post(name: .HideTabBarView, object: nil) } .onDisappear { symLog.log("❗️ \(navTitle) onDisappear") diff --git a/TalerWallet1/Views/HelperViews/View+fixedInnerHeight.swift b/TalerWallet1/Views/HelperViews/View+fixedInnerHeight.swift @@ -23,8 +23,10 @@ import SwiftUI import UIKit struct InnerHeightPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat = .zero - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } + static var defaultValue: CGFloat = 400 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } } extension View { diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -10,6 +10,7 @@ import SwiftUI import os.log import SymLog import AVFoundation +import taler_swift // Use this to delay instantiation when using `NavigationLink`, etc... struct LazyView<Content: View>: View { @@ -162,10 +163,119 @@ struct MainView: View { } // body } // MARK: - TabBar -enum Tab { +enum Tab: String, Hashable, CaseIterable { case balances - case overview + case actions +// case overview case settings + + var index: Int { + switch self { + case .balances: return 0 + case .actions: return 1 +// case .overview: return 1 + case .settings: return 2 + } + } + + var title: String { + switch self { + case .balances: return String(localized: "TitleBalances", defaultValue: "Balances") + case .actions: return String(localized: "TitleActions", defaultValue: "Actions") +// case .overview: return String(localized: "TitleOverview", defaultValue: "Overview") + case .settings: return String(localized: "TitleSettings", defaultValue: "Settings") + } + } + + var image: Image { +#if TALER_WALLET + let logo = "taler-logo-2023-blue" +#else // GNU Taler + let logo = "taler-logo-2023-red" +#endif + switch self { + case .balances: return Image(systemName: "chart.bar.xaxis") + case .actions: return Image(logo) +// case .overview: return Image(systemName: "dollarsign") + case .settings: return Image(systemName: "gear") + } + } + +} + +struct TabBarView: View { + @Binding var selection: Tab + let onActionTab: () -> Void + @AppStorage("minimalistic") var minimalistic: Bool = false + @State private var isHidden = 0 + + private func tabBarItem(for tab: Tab) -> some View { + VStack(spacing: 0) { + if tab == .actions { + let width = 72.0 + let height = 57.6 + tab.image + .resizable() + .scaledToFill() + .frame(width: width, height: height) + .clipped() // Crop the image to the frame size + .padding(.bottom, 4) + } else { + let size = minimalistic ? 36.0 : 24.0 + tab.image + .resizable() + .renderingMode(.template) + .tint(.black) + .aspectRatio(contentMode: .fit) + .frame(width: size, height: size) + if !minimalistic { + Text(tab.title) + .lineLimit(1) + .talerFont(.body) +// .padding(.top, 2) + } + } + } + .foregroundColor(selection == tab ? .accentColor : .secondary) + .padding(.vertical, 8) + .accessibilityElement(children: .combine) + .accessibility(label: Text(tab.title)) + .accessibility(addTraits: [.isButton]) + } + + var body: some View { + Group { + if isHidden > 0 { + EmptyView() + } else { + HStack(alignment: .bottom) { + ForEach(Tab.allCases, id: \.self) { tab in + tabBarItem(for: tab) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + .onTapGesture { + if tab == .actions { + onActionTab() + } else { + selection = tab + } + } + } + } + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.bottom)) + } + } + .onNotification(.HideTabBarView) { + isHidden += 1 + print("❗️HideTabBarView \(isHidden)") + } + .onNotification(.ShowTabBarView) { + if isHidden > 0 { + isHidden -= 1 + print("❗️ShowTabBarView \(isHidden)") + } + } + } } // MARK: - Content @@ -194,6 +304,11 @@ extension MainView { @State private var showKycAlert: Bool = false @State private var kycURI: URL? + @State private var showActionSheet = false + @State private var sheetHeight: CGFloat = .zero + @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used + @State private var summary: String = "" + @State private var showSpendingHint = true private var openKycButton: some View { Button("KYC") { @@ -251,59 +366,59 @@ extension MainView { #else let delay: UInt = 0 // no delay for release builds #endif - let balancesTitle = String(localized: "TitleBalances", defaultValue: "Balances") - let overviewTitle = String(localized: "TitleOverview", defaultValue: "Overview") - let settingsTitle = String(localized: "TitleSettings", defaultValue: "Settings") - Group { -// let labelStyle = minimalistic ? IconOnlyLabelStyle() : TitleAndIconLabelStyle() // labelStyle doesn't work - TabView(selection: tabSelection()) { - NavigationView { - BalancesListView(stack: stack.push(balancesTitle), - balances: $balances, -// shouldReloadPending: $shouldReloadPending, - shouldReloadBalances: $shouldReloadBalances, - cameraAction: cameraAction) - .navigationTitle(balancesTitle) - }.id(viewState.rootViewId) // any change to rootViewId triggers popToRootView behaviour - .navigationViewStyle(.stack) - .tabItem { - Image(systemName: "chart.bar.xaxis") // iOS will automatically use filled variant - .accessibilityLabel(balancesTitle) - if !minimalistic { Text(balancesTitle) } + let balancesTitle = Tab.balances.title +// let actionTitle = Tab.actions.title + let settingsTitle = Tab.settings.title + let tabBarView = TabBarView(selection: $selectedTab) { + if selectedTab != .balances { + selectedTab = .balances } - .tag(Tab.balances) - .badge(0) // TODO: set badge if transaction finished in background -// if balances.count > 1 { + // TODO: check NavigationStack, pop only if necessary + ViewState.shared.popToRootView(nil) + showActionSheet = true + } + ZStack(alignment: .bottom) { + TabView(selection: tabSelection()) { + NavigationView { + BalancesListView(stack: stack.push(balancesTitle), + balances: $balances, +// shouldReloadPending: $shouldReloadPending, + shouldReloadBalances: $shouldReloadBalances, + cameraAction: cameraAction) + .navigationTitle(balancesTitle) + }.id(viewState.rootViewId) // any change to rootViewId triggers popToRootView behaviour + .navigationViewStyle(.stack) + .tag(Tab.balances) +// .badge(0) // TODO: set badge if transaction finished in background (not here, but in the overlayed tabView instead) + + EmptyView() + .tag(Tab.actions) +// if balances.count > 1 { +// let overviewTitle = Tab.overview.title // NavigationView { // OverviewListV(stack: stack.push(overviewTitle), // balances: $balances, -//// shouldReloadPending: $shouldReloadPending, +// shouldReloadPending: $shouldReloadPending, // shouldReloadBalances: $shouldReloadBalances, // cameraAction: cameraAction) // .navigationTitle(overviewTitle) // }.id(viewState2.rootViewId) // any change to rootViewId triggers popToRootView behaviour // .navigationViewStyle(.stack) -// .tabItem { -// Image(systemName: "dollarsign") -// .accessibilityLabel(overviewTitle) -// if !minimalistic { Text(overviewTitle) } -// } // .tag(Tab.overview) // } - NavigationView { - SettingsView(stack: stack.push(), - balances: $balances, - navTitle: settingsTitle) - }.navigationViewStyle(.stack) - .tabItem { - Image(systemName: "gear") // system will automatically use filled variant - .accessibilityLabel(settingsTitle) - if !minimalistic { Text(settingsTitle) } - } - .tag(Tab.settings) + + NavigationView { + SettingsView(stack: stack.push(), + balances: $balances, + navTitle: settingsTitle) + }.id(viewState2.rootViewId) // any change to rootViewId triggers popToRootView behaviour + .navigationViewStyle(.stack) + .tag(Tab.settings) + } // TabView + .padding(.bottom, 16) + tabBarView } -// .animation(.linear(duration: LAUNCHDURATION), value: selectedTab) doesn't work. Needs CustomTabView - } .onNotification(.KYCrequired) { notification in + .onNotification(.KYCrequired) { notification in // show an alert with the KYC link (button) which opens in Safari if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { if let kycString = transition.experimentalUserData { @@ -322,6 +437,22 @@ extension MainView { actions: { openKycButton dismissAlertButton }, message: { Text("Tap the button to go to the KYC website.") }) + .sheet(isPresented: $showActionSheet) { + let content = VStack { + ActionsSheet(stack: stack.push(), + balances: $balances, + showSpendingHint: $showSpendingHint, + amountToTransfer: $amountToTransfer, + summary: $summary, + cameraAction: cameraAction) + .padding(.bottom, 32) + } + if #available(iOS 16, *) { + content.fixedInnerHeight($sheetHeight) + } else { + content + } + } .onNotification(.BalanceChange) { notification in // reload balances on receiving BalanceChange notification ... logger.info(".onNotification(.BalanceChange) ==> reload") diff --git a/TalerWallet1/Views/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Peer2peer/RequestPayment.swift @@ -10,7 +10,7 @@ import SymLog struct RequestPayment: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo +// @Binding var currencyInfo: CurrencyInfo @Binding var amountToTransfer: Amount @Binding var summary: String @@ -30,6 +30,9 @@ struct RequestPayment: View { @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING) // Update currency when used @State private var amountZero = Amount.zero(currency: EMPTYSTRING) // needed for isZero @State private var exchange: Exchange? = nil // wg. noFees + @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) + @State private var currencyName: String = UNKNOWN + @State private var currencySymbol: String = UNKNOWN private func shortcutAction(_ shortcut: Amount) { amountShortcut = shortcut diff --git a/TalerWallet1/Views/Peer2peer/SendAmount.swift b/TalerWallet1/Views/Peer2peer/SendAmount.swift @@ -13,9 +13,9 @@ import SymLog struct SendAmount: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo +// @Binding var currencyInfo: CurrencyInfo - let available: Amount + let available: Amount? @Binding var amountToTransfer: Amount @Binding var summary: String let scopeInfo: ScopeInfo @@ -38,6 +38,10 @@ struct SendAmount: View { @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING) // GetMaxPeerPushAmount @State private var exchange: Exchange? = nil // wg. noFees + @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) + @State private var currencyName: String = UNKNOWN + @State private var currencySymbol: String = UNKNOWN + private func shortcutAction(_ shortcut: Amount) { amountShortcut = shortcut shortcutSelected = true @@ -111,8 +115,8 @@ struct SendAmount: View { defaultValue: "Send \(currencySymbol)", comment: "NavTitle: Send 'currencySymbol'") let availableStr = amountAvailable.formatted(currencyInfo, isNegative: false) -// let _ = print("available: \(available)") - let _ = symLog.log("currency: \(currencyInfo.specs.name), available: \(available)") +// let _ = print("available: \(availableStr)") +// let _ = symLog.log("currency: \(currencyInfo.specs.name), available: \(availableStr)") let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.") @@ -170,7 +174,7 @@ struct SendAmount: View { scope: scopeInfo, viewHandles: true) amountAvailable = amount } catch { - amountAvailable = available + amountAvailable = available ?? Amount.zero(currency: amountToTransfer.currencyStr) } } .onAppear { @@ -241,7 +245,7 @@ fileprivate struct Preview_Content: View { exchangeUpdateStatus: .ready, ageRestrictionOptions: []) SendAmount(stack: CallStack("Preview"), - currencyInfo: $currencyInfoL, +// currencyInfo: $currencyInfoL, available: amount, amountToTransfer: $amountToPreview, summary: $summary, diff --git a/TalerWallet1/Views/Sheets/WithdrawExchangeV.swift b/TalerWallet1/Views/Sheets/WithdrawExchangeV.swift @@ -30,7 +30,7 @@ struct WithdrawExchangeV: View { let currency = exchange.scopeInfo.currency Group { ManualWithdraw(stack: stack.push(), - currencyInfo: $currencyInfo, +// currencyInfo: $currencyInfo, isSheet: true, scopeInfo: exchange.scopeInfo, exchange: $exchange, diff --git a/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift b/TalerWallet1/Views/Transactions/ManualDetailsWireV.swift @@ -24,7 +24,8 @@ struct TransferRestrictionsV: View { let obtainNBS = obtainStr.nbs Text(minimalistic ? "Transfer \(amountNBS) to the Payment Service." : "You need to transfer \(amountNBS) from your regular bank account to the Payment Service Provider to receive \(obtainNBS) as electronic cash in this wallet.") - .multilineTextAlignment(.leading) + .talerFont(.body) + .multilineTextAlignment(.leading) if let restrictions { ForEach(restrictions) { restriction in if let hintsI18 = restriction.human_hint_i18n { @@ -113,22 +114,27 @@ struct ManualDetailsWireV: View { } .padding(.top, -8) let step1 = Text(minimalistic ? "**Step 1:** Copy+Paste this subject:" : "**Step 1:** Copy this code and paste it into the subject/purpose field in your banking app or bank website:") + .talerFont(.body) .multilineTextAlignment(.leading) let mandatory = Text("This is mandatory, otherwise your money will not arrive in this wallet.") .bold() + .talerFont(.body) .multilineTextAlignment(.leading) .listRowSeparator(.hidden) let step2i = Text(minimalistic ? "**Step 2:** Copy+Paste payee and IBAN:" : "**Step 2:** If you don't already have it in your banking favorites list, then copy and paste payee and IBAN into the payee/IBAN fields in your banking app or website (and save it as favorite for the next time):") + .talerFont(.body) .multilineTextAlignment(.leading) .padding(.top) let step2x = Text(minimalistic ? "**Step 2:** Copy+Paste payee and account:" : "**Step 2:** Copy and paste payee and account into the corresponding fields in your banking app or website:") + .talerFont(.body) .multilineTextAlignment(.leading) .padding(.top) let amountNBS = amountStr.nbs let step3 = Text(minimalistic ? "**Step 3:** Transfer \(amountNBS)." : "**Step 3:** Finish the wire transfer of \(amountNBS) in your banking app or website, then this withdrawal will proceed automatically. Depending on your bank the transfer can take from minutes to two working days, please be patient.") + .talerFont(.body) .multilineTextAlignment(.leading) .padding(.top)