taler-ios

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

commit f5f19a8aabcf3de80345b01375f433e569f25850
parent 1410e3d878da65ef23889f8c8c941eb506f7c244
Author: Marc Stibane <marc@taler.net>
Date:   Fri, 13 Sep 2024 11:52:36 +0200

SendAmountV

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 12++++++------
DTalerWallet1/Views/Actions/Peer2peer/SendAmount.swift | 367-------------------------------------------------------------------------------
ATalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift | 369+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/Main/MainView.swift | 16++++++++--------
4 files changed, 383 insertions(+), 381 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -53,7 +53,7 @@ 4E3EAE2D2A990778009F1BE8 /* Model+Exchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC82A42BC4800CC88B8 /* Model+Exchange.swift */; }; 4E3EAE2E2A990778009F1BE8 /* QRCodeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5A88F42A38A4FD00072618 /* QRCodeDetailView.swift */; }; 4E3EAE2F2A990778009F1BE8 /* TransactionsEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E87C8722A31CB7F001C6406 /* TransactionsEmptyView.swift */; }; - 4E3EAE312A990778009F1BE8 /* SendAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E40E0BD29F25ABB00B85369 /* SendAmount.swift */; }; + 4E3EAE312A990778009F1BE8 /* SendAmountV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E40E0BD29F25ABB00B85369 /* SendAmountV.swift */; }; 4E3EAE332A990778009F1BE8 /* EqualIconWidthDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E8E25322A1CD39700A27BFA /* EqualIconWidthDomain.swift */; }; 4E3EAE342A990778009F1BE8 /* SuperScriptDigits.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA563E2A7FD9390084948B /* SuperScriptDigits.swift */; }; 4E3EAE352A990778009F1BE8 /* P2pPayURIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E578E932A4822D500F21F1C /* P2pPayURIView.swift */; }; @@ -140,7 +140,7 @@ 4E3EAEA52AA12582009F1BE8 /* Nunito-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4E3EAEA12AA12582009F1BE8 /* Nunito-BoldItalic.ttf */; }; 4E3EAEA82AA70157009F1BE8 /* Binding+onChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3EAEA72AA70157009F1BE8 /* Binding+onChange.swift */; }; 4E3EAEA92AA70157009F1BE8 /* Binding+onChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3EAEA72AA70157009F1BE8 /* Binding+onChange.swift */; }; - 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E40E0BD29F25ABB00B85369 /* SendAmount.swift */; }; + 4E40E0BE29F25ABB00B85369 /* SendAmountV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E40E0BD29F25ABB00B85369 /* SendAmountV.swift */; }; 4E448AB72C4A4109007D5C92 /* BalancesPendingRowV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E448AB62C4A4109007D5C92 /* BalancesPendingRowV.swift */; }; 4E448AB82C4A4109007D5C92 /* BalancesPendingRowV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E448AB62C4A4109007D5C92 /* BalancesPendingRowV.swift */; }; 4E4F60A82C3BBF9F003BB669 /* View+Condition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4F60A72C3BBF9F003BB669 /* View+Condition.swift */; }; @@ -377,7 +377,7 @@ 4E3EAEA02AA12582009F1BE8 /* Nunito-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Nunito-Italic.ttf"; sourceTree = "<group>"; }; 4E3EAEA12AA12582009F1BE8 /* Nunito-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Nunito-BoldItalic.ttf"; sourceTree = "<group>"; }; 4E3EAEA72AA70157009F1BE8 /* Binding+onChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Binding+onChange.swift"; sourceTree = "<group>"; }; - 4E40E0BD29F25ABB00B85369 /* SendAmount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendAmount.swift; sourceTree = "<group>"; }; + 4E40E0BD29F25ABB00B85369 /* SendAmountV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendAmountV.swift; sourceTree = "<group>"; }; 4E448AB62C4A4109007D5C92 /* BalancesPendingRowV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalancesPendingRowV.swift; sourceTree = "<group>"; }; 4E4F60A72C3BBF9F003BB669 /* View+Condition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Condition.swift"; sourceTree = "<group>"; }; 4E50B34F2A1BEE8000F9F01C /* ManualWithdraw.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManualWithdraw.swift; sourceTree = "<group>"; }; @@ -875,7 +875,7 @@ 4ECB627E2A0BA4DA004ABBB7 /* Peer2peer */ = { isa = PBXGroup; children = ( - 4E40E0BD29F25ABB00B85369 /* SendAmount.swift */, + 4E40E0BD29F25ABB00B85369 /* SendAmountV.swift */, 4E9320442A1645B600A87B0E /* RequestPayment.swift */, 4E7940DD29FC307C00A9AEA1 /* P2PSubjectV.swift */, 4EB3136029FEE79B007D68BC /* P2PReadyV.swift */, @@ -1216,7 +1216,7 @@ 4E3EAE2F2A990778009F1BE8 /* TransactionsEmptyView.swift in Sources */, 4EEBEFB02C8982180020D340 /* View+fixedInnerHeight.swift in Sources */, 4E605DAF2AADDD13002FB9A7 /* UIScreen+screenSize.swift in Sources */, - 4E3EAE312A990778009F1BE8 /* SendAmount.swift in Sources */, + 4E3EAE312A990778009F1BE8 /* SendAmountV.swift in Sources */, 4E3EAE332A990778009F1BE8 /* EqualIconWidthDomain.swift in Sources */, 4EEC11932B83FB7A00146CFF /* SubjectInputV.swift in Sources */, E3E48FB52B9B7D5000898A0F /* Encodable+toJSON.swift in Sources */, @@ -1348,7 +1348,7 @@ 4E87C8732A31CB7F001C6406 /* TransactionsEmptyView.swift in Sources */, 4EEBEFB12C8982180020D340 /* View+fixedInnerHeight.swift in Sources */, 4E605DB02AADDD13002FB9A7 /* UIScreen+screenSize.swift in Sources */, - 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */, + 4E40E0BE29F25ABB00B85369 /* SendAmountV.swift in Sources */, 4E8E25332A1CD39700A27BFA /* EqualIconWidthDomain.swift in Sources */, 4EEC11942B83FB7A00146CFF /* SubjectInputV.swift in Sources */, E3E48FB62B9B7D5000898A0F /* Encodable+toJSON.swift in Sources */, diff --git a/TalerWallet1/Views/Actions/Peer2peer/SendAmount.swift b/TalerWallet1/Views/Actions/Peer2peer/SendAmount.swift @@ -1,367 +0,0 @@ -/* - * 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 - -// Called when tapping "Send Coins" in the balances list -struct SendAmount: View { - private let symLog = SymLogV(0) - let stack: CallStack - let balances: [Balance] - @Binding var selectedBalance: Balance? // selected balance when the action button is tapped in Transactions - @Binding var amountToTransfer: Amount - @Binding var summary: String - let scopeInfo: ScopeInfo - let cameraAction: () -> Void - - @State private var balanceIndex = 0 - @State private var nonZeroBalances: [Balance] = [] - @State private var balance: Balance? = nil - - var body: some View { -#if PRINT_CHANGES - let _ = Self._printChanges() -#endif -// nonZeroBalances.count > 0 -// let balance = selectedBalance ?? nonZeroBalances[balanceIndex] - let sendAmountView = ScrollView { - let count = nonZeroBalances.count - if let selectedBalance { - let urlOrCurrency = selectedBalance.scopeInfo.url?.trimURL() - ?? selectedBalance.scopeInfo.currency - let amount = selectedBalance.available - let formattedAmount = amount.formatted(isNegative: false, useISO: false) - let label = String("\(urlOrCurrency):\t\(formattedAmount.nbs)") - Text(label) -// .padding(.leading) - .talerFont(.title3) - } else if count > 0 { - ScopePicker(value: $balanceIndex, balances: nonZeroBalances) { index in - balanceIndex = index - balance = nonZeroBalances[index] - } - } - SendAmountContent(stack: stack.push(), - balance: $balance, - balanceIndex: $balanceIndex, - amountToTransfer: $amountToTransfer, - summary: $summary, - cameraAction: cameraAction) - } // ScrollView - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) -// .scrollBounceBehavior(.basedOnSize) needs iOS 16.4 - .task { - if selectedBalance == nil { - nonZeroBalances = Balance.nonZeroBalances(balances) - let count = nonZeroBalances.count - if balanceIndex >= count { - balanceIndex = 0 - } - if count > 0 { - balance = nonZeroBalances[balanceIndex] - } else { - balance = nil - } - } else { - balance = selectedBalance - } - } - - if #available(iOS 16.0, *) { - sendAmountView.toolbar(.hidden, for: .tabBar) - } else { - sendAmountView - } - } -} -// MARK: - -struct SendAmountContent: View { - private let symLog = SymLogV() - let stack: CallStack - @Binding var balance: Balance? - @Binding var balanceIndex: Int - @Binding var amountToTransfer: Amount - @Binding var summary: String - let cameraAction: () -> Void - - // TODO: call getMaxPeerPushDebitAmountM - - @EnvironmentObject private var controller: Controller - @EnvironmentObject private var model: WalletModel - @Environment(\.colorScheme) private var colorScheme - @Environment(\.colorSchemeContrast) private var colorSchemeContrast - @AppStorage("minimalistic") var minimalistic: Bool = false - - @State var peerPushCheck: CheckPeerPushDebitResponse? = nil - @State private var expireDays = SEVENDAYS - @State private var insufficient = false -// @State private var feeAmount: Amount? = nil - @State private var feeStr: String = EMPTYSTRING - @State private var buttonSelected = false - @State private var shortcutSelected = false - @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING) // Update currency when used - @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING) // GetMaxPeerPushAmount - @State private var exchange: Exchange? = nil // wg. noFees - - @State private var currencyInfo = CurrencyInfo.zero(UNKNOWN) - @State private var currencyName = UNKNOWN - @State private var currencySymbol = UNKNOWN - - private func shortcutAction(_ shortcut: Amount) { - amountShortcut = shortcut - shortcutSelected = true - } - private func buttonAction() { buttonSelected = true } - - private func feeLabel(_ feeString: String) -> String { - feeString.count > 0 ? String(localized: "+ \(feeString) fee") - : EMPTYSTRING - } - - private func fee(raw: Amount, effective: Amount) -> Amount? { - do { // Outgoing: fee = effective - raw - let fee = try effective - raw - return fee - } catch {} - return nil - } - - private func feeIsNotZero() -> Bool? { - if let hasNoFees = exchange?.noFees { - if hasNoFees { - return nil // this exchange never has fees - } - } - return peerPushCheck == nil ? false - : true // TODO: !(feeAmount?.isZero ?? false) - } - - private func computeFeeSend(_ amount: Amount) async -> ComputeFeeResult? { - if amount.isZero { - return ComputeFeeResult.zero() - } - let insufficient = (try? amount > amountAvailable) ?? true - if insufficient { - return ComputeFeeResult.insufficient() - } - do { - let ppCheck = try await model.checkPeerPushDebitM(amount, scope: scopeInfo, viewHandles: true) - let raw = ppCheck.amountRaw - let effective = ppCheck.amountEffective - if let fee = fee(raw: raw, effective: effective) { - feeStr = fee.formatted(currencyInfo, isNegative: false) - symLog.log("Fee = \(feeStr)") - let insufficient = (try? effective > amountAvailable) ?? true - - peerPushCheck = ppCheck - let feeLabel = feeLabel(feeStr) -// announce("\(amountVoiceOver), \(feeLabel)") - return ComputeFeeResult(insufficient: insufficient, - feeAmount: fee, - feeStr: feeLabel, - numCoins: nil) - } else { - peerPushCheck = nil - } - } catch { - // handle cancel, errors - symLog.log("❗️ \(error), \(error.localizedDescription)") - switch error { - case let walletError as WalletBackendError: - switch walletError { - case .walletCoreError(let wError): - if wError?.code == 7027 { - return ComputeFeeResult.insufficient() - } - default: break - } - default: break - } - } - return nil - } - - var body: some View { -#if true //PRINT_CHANGES - let _ = Self._printChanges() - let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear -#endif - let navTitle = String(localized: "NavTitle_Send", - defaultValue: "Send", - comment: "NavTitle: Send") - let availableStr = amountAvailable.formatted(currencyInfo, isNegative: false) - let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) - let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.") - - let inputDestination = LazyView { - P2PSubjectV(stack: stack.push(), - scope: scopeInfo, - currencyInfo: $currencyInfo, - feeLabel: feeLabel(feeStr), - feeIsNotZero: feeIsNotZero(), - outgoing: true, - amountToTransfer: $amountToTransfer, // from the textedit - summary: $summary, - expireDays: $expireDays) - } - let shortcutDestination = LazyView { - P2PSubjectV(stack: stack.push(), - scope: scopeInfo, - currencyInfo: $currencyInfo, - feeLabel: nil, - feeIsNotZero: feeIsNotZero(), - outgoing: true, - amountToTransfer: $amountShortcut, // from the tapped shortcut button - summary: $summary, - expireDays: $expireDays) - } - - Group { - let amountLabel = minimalistic ? String(localized: "Amount:") - : String(localized: "Amount to send:") - AmountInputV(stack: stack.push(), - currencyInfo: $currencyInfo, - amountAvailable: $amountAvailable, - amountLabel: amountLabel, - amountToTransfer: $amountToTransfer, - wireFee: nil, - summary: $summary, - shortcutAction: shortcutAction, - buttonAction: buttonAction, - feeIsNegative: false, - computeFee: computeFeeSend) - .background(NavigationLink(destination: shortcutDestination, isActive: $shortcutSelected) - { EmptyView() }.frame(width: 0).opacity(0).hidden() - ) // shortcutDestination - .background(NavigationLink(destination: inputDestination, isActive: $buttonSelected) - { EmptyView() }.frame(width: 0).opacity(0).hidden() - ) // inputDestination - } - .frame(maxWidth: .infinity, alignment: .leading) - .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) - .navigationTitle(navTitle) - .task { - do { - let amount = try await model.getMaxPeerPushDebitAmountM(amountToTransfer.currencyStr, - scope: scopeInfo, viewHandles: true) - amountAvailable = amount - } catch { - amountAvailable = Amount.zero(currency: amountToTransfer.currencyStr) - } - } - .onAppear { - DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push()) - // if we set >> controller.frontendState = -1 << here, then becomeFirstResponder won't work! - symLog.log("❗️ \(navTitle) onAppear") - } - .onDisappear { - symLog.log("❗️ \(navTitle) onDisappear") - } - .navigationBarItems(trailing: QRButton(action: cameraAction)) - .task(id: balanceIndex + (1000 * controller.currencyTicker)) { - if let balance { - let scopeInfo = balance.scopeInfo - let currency = scopeInfo.currency - amountAvailable = balance.available - amountToTransfer.setCurrency(currency) - currencyInfo = controller.info(for: currency, controller.currencyTicker) - currencyName = currencyInfo.scope.currency - currencySymbol = currencyInfo.altUnitSymbol ?? currencyInfo.specs.name - } - } -// .task(id: amountToTransfer.value) { -// if exchange == nil { -// if let url = scopeInfo.url { -// exchange = try? await model.getExchangeByUrl(url: url) -// } -// } -// do { -// insufficient = try amountToTransfer > amountAvailable -// } catch { -// print("Yikes❗️ insufficient failed❗️") -// insufficient = true -// } -// -// if insufficient { -// announce("\(amountVoiceOver), \(insufficientLabel2)") -// } else if amountToTransfer.isZero { -// feeStr = EMPTYSTRING -// } else { -// if let ppCheck = try? await model.checkPeerPushDebitM(amountToTransfer) { -// // TODO: set from exchange -//// agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) -// if let feeAmount = fee(ppCheck: ppCheck) { -// feeStr = feeAmount.formatted(currencyInfo, isNegative: false) -// let feeLabel = feeLabel(feeStr) -// announce("\(amountVoiceOver), \(feeLabel)") -// } else { -// feeStr = EMPTYSTRING -// announce(amountVoiceOver) -// } -// peerPushCheck = ppCheck -// } else { -// peerPushCheck = nil -// } -// } -// } - } -} -// MARK: - -#if DEBUG -fileprivate struct Preview_Content: View { - @State private var amountToPreview = Amount(currency: DEMOCURRENCY, cent: 510) - @State private var summary: String = "" - @State private var currencyInfoL: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) - @State private var noBalance: Balance? = nil - - private func checkCameraAvailable() -> Void { - // Open Camera when QR-Button was tapped - } - var body: some View { - let amount = Amount(currency: DEMOCURRENCY, cent: 1000) - let pending = Amount(currency: DEMOCURRENCY, cent: 0) - let currencyInfo = CurrencyInfo.zero(DEMOCURRENCY) - let exchange2 = Exchange(exchangeBaseUrl: ARS_EXP_EXCHANGE, - masterPub: "masterPub", - scopeInfo: currencyInfo.scope, - paytoUris: [], - tosStatus: .proposed, - exchangeEntryStatus: .ephemeral, - exchangeUpdateStatus: .ready, - ageRestrictionOptions: []) - let scopeInfo = ScopeInfo(type: .exchange, url: DEMOEXCHANGE, currency: DEMOCURRENCY) - let balance = Balance(scopeInfo: scopeInfo, - available: amount, - pendingIncoming: pending, - pendingOutgoing: pending, - flags: []) - SendAmount(stack: CallStack("Preview"), - balances: [balance], - selectedBalance: $noBalance, - amountToTransfer: $amountToPreview, - summary: $summary, - scopeInfo: currencyInfo.scope, - cameraAction: checkCameraAvailable) - } -} - -fileprivate struct Previews: PreviewProvider { - @MainActor - struct StateContainer: View { - @StateObject private var controller = Controller.shared - var body: some View { - Preview_Content() - .environmentObject(controller) - } - } - static var previews: some View { - StateContainer() - } -} -#endif diff --git a/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift b/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift @@ -0,0 +1,369 @@ +/* + * 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 + +// Called when tapping "Send Coins" in the balances list +struct SendAmountV: View { + private let symLog = SymLogV(0) + let stack: CallStack + let balances: [Balance] + @Binding var selectedBalance: Balance? // selected balance when the action button is tapped in Transactions + @Binding var amountToTransfer: Amount + @Binding var summary: String + let scopeInfo: ScopeInfo + let cameraAction: () -> Void + + @State private var balanceIndex = 0 + @State private var nonZeroBalances: [Balance] = [] + @State private var balance: Balance? = nil + + var body: some View { +#if PRINT_CHANGES + let _ = Self._printChanges() +#endif +// nonZeroBalances.count > 0 +// let balance = selectedBalance ?? nonZeroBalances[balanceIndex] + let sendAmountView = ScrollView { + let count = nonZeroBalances.count + if let selectedBalance { + let urlOrCurrency = selectedBalance.scopeInfo.url?.trimURL + ?? selectedBalance.scopeInfo.currency + let amount = selectedBalance.available + let formattedAmount = amount.formatted(isNegative: false, useISO: false) + let label = String("\(urlOrCurrency):\t\(formattedAmount.nbs)") + Text(label) +// .padding(.leading) + .talerFont(.title3) + } else if count > 0 { + ScopePicker(value: $balanceIndex, balances: nonZeroBalances) { index in + balanceIndex = index + balance = nonZeroBalances[index] + } + } + SendAmountContent(stack: stack.push(), + balance: $balance, + balanceIndex: $balanceIndex, + amountToTransfer: $amountToTransfer, + summary: $summary, + scopeInfo: scopeInfo, + cameraAction: cameraAction) + } // ScrollView + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) +// .scrollBounceBehavior(.basedOnSize) needs iOS 16.4 + .task { + if selectedBalance == nil { + nonZeroBalances = Balance.nonZeroBalances(balances) + let count = nonZeroBalances.count + if balanceIndex >= count { + balanceIndex = 0 + } + if count > 0 { + balance = nonZeroBalances[balanceIndex] + } else { + balance = nil + } + } else { + balance = selectedBalance + } + } + + if #available(iOS 16.0, *) { + sendAmountView.toolbar(.hidden, for: .tabBar) + } else { + sendAmountView + } + } +} +// MARK: - +struct SendAmountContent: View { + private let symLog = SymLogV() + let stack: CallStack + @Binding var balance: Balance? + @Binding var balanceIndex: Int + @Binding var amountToTransfer: Amount + @Binding var summary: String + let scopeInfo: ScopeInfo + let cameraAction: () -> Void + + // TODO: call getMaxPeerPushDebitAmountM + + @EnvironmentObject private var controller: Controller + @EnvironmentObject private var model: WalletModel + @Environment(\.colorScheme) private var colorScheme + @Environment(\.colorSchemeContrast) private var colorSchemeContrast + @AppStorage("minimalistic") var minimalistic: Bool = false + + @State var peerPushCheck: CheckPeerPushDebitResponse? = nil + @State private var expireDays = SEVENDAYS + @State private var insufficient = false +// @State private var feeAmount: Amount? = nil + @State private var feeStr: String = EMPTYSTRING + @State private var buttonSelected = false + @State private var shortcutSelected = false + @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING) // Update currency when used + @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING) // GetMaxPeerPushAmount + @State private var exchange: Exchange? = nil // wg. noFees + + @State private var currencyInfo = CurrencyInfo.zero(UNKNOWN) + @State private var currencyName = UNKNOWN + @State private var currencySymbol = UNKNOWN + + private func shortcutAction(_ shortcut: Amount) { + amountShortcut = shortcut + shortcutSelected = true + } + private func buttonAction() { buttonSelected = true } + + private func feeLabel(_ feeString: String) -> String { + feeString.count > 0 ? String(localized: "+ \(feeString) fee") + : EMPTYSTRING + } + + private func fee(raw: Amount, effective: Amount) -> Amount? { + do { // Outgoing: fee = effective - raw + let fee = try effective - raw + return fee + } catch {} + return nil + } + + private func feeIsNotZero() -> Bool? { + if let hasNoFees = exchange?.noFees { + if hasNoFees { + return nil // this exchange never has fees + } + } + return peerPushCheck == nil ? false + : true // TODO: !(feeAmount?.isZero ?? false) + } + + private func computeFeeSend(_ amount: Amount) async -> ComputeFeeResult? { + if amount.isZero { + return ComputeFeeResult.zero() + } + let insufficient = (try? amount > amountAvailable) ?? true + if insufficient { + return ComputeFeeResult.insufficient() + } + do { + let ppCheck = try await model.checkPeerPushDebitM(amount, scope: scopeInfo, viewHandles: true) + let raw = ppCheck.amountRaw + let effective = ppCheck.amountEffective + if let fee = fee(raw: raw, effective: effective) { + feeStr = fee.formatted(currencyInfo, isNegative: false) + symLog.log("Fee = \(feeStr)") + let insufficient = (try? effective > amountAvailable) ?? true + + peerPushCheck = ppCheck + let feeLabel = feeLabel(feeStr) +// announce("\(amountVoiceOver), \(feeLabel)") + return ComputeFeeResult(insufficient: insufficient, + feeAmount: fee, + feeStr: feeLabel, + numCoins: nil) + } else { + peerPushCheck = nil + } + } catch { + // handle cancel, errors + symLog.log("❗️ \(error), \(error.localizedDescription)") + switch error { + case let walletError as WalletBackendError: + switch walletError { + case .walletCoreError(let wError): + if wError?.code == 7027 { + return ComputeFeeResult.insufficient() + } + default: break + } + default: break + } + } + return nil + } + + var body: some View { +#if true //PRINT_CHANGES + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif + let navTitle = String(localized: "NavTitle_Send", + defaultValue: "Send", + comment: "NavTitle: Send") + let availableStr = amountAvailable.formatted(currencyInfo, isNegative: false) + let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) + let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.") + + let inputDestination = LazyView { + P2PSubjectV(stack: stack.push(), + scope: scopeInfo, + currencyInfo: $currencyInfo, + feeLabel: feeLabel(feeStr), + feeIsNotZero: feeIsNotZero(), + outgoing: true, + amountToTransfer: $amountToTransfer, // from the textedit + summary: $summary, + expireDays: $expireDays) + } + let shortcutDestination = LazyView { + P2PSubjectV(stack: stack.push(), + scope: scopeInfo, + currencyInfo: $currencyInfo, + feeLabel: nil, + feeIsNotZero: feeIsNotZero(), + outgoing: true, + amountToTransfer: $amountShortcut, // from the tapped shortcut button + summary: $summary, + expireDays: $expireDays) + } + + Group { + let amountLabel = minimalistic ? String(localized: "Amount:") + : String(localized: "Amount to send:") + AmountInputV(stack: stack.push(), + currencyInfo: $currencyInfo, + amountAvailable: $amountAvailable, + amountLabel: amountLabel, + amountToTransfer: $amountToTransfer, + wireFee: nil, + summary: $summary, + shortcutAction: shortcutAction, + buttonAction: buttonAction, + feeIsNegative: false, + computeFee: computeFeeSend) + .background(NavigationLink(destination: shortcutDestination, isActive: $shortcutSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden() + ) // shortcutDestination + .background(NavigationLink(destination: inputDestination, isActive: $buttonSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden() + ) // inputDestination + } + .frame(maxWidth: .infinity, alignment: .leading) + .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) + .navigationTitle(navTitle) + .task { + do { + let amount = try await model.getMaxPeerPushDebitAmountM(amountToTransfer.currencyStr, + scope: scopeInfo, viewHandles: true) + amountAvailable = amount + } catch { + amountAvailable = Amount.zero(currency: amountToTransfer.currencyStr) + } + } + .onAppear { + DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push()) + // if we set >> controller.frontendState = -1 << here, then becomeFirstResponder won't work! + symLog.log("❗️ \(navTitle) onAppear") + } + .onDisappear { + symLog.log("❗️ \(navTitle) onDisappear") + } + .navigationBarItems(trailing: QRButton(action: cameraAction)) + .task(id: balanceIndex + (1000 * controller.currencyTicker)) { + if let balance { + let scopeInfo = balance.scopeInfo + let currency = scopeInfo.currency + amountAvailable = balance.available + amountToTransfer.setCurrency(currency) + currencyInfo = controller.info(for: currency, controller.currencyTicker) + currencyName = currencyInfo.scope.currency + currencySymbol = currencyInfo.altUnitSymbol ?? currencyInfo.specs.name + } + } +// .task(id: amountToTransfer.value) { +// if exchange == nil { +// if let url = scopeInfo.url { +// exchange = try? await model.getExchangeByUrl(url: url) +// } +// } +// do { +// insufficient = try amountToTransfer > amountAvailable +// } catch { +// print("Yikes❗️ insufficient failed❗️") +// insufficient = true +// } +// +// if insufficient { +// announce("\(amountVoiceOver), \(insufficientLabel2)") +// } else if amountToTransfer.isZero { +// feeStr = EMPTYSTRING +// } else { +// if let ppCheck = try? await model.checkPeerPushDebitM(amountToTransfer) { +// // TODO: set from exchange +//// agePicker.setAges(ages: peerPushCheck?.ageRestrictionOptions) +// if let feeAmount = fee(ppCheck: ppCheck) { +// feeStr = feeAmount.formatted(currencyInfo, isNegative: false) +// let feeLabel = feeLabel(feeStr) +// announce("\(amountVoiceOver), \(feeLabel)") +// } else { +// feeStr = EMPTYSTRING +// announce(amountVoiceOver) +// } +// peerPushCheck = ppCheck +// } else { +// peerPushCheck = nil +// } +// } +// } + } +} +// MARK: - +#if DEBUG +fileprivate struct Preview_Content: View { + @State private var amountToPreview = Amount(currency: DEMOCURRENCY, cent: 510) + @State private var summary: String = "" + @State private var currencyInfoL: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) + @State private var noBalance: Balance? = nil + + private func checkCameraAvailable() -> Void { + // Open Camera when QR-Button was tapped + } + var body: some View { + let amount = Amount(currency: DEMOCURRENCY, cent: 1000) + let pending = Amount(currency: DEMOCURRENCY, cent: 0) + let currencyInfo = CurrencyInfo.zero(DEMOCURRENCY) + let exchange2 = Exchange(exchangeBaseUrl: ARS_EXP_EXCHANGE, + masterPub: "masterPub", + scopeInfo: currencyInfo.scope, + paytoUris: [], + tosStatus: .proposed, + exchangeEntryStatus: .ephemeral, + exchangeUpdateStatus: .ready, + ageRestrictionOptions: []) + let scopeInfo = ScopeInfo(type: .exchange, url: DEMOEXCHANGE, currency: DEMOCURRENCY) + let balance = Balance(scopeInfo: scopeInfo, + available: amount, + pendingIncoming: pending, + pendingOutgoing: pending, + flags: []) + SendAmountV(stack: CallStack("Preview"), + balances: [balance], + selectedBalance: $noBalance, + amountToTransfer: $amountToPreview, + summary: $summary, + scopeInfo: currencyInfo.scope, + cameraAction: checkCameraAvailable) + } +} + +fileprivate struct Previews: PreviewProvider { + @MainActor + struct StateContainer: View { + @StateObject private var controller = Controller.shared + var body: some View { + Preview_Content() + .environmentObject(controller) + } + } + static var previews: some View { + StateContainer() + } +} +#endif diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -321,18 +321,18 @@ extension MainView { url: DEMOEXCHANGE, currency: DEMOCURRENCY) - let sendDest = SendAmount(stack: stack.push("\(Self.className())()"), - balances: balances, - selectedBalance: $selectedBalance, // if nil shows currency picker - amountToTransfer: $amountToTransfer, // with correct currency - summary: $summary, - scopeInfo: scope, - cameraAction: cameraAction) + let sendDest = SendAmountV(stack: stack.push("\(Self.className())()"), + balances: balances, + selectedBalance: $selectedBalance, // if nil shows currency picker + amountToTransfer: $amountToTransfer, // currency needs to be updated! + summary: $summary, + scopeInfo: scope, + cameraAction: cameraAction) let requestDest = RequestPayment(stack: stack.push("\(Self.className())()"), balances: balances, selectedBalance: $selectedBalance, - amountToTransfer: $amountToTransfer, // with correct currency + amountToTransfer: $amountToTransfer, // currency needs to be updated! summary: $summary, scopeInfo: scope, cameraAction: cameraAction)