commit 426dcb2444185c38b33209fa9c1b06188047231e parent 39767097234f31ced92e17e07f468cde0bb859a2 Author: Marc Stibane <marc@taler.net> Date: Sun, 3 Nov 2024 08:51:05 +0100 scope instead of currencyInfo Diffstat:
44 files changed, 779 insertions(+), 742 deletions(-)
diff --git a/TalerWallet1/Backend/WalletBackendRequest.swift b/TalerWallet1/Backend/WalletBackendRequest.swift @@ -39,6 +39,9 @@ struct ScopeInfo: Codable, Hashable { public static func zero() -> ScopeInfo { ScopeInfo(type: .madeUp, currency: UNKNOWN) } + public static func zero(_ currency: String) -> ScopeInfo { + ScopeInfo(type: .madeUp, currency: currency) + } public static func < (lhs: ScopeInfo, rhs: ScopeInfo) -> Bool { if lhs.type == .global { if rhs.type == .global { // both global ==> alphabetic currency diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift @@ -62,7 +62,7 @@ class Controller: ObservableObject { let logger = Logger(subsystem: "net.taler.gnu", category: "Controller") let player = AVQueuePlayer() let semaphore = AsyncSemaphore(value: 1) - var currencyInfos: [CurrencyInfo] + private var currencyInfos: [ScopeInfo : CurrencyInfo] var exchanges: [Exchange] var messageForSheet: String? = nil @@ -111,7 +111,7 @@ class Controller: ObservableObject { // } backendState = .instantiated currencyTicker = 0 - currencyInfos = [] + currencyInfos = [:] exchanges = [] balances = [] // checkInternetConnection() @@ -144,13 +144,7 @@ class Controller: ObservableObject { func info(for scope: ScopeInfo) -> CurrencyInfo? { // return CurrencyInfo.euro() // Fake EUR instead of the real Currency // return CurrencyInfo.francs() // Fake CHF instead of the real Currency - for info in currencyInfos { - if info.scope == scope { - return info - } - } -// logger.log(" ❗️ no info for \(currency)") - return nil + return currencyInfos[scope] } func info(for scope: ScopeInfo, _ ticker: Int) -> CurrencyInfo { if ticker != currencyTicker { @@ -162,8 +156,8 @@ class Controller: ObservableObject { func info2(for currency: String) -> CurrencyInfo? { // return CurrencyInfo.euro() // Fake EUR instead of the real Currency // return CurrencyInfo.francs() // Fake CHF instead of the real Currency - for info in currencyInfos { - if info.scope.currency == currency { + for (scope, info) in currencyInfos { + if scope.currency == currency { return info } } @@ -178,8 +172,8 @@ class Controller: ObservableObject { } func hasInfo(for currency: String) -> Bool { - for info in currencyInfos { - if info.scope.currency == currency { + for (scope, info) in currencyInfos { + if scope.currency == currency { return true } } @@ -204,7 +198,7 @@ class Controller: ObservableObject { func updateInfo(_ scope: ScopeInfo, model: WalletModel) async { if let info = try? await model.getCurrencyInfoM(scope: scope) { - await setInfo(info) + await setInfo(info, for: scope) // logger.log(" ❗️info set for \(scope.currency)") } } @@ -212,26 +206,22 @@ class Controller: ObservableObject { func checkCurrencyInfo(for baseUrl: String, model: WalletModel) async { if let exchange = await exchange(for: baseUrl, model: model) { let scope = exchange.scopeInfo - for info in currencyInfos { - if info.scope == scope { - return // got it, nothing to do - } + if currencyInfos[scope] == nil { + await updateInfo(scope, model: model) } - await updateInfo(scope, model: model) + } else { + // Yikes❗️ TODO: error? } } /// called whenever a new currency pops up - will first load the Exchange and then currencyInfos func checkInfo(for scope: ScopeInfo, model: WalletModel) { - for info in currencyInfos { - if info.scope == scope { - return // got it, nothing to do - } - } - Task { - let exchange = await exchange(for: scope.url, model: model) - if let scope2 = exchange?.scopeInfo { - await updateInfo(scope2, model: model) + if currencyInfos[scope] == nil { + Task { + let exchange = await exchange(for: scope.url, model: model) + if let scope2 = exchange?.scopeInfo { + await updateInfo(scope2, model: model) + } } } } @@ -239,33 +229,21 @@ class Controller: ObservableObject { @MainActor func getInfo(from exchangeBaseUrl: String, model: WalletModel) async throws -> CurrencyInfo? { let exchange = try await model.getExchangeByUrl(url: exchangeBaseUrl) - let scopeInfo = exchange.scopeInfo - if let info = info(for: scopeInfo) { + let scope = exchange.scopeInfo + if let info = info(for: scope) { return info } - let info = try await model.getCurrencyInfoM(scope: scopeInfo, delay: 0) - await setInfo(info) + let info = try await model.getCurrencyInfoM(scope: scope, delay: 0) + await setInfo(info, for: scope) return info } @MainActor - func setInfo(_ newInfo: CurrencyInfo) async { + func setInfo(_ newInfo: CurrencyInfo, for scope: ScopeInfo) async { await semaphore.wait() defer { semaphore.signal() } - var replaced = false - var newInfos = currencyInfos.map { currencyInfo in - if currencyInfo.scope.currency == newInfo.scope.currency { - replaced = true - return newInfo - } else { - return currencyInfo - } - } - if !replaced { - newInfos.append(newInfo) - } - currencyInfos = newInfos + currencyInfos[scope] = newInfo currencyTicker += 1 // triggers published view update } // MARK: - diff --git a/TalerWallet1/Helper/CurrencySpecification.swift b/TalerWallet1/Helper/CurrencySpecification.swift @@ -9,19 +9,6 @@ import Foundation import SwiftUI import taler_swift -private struct CurrencyInfoKey: EnvironmentKey { - static let defaultValue: CurrencyInfo = { - return CurrencyInfo.zero(UNKNOWN) - }() -} - -extension EnvironmentValues { - var currencyInfo: CurrencyInfo { - get { self[CurrencyInfoKey.self] } - set { self[CurrencyInfoKey.self] = newValue } - } -} - extension Locale { static var preferredLanguageCode: String { guard let preferredLanguage = preferredLanguages.first, @@ -50,27 +37,34 @@ extension Locale { } extension Amount { - func formatted(_ currencyInfo: CurrencyInfo?, isNegative: Bool, useISO: Bool = false, a11y: String? = nil) -> String { + func formatted(_ currencyInfo: CurrencyInfo?, isNegative: Bool, + useISO: Bool = false, a11y: String? = nil + ) -> String { if let currencyInfo { - return currencyInfo.string(for: valueAsFloatTuple, isNegative: isNegative, useISO: useISO, a11y: a11y) + return currencyInfo.string(for: valueAsFloatTuple, isNegative: isNegative, currency: currencyStr, + useISO: useISO, a11y: a11y) } else { return valueStr } } - func formatted(_ scope: ScopeInfo, isNegative: Bool, useISO: Bool = false, a11y: String? = nil) -> String { + // this is the function to use + func formatted(_ scope: ScopeInfo?, isNegative: Bool, useISO: Bool = false, a11y: String? = nil) -> String { let controller = Controller.shared - if let currencyInfo = controller.info(for: scope) { - return self.formatted(currencyInfo, isNegative: isNegative, useISO: useISO, a11y: a11y) + if let scope { + if let currencyInfo = controller.info(for: scope) { + return self.formatted(currencyInfo, isNegative: isNegative, useISO: useISO, a11y: a11y) + } } return self.readableDescription } - func formatted(specs: CurrencySpecification?, isNegative: Bool, scope: ScopeInfo? = nil, useISO: Bool = false) -> String { + func formatted(specs: CurrencySpecification?, isNegative: Bool, scope: ScopeInfo? = nil, + useISO: Bool = false + ) -> String { if let specs { - let myScope = scope ?? ScopeInfo(type: .madeUp, currency: currencyStr) - let formatter = CurrencyFormatter.formatter(scope: myScope, specs: specs) - let currencyInfo = CurrencyInfo(scope: myScope, specs: specs, formatter: formatter) + let formatter = CurrencyFormatter.formatter(currency: currencyStr, specs: specs) + let currencyInfo = CurrencyInfo(specs: specs, formatter: formatter) return formatted(currencyInfo, isNegative: isNegative, useISO: useISO) } else if let scope { return formatted(scope, isNegative: isNegative, useISO: useISO) @@ -100,51 +94,51 @@ extension Amount { } public struct CurrencyInfo: Sendable { - let scope: ScopeInfo +// let scope: ScopeInfo let specs: CurrencySpecification let formatter: CurrencyFormatter +// public static func zero(_ currency: String) -> CurrencyInfo { public static func zero(_ currency: String) -> CurrencyInfo { - let scope = ScopeInfo(type: .global, currency: currency) let specs = CurrencySpecification(name: currency, fractionalInputDigits: 0, fractionalNormalDigits: 0, fractionalTrailingZeroDigits: 0, altUnitNames: [0 : "ヌ"]) // use `nu´ for Null - let formatter = CurrencyFormatter.formatter(scope: scope, specs: specs) - return CurrencyInfo(scope: scope, specs: specs, formatter: formatter) + let formatter = CurrencyFormatter.formatter(currency: currency, specs: specs) + return CurrencyInfo(specs: specs, formatter: formatter) } public static func euro() -> CurrencyInfo { - let currency = "EUR" - let scope = ScopeInfo(type: .global, currency: currency) + let currency = EUR_4217 let specs = CurrencySpecification(name: currency, fractionalInputDigits: 2, fractionalNormalDigits: 2, fractionalTrailingZeroDigits: 2, altUnitNames: [0 : "€"]) // ensure altUnitSymbol - let formatter = CurrencyFormatter.formatter(scope: scope, specs: specs) - print(formatter.longName ?? formatter.altUnitSymbol ?? formatter.altUnitName0 ?? formatter.currencyName ?? currency) - return CurrencyInfo(scope: scope, specs: specs, formatter: formatter) + let formatter = CurrencyFormatter.formatter(currency: currency, specs: specs) + print(formatter.name ?? formatter.altUnitSymbol ?? formatter.altUnitName0 ?? formatter.currency) + return CurrencyInfo(specs: specs, formatter: formatter) } public static func francs() -> CurrencyInfo { - let currency = "CHF" - let scope = ScopeInfo(type: .global, currency: currency) + let currency = CHF_4217 let specs = CurrencySpecification(name: currency, - fractionalInputDigits: 2, - fractionalNormalDigits: 2, - fractionalTrailingZeroDigits: 2, - altUnitNames: [0 : " CHF"]) // ensure altUnitName0 - let formatter = CurrencyFormatter.formatter(scope: scope, specs: specs) - print(formatter.longName ?? formatter.altUnitSymbol ?? formatter.altUnitName0 ?? formatter.currencyName ?? currency) - return CurrencyInfo(scope: scope, specs: specs, formatter: formatter) + fractionalInputDigits: 2, + fractionalNormalDigits: 2, + fractionalTrailingZeroDigits: 2, + altUnitNames: [0 : " CHF"]) // ensure altUnitName0 + let formatter = CurrencyFormatter.formatter(currency: currency, specs: specs) + print(formatter.name ?? formatter.altUnitSymbol ?? formatter.altUnitName0 ?? formatter.currency) + return CurrencyInfo(specs: specs, formatter: formatter) } - var currency: String { scope.currency } + var currency: String { formatter.currency } + private var altUnitName0: String? { formatter.altUnitName0 } + private var altUnitSymbol: String? { formatter.altUnitSymbol } var name: String { specs.name } - var symbol: String { altUnitSymbol ?? specs.name } // fall back to name if no symbol defined + var symbol: String { altUnitSymbol ?? name } // fall back to name if no symbol defined var hasSymbol: Bool { if symbol != name { let count = symbol.count @@ -170,9 +164,6 @@ public struct CurrencyInfo: Sendable { } } - var altUnitName0: String? { formatter.altUnitName0 } - var altUnitSymbol: String? { formatter.altUnitSymbol } - func currencyString(_ aString: String, useISO: Bool = false) -> String { if !useISO { if let aSymbol = altUnitSymbol { @@ -188,9 +179,10 @@ public struct CurrencyInfo: Sendable { return spacedString3 } } - if let currencyName = formatter.currencyName { - let spacedName = formatter.leadingCurrencySymbol ? currencyName + " " - : " " + currencyName + let currency = formatter.currency + if currency.count > 0 { + let spacedName = formatter.leadingCurrencySymbol ? currency + " " + : " " + currency let spacedString1 = aString.replacingOccurrences(of: formatter.currencySymbol, with: spacedName) let spacedString2 = spacedString1.replacingOccurrences(of: formatter.currencyCode, with: spacedName) let spacedString3 = spacedString2.replacingOccurrences(of: " ", with: " ") // ensure we have only 1 space @@ -200,7 +192,8 @@ public struct CurrencyInfo: Sendable { } // TODO: use valueAsDecimalTuple instead of valueAsFloatTuple - func string(for valueTuple: (Double, Double), isNegative: Bool, useISO: Bool = false, a11y: String? = nil) -> String { + func string(for valueTuple: (Double, Double), isNegative: Bool, currency: String, + useISO: Bool = false, a11y: String? = nil) -> String { formatter.setUseISO(useISO) let (integer, fraction) = valueTuple if let integerStr = formatter.string(for: isNegative ? -integer : integer) { @@ -235,7 +228,7 @@ public struct CurrencyInfo: Sendable { } // print(resultStr) if let a11y { - resultStr.replacingOccurrences(of: decimalSeparator, with: a11y) + resultStr = resultStr.replacingOccurrences(of: decimalSeparator, with: a11y) } return currencyString(resultStr, useISO: useISO) } @@ -248,7 +241,8 @@ public struct CurrencyInfo: Sendable { // if we arrive here then we do not get a formatted string for integerStr. Yikes! // TODO: log.error(formatter doesn't work) // we need to format ourselves - var currencyName = scope.currency +// var currencyName = scope.currency + var currencyName = currency if !useISO { if let altUnitName0 { currencyName = altUnitName0 @@ -290,14 +284,14 @@ public struct CurrencySpecification: Codable, Sendable { public class CurrencyFormatter: NumberFormatter { - var longName: String? + var name: String? var altUnitName0: String? // specs.altUnitNames[0] should have either the name var altUnitSymbol: String? // specs.altUnitNames[0] should have the Symbol ($,€,¥) - var currencyName: String? + var currency: String var leadingCurrencySymbol: Bool /// factory - static func formatter(scope: ScopeInfo, specs: CurrencySpecification) -> CurrencyFormatter { + static func formatter(currency: String, specs: CurrencySpecification) -> CurrencyFormatter { let formatter = CurrencyFormatter() if let altUnitNameZero = specs.altUnitNames?[0] { if altUnitNameZero.hasPrefix(" ") { @@ -306,32 +300,32 @@ public class CurrencyFormatter: NumberFormatter { formatter.altUnitSymbol = altUnitNameZero } } - formatter.longName = specs.name - formatter.currencyName = scope.currency -// formatter.setCode(to: "EUR") + formatter.name = specs.name + formatter.currency = currency +// formatter.setCode(to: EUR_4217) // formatter.setSymbol(to: "€") formatter.setMinimumFractionDigits(specs.fractionalTrailingZeroDigits) return formatter } public override init() { - self.longName = nil + self.name = nil self.altUnitName0 = nil self.altUnitSymbol = nil - self.currencyName = nil + self.currency = EMPTYSTRING self.leadingCurrencySymbol = false super.init() -// self.currencyCode = "EUR" +// self.currencyCode = EUR_4217 // self.currencySymbol = "€" self.locale = Locale.autoupdatingCurrent if #available(iOS 16.0, *) { let currency = self.locale.currency if let currencyCode = currency?.identifier { - self.longName = self.locale.localizedString(forCurrencyCode: currencyCode) + self.name = self.locale.localizedString(forCurrencyCode: currencyCode) } } else { if let currencyCode = self.locale.currencyCode { - self.longName = self.locale.localizedString(forCurrencyCode: currencyCode) + self.name = self.locale.localizedString(forCurrencyCode: currencyCode) } } self.usesGroupingSeparator = true @@ -381,13 +375,12 @@ public class CurrencyFormatter: NumberFormatter { #if DEBUG func PreviewCurrencyInfo(_ currency: String, digits: Int) -> CurrencyInfo { let unitName = digits == 0 ? "テ" : "ク" // do not use real currency symbols like "¥" : "€" - let scope = ScopeInfo(type: .global, currency: currency) let specs = CurrencySpecification(name: currency, - fractionalInputDigits: digits, - fractionalNormalDigits: digits, - fractionalTrailingZeroDigits: digits, - altUnitNames: [0 : unitName]) - let previewFormatter = CurrencyFormatter.formatter(scope: scope, specs: specs) - return CurrencyInfo(scope: scope, specs: specs, formatter: previewFormatter) + fractionalInputDigits: digits, + fractionalNormalDigits: digits, + fractionalTrailingZeroDigits: digits, + altUnitNames: [0 : unitName]) + let previewFormatter = CurrencyFormatter.formatter(currency: currency, specs: specs) + return CurrencyInfo(specs: specs, formatter: previewFormatter) } #endif diff --git a/TalerWallet1/Model/Model+Exchange.swift b/TalerWallet1/Model/Model+Exchange.swift @@ -226,8 +226,8 @@ extension WalletModel { async throws -> CurrencyInfo { let request = GetCurrencySpecification(scope: scope) let response = try await sendRequest(request, ASYNCDELAY + delay, viewHandles: viewHandles) - return CurrencyInfo(scope: scope, specs: response.currencySpecification, - formatter: CurrencyFormatter.formatter(scope: scope, - specs: response.currencySpecification)) + return CurrencyInfo(specs: response.currencySpecification, + formatter: CurrencyFormatter.formatter(currency: scope.currency, + specs: response.currencySpecification)) } } diff --git a/TalerWallet1/Model/Model+P2P.swift b/TalerWallet1/Model/Model+P2P.swift @@ -182,9 +182,10 @@ struct PreparePeerPushCreditResponse: Codable { let contractTerms: PeerContractTerms let amountRaw: Amount let amountEffective: Amount - let exchangeBaseUrl: String - // the dialog transaction is already created in the local DB - must either accept or delete + // TODO: the dialog transaction is already created in the local DB - must either accept or delete let transactionId: String + let exchangeBaseUrl: String // need (exchange.tosStatus == .accepted) for incoming coins + let scopeInfo: ScopeInfo } fileprivate struct PreparePeerPushCredit: WalletBackendFormattedRequest { typealias Response = PreparePeerPushCreditResponse @@ -232,8 +233,10 @@ struct PreparePeerPullDebitResponse: Codable { let contractTerms: PeerContractTerms let amountRaw: Amount let amountEffective: Amount - // the dialog transaction is already created in the local DB - must either accept or delete + // TODO: the dialog transaction is already created in the local DB - must either accept or delete let transactionId: String +// let exchangeBaseUrl: String use scope instead + let scopeInfo: ScopeInfo } fileprivate struct PreparePeerPullDebit: WalletBackendFormattedRequest { typealias Response = PreparePeerPullDebitResponse diff --git a/TalerWallet1/Model/Model+Payment.swift b/TalerWallet1/Model/Model+Payment.swift @@ -141,13 +141,13 @@ struct ExchangeFeeGapEstimate: Codable { struct PreparePayResult: Codable { let status: PreparePayResultType let transactionId: String + let contractTerms: MerchantContractTerms + let scopes: [ScopeInfo] let amountRaw: Amount let amountEffective: Amount? // only if status != insufficientBalance let paid: Bool? // only if status == alreadyConfirmed let talerUri: String? let balanceDetails: PayMerchantInsufficientBalanceDetails? // only if status == insufficientBalance - let contractTermsHash: String? // only if status != insufficientBalance - let contractTerms: MerchantContractTerms } /// A request to get an exchange's payment contract terms. fileprivate struct PreparePayForUri: WalletBackendFormattedRequest { @@ -180,22 +180,24 @@ fileprivate struct PreparePayForTemplateRequest: WalletBackendFormattedRequest { } // MARK: - struct TemplateContractDetails: Codable { - let summary: String? // Human-readable short summary of the contract. Fixed if this field exists, editable if nil - let currency: String? - let amount: Amount? // Total amount payable. Fixed if this field exists, editable if nil + let summary: String? // Human-readable short summary of the contract. Editable if nil + let currency: String? // specify currency when amount is nil - unspecified if nil + let amount: Amount? // Total amount payable. Fixed if this field exists, editable if nil + let scopeInfo: ScopeInfo? let minimumAge: Int let payDuration: RelativeTime enum CodingKeys: String, CodingKey { case summary, currency, amount + case scopeInfo case minimumAge = "minimum_age" case payDuration = "pay_duration" } } struct TemplateContractDetailsDefaults: Codable { - let summary: String? // Default 'Human-readable short summary' if editable, or empty if nil. - let currency: String? - let amount: Amount? // Default amount if editable, or unspecified if nil. + let summary: String? // Default 'Human-readable summary' when editable: empty if nil + let currency: String? // Default currency when unspecified: any if nil (e.g. donations) + let amount: Amount? // Default amount when editable: unspecified if nil } struct TalerMerchantTemplateDetails: Codable { let templateContract: TemplateContractDetails diff --git a/TalerWallet1/Model/Model+Withdraw.swift b/TalerWallet1/Model/Model+Withdraw.swift @@ -61,6 +61,7 @@ struct WithdrawUriInfoResponse: Decodable { var maxAmount: Amount? // limit how much the user may withdraw var wireFee: Amount? var defaultExchangeBaseUrl: String? // if nil then use possibleExchanges + var editableExchange: Bool // TODO: what for? var possibleExchanges: [Exchange] // TODO: query these for fees? } /// A request to get an exchange's withdrawal details. @@ -93,7 +94,8 @@ fileprivate struct PrepareWithdrawExchange: WalletBackendFormattedRequest { } // MARK: - /// The result from getWithdrawalDetailsForAmount -struct WithdrawalAmountDetails: Decodable { +struct WithdrawalDetailsForAmount: Decodable { + var exchangeBaseUrl: String var amountRaw: Amount // Amount that the user will transfer to the exchange var amountEffective: Amount // Amount that will be added to the user's wallet balance var numCoins: Int? // Number of coins this amountEffective will create @@ -103,16 +105,19 @@ struct WithdrawalAmountDetails: Decodable { } /// A request to get an exchange's withdrawal details. fileprivate struct GetWithdrawalDetailsForAmount: WalletBackendFormattedRequest { - typealias Response = WithdrawalAmountDetails + typealias Response = WithdrawalDetailsForAmount func operation() -> String { "getWithdrawalDetailsForAmount" } - func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount, - clientCancellationId: "cancel") } - - var exchangeBaseUrl: String + func args() -> Args { Args(amount: amount, + exchangeBaseUrl: baseUrl, + restrictScope: scope, + clientCancellationId: "cancel") } var amount: Amount + var baseUrl: String? + var scope: ScopeInfo? struct Args: Encodable { - var exchangeBaseUrl: String var amount: Amount + var exchangeBaseUrl: String? // needed for b-i-withdrawals + var restrictScope: ScopeInfo? // only if exchangeBaseUrl is nil var clientCancellationId: String? } } @@ -196,15 +201,15 @@ struct AcceptManualWithdrawalResult: Decodable { fileprivate struct AcceptManualWithdrawal: WalletBackendFormattedRequest { typealias Response = AcceptManualWithdrawalResult func operation() -> String { "acceptManualWithdrawal" } - func args() -> Args { Args(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) } + func args() -> Args { Args(amount: amount, exchangeBaseUrl: baseUrl, restrictAge: restrictAge) } - var exchangeBaseUrl: String var amount: Amount + var baseUrl: String var restrictAge: Int? struct Args: Encodable { - var exchangeBaseUrl: String var amount: Amount + var exchangeBaseUrl: String var restrictAge: Int? } } @@ -247,11 +252,10 @@ extension WalletModel { return response } @MainActor // M for MainActor - func getWithdrawalDetailsForAmountM(_ exchangeBaseUrl: String, amount: Amount, + func getWithdrawalDetailsForAmountM(_ amount: Amount, baseUrl: String?, scope: ScopeInfo?, viewHandles: Bool = false) - async throws -> WithdrawalAmountDetails { - let request = GetWithdrawalDetailsForAmount(exchangeBaseUrl: exchangeBaseUrl, - amount: amount) + async throws -> WithdrawalDetailsForAmount { + let request = GetWithdrawalDetailsForAmount(amount: amount, baseUrl: baseUrl, scope: scope) let response = try await sendRequest(request, ASYNCDELAY, viewHandles: viewHandles) return response } @@ -279,10 +283,10 @@ extension WalletModel { let response = try await sendRequest(request, ASYNCDELAY, viewHandles: viewHandles) return response } - @MainActor // M for MainActor - func sendAcceptManualWithdrawalM(_ exchangeBaseUrl: String, amount: Amount, restrictAge: Int?, viewHandles: Bool = false) + @MainActor // M for MainActor + func acceptManualWithdrawalM(_ amount: Amount, baseUrl: String, restrictAge: Int?, viewHandles: Bool = false) async throws -> AcceptManualWithdrawalResult? { - let request = AcceptManualWithdrawal(exchangeBaseUrl: exchangeBaseUrl, amount: amount, restrictAge: restrictAge) + let request = AcceptManualWithdrawal(amount: amount, baseUrl: baseUrl, restrictAge: restrictAge) let response = try await sendRequest(request, ASYNCDELAY, viewHandles: viewHandles) return response } diff --git a/TalerWallet1/Model/Transaction.swift b/TalerWallet1/Model/Transaction.swift @@ -291,6 +291,7 @@ struct TransactionCommon: Decodable, Sendable { var amountRaw: Amount var transactionId: String var timestamp: Timestamp + var scopes: [ScopeInfo] var txActions: [TxAction] var kycUrl: String? diff --git a/TalerWallet1/Views/Actions/Banking/DepositAmountV.swift b/TalerWallet1/Views/Actions/Banking/DepositAmountV.swift @@ -22,8 +22,6 @@ struct DepositAmountV: View { @State private var balanceIndex = 0 @State private var balance: Balance? = nil // nil only when balances == [] - @State private var currencyInfo = CurrencyInfo.zero(UNKNOWN) - @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING) // GetMaxPeerPushAmount @@ -53,9 +51,7 @@ struct DepositAmountV: View { amountLastUsed: $amountLastUsed, amountToTransfer: $amountToTransfer, amountAvailable: amountAvailable, - currencyInfo: currencyInfo, paytoUri: paytoUri) - .environment(\.currencyInfo, currencyInfo) } // ScrollView .navigationTitle(navTitle) .frame(maxWidth: .infinity, alignment: .leading) @@ -90,7 +86,6 @@ struct DepositAmountV: View { if let balance { let scopeInfo = balance.scopeInfo amountToTransfer.setCurrency(scopeInfo.currency) - currencyInfo = controller.info(for: scopeInfo, controller.currencyTicker) do { amountAvailable = try await model.getMaxPeerPushDebitAmountM(scopeInfo) } catch { @@ -121,7 +116,6 @@ struct DepositAmountContent: View { @Binding var amountLastUsed: Amount @Binding var amountToTransfer: Amount let amountAvailable: Amount - let currencyInfo: CurrencyInfo let paytoUri: String? @EnvironmentObject private var controller: Controller @@ -174,12 +168,15 @@ struct DepositAmountContent: View { } private func buttonTitle(_ amount: Amount) -> String { - let amountWithCurrency = amount.formatted(currencyInfo, isNegative: false, useISO: true) - return String(localized: "Deposit \(amountWithCurrency)", comment: "Button: amount with currency") + if let balance { + let amountWithCurrency = amount.formatted(balance.scopeInfo, isNegative: false, useISO: true) + return String(localized: "Deposit \(amountWithCurrency)", comment: "Button: amount with currency") + } + return subjectTitle(amount) // should never happen } private func subjectTitle(_ amount: Amount) -> String { - let amountStr = amount.formatted(currencyInfo, isNegative: false) +// let amountStr = amount.formatted(scopeInfo, isNegative: false) return String(localized: "NavTitle_Deposit_AmountStr", defaultValue: "Deposit", comment: "NavTitle: Deposit") // defaultValue: "Deposit \(amountStr)", comment: "NavTitle: Deposit 'amountStr'") @@ -197,9 +194,10 @@ struct DepositAmountContent: View { } else { Group { if let balance { let scopeInfo = balance.scopeInfo - let availableStr = amountAvailable.formatted(currencyInfo, isNegative: false) + let availableStr = amountAvailable.formatted(scopeInfo, isNegative: false) -// let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) + let currencyInfo = controller.info(for: scopeInfo, controller.currencyTicker) +// let amountVoiceOver = amountToTransfer.formatted(scopeInfo, isNegative: false) let insufficientLabel = String(localized: "You don't have enough \(currencyInfo.specs.name).") // let insufficientLabel2 = String(localized: "but you only have \(available) to deposit.") @@ -208,13 +206,13 @@ struct DepositAmountContent: View { Text("Available:\t\(availableStr)") .talerFont(.title3) .padding(.bottom, 2) - CurrencyInputView(currencyInfo: currencyInfo, - amount: $amountToTransfer, - amountLastUsed: amountLastUsed, - available: nil, // amountAvailable, - title: minimalistic ? String(localized: "Amount:") - : String(localized: "Amount to deposit:"), - shortcutAction: nil) + CurrencyInputView(scope: scopeInfo, + amount: $amountToTransfer, + amountLastUsed: amountLastUsed, + available: nil, // amountAvailable, + title: minimalistic ? String(localized: "Amount:") + : String(localized: "Amount to deposit:"), + shortcutAction: nil) Text(insufficient ? insufficientLabel : feeLabel(feeStr)) @@ -266,7 +264,7 @@ struct DepositAmountContent: View { // } else if let paytoUri { // if let ppCheck = try? await model.checkDepositM(paytoUri, amount: amountToTransfer) { // if let feeAmount = fee(ppCheck: ppCheck) { -// feeStr = feeAmount.formatted(currencyInfo, isNegative: false) +// feeStr = feeAmount.formatted(scopeInfo, isNegative: false) // let feeLabel = feeLabel(feeStr) // announce("\(amountVoiceOver), \(feeLabel)") // } else { diff --git a/TalerWallet1/Views/Actions/Banking/DepositIbanV.swift b/TalerWallet1/Views/Actions/Banking/DepositIbanV.swift @@ -14,13 +14,11 @@ struct DepositIbanV: View { private let symLog = SymLogV(0) let stack: CallStack @Binding var selectedBalance: Balance? -// @Binding var currencyInfo: CurrencyInfo - let feeLabel: String? - let feeIsNotZero: Bool? // nil = no fees at all, false = no fee for this tx + @Binding var amountLastUsed: Amount + // let amountAvailable: Amount? // @Binding var depositIBAN: String // @Binding var accountHolder: String - @Binding var amountLastUsed: Amount @EnvironmentObject private var controller: Controller @EnvironmentObject private var model: WalletModel @@ -48,10 +46,8 @@ struct DepositIbanV: View { private var subjectTitle: String { return String(localized: "NavTitle_Deposit_AmountStr", -// defaultValue: "Deposit \(currencySymbol)", defaultValue: "Deposit IBAN", comment: "NavTitle: Deposit") -// defaultValue: "Deposit \(amountStr)", comment: "NavTitle: Deposit 'amountStr'") } var body: some View { @@ -64,22 +60,12 @@ struct DepositIbanV: View { amountLastUsed: $amountLastUsed, paytoUri: paytoUri) ScrollView { VStack (alignment: .leading, spacing: 6) { - if let feeIsNotZero { // don't show fee if nil - let label = feeLabel ?? myFeeLabel - if label.count > 0 { - Text(label) - .frame(maxWidth: .infinity, alignment: .trailing) - .foregroundColor(feeIsNotZero ? .red - : WalletColors().secondary(colorScheme, colorSchemeContrast)) - .talerFont(.body) - } - } if !minimalistic { Text("Account holder:") .talerFont(.title3) .accessibilityAddTraits(.isHeader) .accessibilityRemoveTraits(.isStaticText) - .padding(.top) +// .padding(.top) } TextField(minimalistic ? "Account holder" : EMPTYSTRING, text: $accountHolder) .focused($isFocused) @@ -87,7 +73,7 @@ struct DepositIbanV: View { .foregroundColor(WalletColors().fieldForeground) // text color .background(WalletColors().fieldBackground) .textFieldStyle(.roundedBorder) - .padding(minimalistic ? .bottom : .vertical) +// .padding(minimalistic ? .bottom : .vertical) .onAppear { if !UIAccessibility.isVoiceOverRunning { symLog.log("dispatching kbd...") @@ -103,14 +89,14 @@ struct DepositIbanV: View { .talerFont(.title3) .accessibilityAddTraits(.isHeader) .accessibilityRemoveTraits(.isStaticText) - .padding(.top) +// .padding(.top) } TextField(minimalistic ? "IBAN" : EMPTYSTRING, text: $depositIBAN) .talerFont(.title2) .foregroundColor(WalletColors().fieldForeground) // text color .background(WalletColors().fieldBackground) .textFieldStyle(.roundedBorder) - .padding(minimalistic ? .bottom : .vertical) +// .padding(minimalistic ? .bottom : .vertical) let disabled = (accountHolder.count < 1) || paytoUri == nil // TODO: check amountAvailable NavigationLink(destination: destination) { diff --git a/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift b/TalerWallet1/Views/Actions/Banking/ManualWithdraw.swift @@ -23,6 +23,7 @@ struct ManualWithdraw: View { @State private var balanceIndex = 0 @State private var balance: Balance? = nil // nil only when balances == [] @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) + @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used func navTitle(_ currency: String, _ condition: Bool = false) -> String { condition ? String(localized: "NavTitle_Withdraw_Currency)", @@ -38,14 +39,13 @@ struct ManualWithdraw: View { let _ = Self._printChanges() #endif let currencySymbol = currencyInfo.symbol - let navA11y = navTitle(currencyInfo.name) + let navA11y = navTitle(currencyInfo.name) // always include currency for a11y let navTitle = navTitle(currencySymbol, currencyInfo.hasSymbol) let count = controller.balances.count let _ = symLog.log("count = \(count)") let scrollView = ScrollView { if count > 0 { - ScopePicker(value: $balanceIndex, - onlyNonZero: false) + ScopePicker(value: $balanceIndex, onlyNonZero: false) { index in balanceIndex = index balance = controller.balances[index] @@ -53,11 +53,12 @@ struct ManualWithdraw: View { .padding(.horizontal) .padding(.bottom, 4) } - ManualWithdrawContent(stack: stack.push(), - currencyInfo: $currencyInfo, - balance: $balance, - balanceIndex: $balanceIndex, - amountLastUsed: $amountLastUsed) + if let balance { + ManualWithdrawContent(stack: stack.push(), + scope: balance.scopeInfo, + amountLastUsed: $amountLastUsed, + amountToTransfer: $amountToTransfer) + } } // ScrollView .navigationTitle(navTitle) .frame(maxWidth: .infinity, alignment: .leading) @@ -83,6 +84,15 @@ struct ManualWithdraw: View { balance = (count > 0) ? controller.balances[0] : nil } } + .task(id: balanceIndex + (1000 * controller.currencyTicker)) { + // runs whenever the user changes the exchange via ScopePicker, or on new currencyInfo + symLog.log("❗️ task \(balanceIndex)") + if let balance { + let scopeInfo = balance.scopeInfo + amountToTransfer.setCurrency(scopeInfo.currency) +// currencyInfo = controller.info(for: scopeInfo, controller.currencyTicker) + } + } if #available(iOS 16.0, *) { if #available(iOS 16.4, *) { @@ -100,22 +110,22 @@ struct ManualWithdraw: View { struct ManualWithdrawContent: View { private let symLog = SymLogV() let stack: CallStack - @Binding var currencyInfo: CurrencyInfo - @Binding var balance: Balance? - @Binding var balanceIndex: Int + let scope: ScopeInfo @Binding var amountLastUsed: Amount + @Binding var amountToTransfer: Amount @EnvironmentObject private var controller: Controller @EnvironmentObject private var model: WalletModel @AppStorage("minimalistic") var minimalistic: Bool = false - @State private var withdrawalAmountDetails: WithdrawalAmountDetails? = nil - @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used + @State private var detailsForAmount: WithdrawalDetailsForAmount? = nil // @State var ageMenuList: [Int] = [] // @State var selectedAge = 0 + @State private var exchanges: [Exchange] = [] @State private var exchange: Exchange? = nil + private func exchangeVia(_ baseURL: String?) -> String? { if let baseURL { return String(localized: "via \(baseURL.trimURL)") @@ -123,100 +133,96 @@ struct ManualWithdrawContent: View { return nil } + private func computeFee(_ amount: Amount) async -> ComputeFeeResult? { + if amount.isZero { + return ComputeFeeResult.zero() + } + do { + let details = try await model.getWithdrawalDetailsForAmountM(amount, baseUrl: nil, scope: scope, + viewHandles: true) + detailsForAmount = details +// agePicker.setAges(ages: detailsForAmount?.ageRestrictionOptions) + } catch WalletBackendError.walletCoreError(let walletBackendResponseError) { + symLog.log(walletBackendResponseError?.hint) + // TODO: ignore WALLET_CORE_REQUEST_CANCELLED but handle all others + // Passing non-nil to clientCancellationId will throw WALLET_CORE_REQUEST_CANCELLED + // when calling getWithdrawalDetailsForAmount again before the last call returned. + // Since amountToTransfer changed and we don't need the old fee anymore, we just + // ignore it and do nothing. + } catch { + symLog.log(error.localizedDescription) + detailsForAmount = nil + } + return nil + } // computeFee + + private func withdrawButtonTitle(_ currency: String) -> String { + switch currency { + case CHF_4217: + String(localized: "WITHDRAW_CONFIRM_BUTTONTITLE_CHF", defaultValue: "Confirm Withdrawal") + case EUR_4217: + String(localized: "WITHDRAW_CONFIRM_BUTTONTITLE_EUR", defaultValue: "Confirm Withdrawal") + default: + String(localized: "WITHDRAW_CONFIRM_BUTTONTITLE", defaultValue: "Confirm Withdrawal") + } + } 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 coinData = CoinData(details: withdrawalAmountDetails) - Group { - if let balance { - let scopeInfo = balance.scopeInfo - let currency = scopeInfo.currency -// let currencyInfo = controller.info(for: currency, controller.currencyTicker) - let baseURL = exchange?.exchangeBaseUrl ?? scopeInfo.url // nil for global currency - + if let detailsForAmount { + Group { + let coinData = CoinData(details: detailsForAmount) + let currency = detailsForAmount.scopeInfo.currency + let baseURL = detailsForAmount.exchangeBaseUrl // let agePicker = AgePicker(ageMenuList: $ageMenuList, selectedAge: $selectedAge) - // let restrictAge: Int? = (selectedAge == 0) ? nil // : selectedAge // let _ = print(selectedAge, restrictAge) let destination = ManualWithdrawDone(stack: stack.push(), - currencyInfo: $currencyInfo, - exchange: nil, // exchange, - balance: balance, + scope: detailsForAmount.scopeInfo, + baseURL: detailsForAmount.exchangeBaseUrl, amountToTransfer: amountToTransfer) // restrictAge: restrictAge) let disabled = amountToTransfer.isZero || coinData.invalid || coinData.tooMany let tosAccepted = (exchange?.tosStatus == .accepted) ?? false - VStack(alignment: .trailing) { - Text(exchangeVia(baseURL) ?? currency) - .multilineTextAlignment(.center) - .talerFont(.body) if tosAccepted { - CurrencyInputView(currencyInfo: currencyInfo, - amount: $amountToTransfer, - amountLastUsed: amountLastUsed, - available: nil, - title: minimalistic ? String(localized: "Amount:") - : String(localized: "Amount to withdraw:"), - shortcutAction: nil) + CurrencyInputView(scope: scope, + amount: $amountToTransfer, + amountLastUsed: amountLastUsed, + available: nil, + title: minimalistic ? String(localized: "Amount:") + : String(localized: "Amount to withdraw:"), + shortcutAction: nil) .padding(.top) - QuiteSomeCoins(currencyInfo: $currencyInfo, -// amountEffective: withdrawalAmountDetails?.amountEffective, - currency: currency, - coinData: coinData, - shouldShowFee: true, // TODO: set to false if we never charge withdrawal fees - feeIsNegative: true) + .task(id: amountToTransfer.value) { // re-run this whenever amountToTransfer changes + await computeFee(amountToTransfer) + } + QuiteSomeCoins(scope: scope, + coinData: coinData, + shouldShowFee: true, // TODO: set to false if we never charge withdrawal fees + feeIsNegative: true) // agePicker - NavigationLink(destination: destination) { - Text("Confirm Withdrawal") // VIEW_WITHDRAW_ACCEPT + NavigationLink(destination: destination) { // VIEW_WITHDRAW_ACCEPT + Text(withdrawButtonTitle(currency)) } .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled)) .disabled(disabled) .padding(.top) - } else if let baseURL { + } else { ToSButtonView(stack: stack.push(), exchangeBaseUrl: baseURL, - viewID: VIEW_WITHDRAW_TOS, // 31 WithdrawTOSView TODO: YIKES might be withdraw-exchange + viewID: VIEW_WITHDRAW_TOS, // 31 WithdrawTOSView TODO: YIKES might be withdraw-exchange p2p: false, acceptAction: nil) .padding(.top) } - }.padding(.horizontal) // VStack - } else { // got no balance yet - Text("No balance. There seems to be a problem with the database...") - } - } // Group -// .task(id: amountToTransfer.value) { // re-run this whenever amountToTransfer changes -// if let exchangeBaseUrl = scopeInfo?.url { -// symLog.log("getExchangeByUrl(\(exchangeBaseUrl))") -// if exchange == nil || exchange?.tosStatus != .accepted { -// exchange = try? await model.getExchangeByUrl(url: exchangeBaseUrl) -// } -// if !amountToTransfer.isZero { -// do { -// let details = try await model.getWithdrawalDetailsForAmountM(exchangeBaseUrl, -// amount: amountToTransfer, -// viewHandles: true) -// withdrawalAmountDetails = details -//// agePicker.setAges(ages: withdrawalAmountDetails?.ageRestrictionOptions) -// } catch WalletBackendError.walletCoreError(let walletBackendResponseError) { -// symLog.log(walletBackendResponseError?.hint) -// // TODO: ignore WALLET_CORE_REQUEST_CANCELLED but handle all others -// // Passing non-nil to clientCancellationId will throw WALLET_CORE_REQUEST_CANCELLED -// // when calling getWithdrawalDetailsForAmount again before the last call returned. -// // Since amountToTransfer changed and we don't need the old fee anymore, we just -// // ignore it and do nothing. -// } catch { -// symLog.log(error.localizedDescription) -// withdrawalAmountDetails = nil -// } -// } -// } -// } + } // Group + .padding(.horizontal) + } } } // MARK: - diff --git a/TalerWallet1/Views/Actions/Banking/ManualWithdrawDone.swift b/TalerWallet1/Views/Actions/Banking/ManualWithdrawDone.swift @@ -12,10 +12,9 @@ import SymLog struct ManualWithdrawDone: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo // TODO: use data from tx itself - let exchange: Exchange? - let balance: Balance? + let baseURL: String let amountToTransfer: Amount // let restrictAge: Int? @@ -35,11 +34,10 @@ struct ManualWithdrawDone: View { let _ = Self._printChanges() let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif - let baseURL = balance?.scopeInfo.url ?? exchange?.exchangeBaseUrl ?? EMPTYSTRING Group { if let transactionId { TransactionSummaryV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, // TODO: use data from tx itself transactionId: transactionId, reloadAction: reloadOneAction, navTitle: navTitle, @@ -65,9 +63,9 @@ struct ManualWithdrawDone: View { DebugViewC.shared.setViewID(VIEW_WITHDRAW_ACCEPT, stack: stack.push()) }.task { if transactionId == nil { - if let result = try? await model.sendAcceptManualWithdrawalM(baseURL, - amount: amountToTransfer, - restrictAge: 0) + if let result = try? await model.acceptManualWithdrawalM(amountToTransfer, + baseUrl: baseURL, + restrictAge: 0) { transactionId = result.transactionId } } } @@ -80,7 +78,7 @@ fileprivate struct ManualWithdrawDone_Previews: PreviewProvider { @MainActor struct BindingViewContainer : View { @State private var amountToTransfer = Amount(currency: LONGCURRENCY, cent: 510) - @State private var currencyInfoD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) + @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) var body: some View { let scopeInfo = ScopeInfo(type: .exchange, currency: LONGCURRENCY) @@ -93,9 +91,8 @@ fileprivate struct ManualWithdrawDone_Previews: PreviewProvider { exchangeUpdateStatus: .initial, ageRestrictionOptions: []) ManualWithdrawDone(stack: CallStack("Preview"), - currencyInfo: $currencyInfoD, - exchange: exchange, - balance: nil, + scope: scopeInfo, + baseURL: DEMOEXCHANGE, amountToTransfer: amountToTransfer) } } diff --git a/TalerWallet1/Views/Actions/Banking/QuiteSomeCoins.swift b/TalerWallet1/Views/Actions/Banking/QuiteSomeCoins.swift @@ -18,11 +18,11 @@ struct CoinData { var tooMany: Bool { numCoins > 999 } let fee: Amount? - func feeLabel(_ currencyInfo: CurrencyInfo, feeZero: String?, isNegative: Bool = false) -> String { + func feeLabel(_ scope: ScopeInfo?, feeZero: String?, isNegative: Bool = false) -> String { return if let fee { fee.isZero ? feeZero ?? EMPTYSTRING // String(localized: "No withdrawal fee") - : isNegative ? String(localized: "- \(fee.formatted(currencyInfo, isNegative: false)) fee") - : String(localized: "+ \(fee.formatted(currencyInfo, isNegative: false)) fee") + : isNegative ? String(localized: "- \(fee.formatted(scope, isNegative: false)) fee") + : String(localized: "+ \(fee.formatted(scope, isNegative: false)) fee") } else { EMPTYSTRING } @@ -34,7 +34,7 @@ extension CoinData { self.init(numCoins: numCoins ?? -1, fee: fee) // either the number of coins, or unknown } - init(details: WithdrawalAmountDetails?) { + init(details: WithdrawalDetailsForAmount?) { do { if let details { // Incoming: fee = raw - effective @@ -63,21 +63,18 @@ extension CoinData { // MARK: - struct QuiteSomeCoins: View { private let symLog = SymLogV(0) - @Binding var currencyInfo: CurrencyInfo -// let amountEffective: Amount? - let currency: String + let scope: ScopeInfo? let coinData: CoinData let shouldShowFee: Bool let feeIsNegative: Bool var body: some View { - let isError = coinData.tooMany // || coinData.invalid + let isError = coinData.tooMany // || coinData.invalid let showFee = shouldShowFee && !isError if showFee { if let fee = coinData.fee { - Text(coinData.feeLabel(currencyInfo, - feeZero: String(localized: "No withdrawal fee"), - isNegative: feeIsNegative)) + Text(coinData.feeLabel(scope, feeZero: String(localized: "No withdrawal fee"), + isNegative: feeIsNegative)) .foregroundColor(.primary) .talerFont(.body) } diff --git a/TalerWallet1/Views/Actions/Peer2peer/P2PReadyV.swift b/TalerWallet1/Views/Actions/Peer2peer/P2PReadyV.swift @@ -13,7 +13,6 @@ import SymLog struct P2PReadyV: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo let scope: ScopeInfo let summary: String let expireDays: UInt @@ -44,16 +43,16 @@ struct P2PReadyV: View { Group { if let transactionId { TransactionSummaryV(stack: stack.push(), - currencyInfo: $currencyInfo, - transactionId: transactionId, - reloadAction: reloadOneAction, - navTitle: navTitle, - hasDone: true, - abortAction: nil, - deleteAction: nil, - failAction: nil, - suspendAction: nil, - resumeAction: nil) + scope: scope, + transactionId: transactionId, + reloadAction: reloadOneAction, + navTitle: navTitle, + hasDone: true, + abortAction: nil, + deleteAction: nil, + failAction: nil, + suspendAction: nil, + resumeAction: nil) .navigationBarBackButtonHidden(true) .interactiveDismissDisabled() // can only use "Done" button to dismiss .safeAreaInset(edge: .bottom) { diff --git a/TalerWallet1/Views/Actions/Peer2peer/P2PSubjectV.swift b/TalerWallet1/Views/Actions/Peer2peer/P2PSubjectV.swift @@ -22,7 +22,6 @@ struct P2PSubjectV: View { private let symLog = SymLogV(0) let stack: CallStack let scope: ScopeInfo - @Binding var currencyInfo: CurrencyInfo let feeLabel: String? let feeIsNotZero: Bool? // nil = no fees at all, false = no fee for this tx let outgoing: Bool @@ -39,16 +38,16 @@ struct P2PSubjectV: View { @State private var transactionStarted: Bool = false @FocusState private var isFocused: Bool - private func buttonTitle(_ amount: Amount, _ currencyInfo: CurrencyInfo) -> String { - let amountWithCurrency = amount.formatted(currencyInfo, isNegative: false, useISO: true) + private func buttonTitle(_ amount: Amount) -> String { + let amountWithCurrency = amount.formatted(scope, isNegative: false, useISO: true) return outgoing ? String(localized: "Send \(amountWithCurrency) now", comment: "amount with currency") : String(localized: "Request \(amountWithCurrency)", comment: "amount with currency") } - private func subjectTitle(_ amount: Amount, _ currencyInfo: CurrencyInfo) -> String { - let amountStr = amount.formatted(currencyInfo, isNegative: false) + private func subjectTitle(_ amount: Amount) -> String { + let amountStr = amount.formatted(scope, isNegative: false) return outgoing ? String(localized: "NavTitle_Send_AmountStr", defaultValue: "Send \(amountStr)", comment: "NavTitle: Send 'amountStr'") @@ -81,12 +80,24 @@ struct P2PSubjectV: View { } Group { if #available(iOS 16.0, *) { TextField(minimalistic ? "Subject" : EMPTYSTRING, text: $summary, axis: .vertical) + .submitLabel(.next) .focused($isFocused) - .lineLimit(2...) - } else { + .onChange(of: summary) { newValue in + guard isFocused else { return } + guard newValue.contains("\n") else { return } + isFocused = false + summary = newValue.replacing("\n", with: "") + } + } else { TextField("Subject", text: $summary) + .submitLabel(.next) .focused($isFocused) -// .lineLimit(2...5) // lineLimit' is only available in iOS 16.0 or newer + .onChange(of: summary) { newValue in + guard isFocused else { return } + guard newValue.contains("\n") else { return } + isFocused = false + summary = newValue.replacingOccurrences(of: "\n", with: "") + } } } // Group for iOS16+ & iOS15 .talerFont(.title2) .foregroundColor(WalletColors().fieldForeground) // text color @@ -112,24 +123,22 @@ struct P2PSubjectV: View { .padding(.bottom) let disabled = (expireDays == 0) || (summary.count < 1) // TODO: check amountAvailable - NavigationLink(destination: LazyView { - P2PReadyV(stack: stack.push(), - currencyInfo: $currencyInfo, - scope: scope, - summary: summary, - expireDays: expireDays, - outgoing: outgoing, - amountToTransfer: amountToTransfer, - transactionStarted: $transactionStarted) - }) { - Text(buttonTitle(amountToTransfer, currencyInfo)) - } + let destination = P2PReadyV(stack: stack.push(), + scope: scope, + summary: summary, + expireDays: expireDays, + outgoing: outgoing, + amountToTransfer: amountToTransfer, + transactionStarted: $transactionStarted) + NavigationLink(destination: destination) { + Text(buttonTitle(amountToTransfer)) + } .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled)) .disabled(disabled) .accessibilityHint(disabled ? String(localized: "enabled when subject and expiration are set") : EMPTYSTRING) }.padding(.horizontal) } // ScrollVStack // .scrollBounceBehavior(.basedOnSize) needs iOS 16.4 - .navigationTitle(subjectTitle(amountToTransfer, currencyInfo)) + .navigationTitle(subjectTitle(amountToTransfer)) .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) .onAppear { DebugViewC.shared.setViewID(VIEW_P2P_SUBJECT, stack: stack.push()) @@ -142,7 +151,7 @@ struct P2PSubjectV: View { if outgoing && feeLabel == nil { if let ppCheck = try? await model.checkPeerPushDebitM(amountToTransfer, scope: scope) { if let feeAmount = p2pFee(ppCheck: ppCheck) { - let feeStr = feeAmount.formatted(currencyInfo, isNegative: false) + let feeStr = feeAmount.formatted(scope, isNegative: false) myFeeLabel = String(localized: "+ \(feeStr) fee") } else { myFeeLabel = EMPTYSTRING } } else { diff --git a/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift b/TalerWallet1/Views/Actions/Peer2peer/RequestPayment.swift @@ -61,7 +61,6 @@ struct RequestPayment: View { .padding(.bottom, 4) } RequestPaymentContent(stack: stack.push(), - currencyInfo: $currencyInfo, balance: $balance, balanceIndex: $balanceIndex, amountLastUsed: $amountLastUsed, @@ -95,7 +94,6 @@ struct RequestPayment: View { struct RequestPaymentContent: View { private let symLog = SymLogV() let stack: CallStack - @Binding var currencyInfo: CurrencyInfo @Binding var balance: Balance? @Binding var balanceIndex: Int @Binding var amountLastUsed: Amount @@ -146,7 +144,7 @@ struct RequestPaymentContent: View { return peerPullCheck != nil ? true : false } - private func computeFeeRequest(_ amount: Amount) async -> ComputeFeeResult? { + private func computeFee(_ amount: Amount) async -> ComputeFeeResult? { if exchange == nil { if let url = scopeInfo.url { exchange = try? await model.getExchangeByUrl(url: url) @@ -161,7 +159,7 @@ struct RequestPaymentContent: View { let raw = ppCheck.amountRaw let effective = ppCheck.amountEffective if let fee = fee(raw: raw, effective: effective) { - feeStr = fee.formatted(currencyInfo, isNegative: true) + feeStr = fee.formatted(scopeInfo, isNegative: true) symLog.log("Fee = \(feeStr)") peerPullCheck = ppCheck @@ -188,25 +186,22 @@ struct RequestPaymentContent: View { } } return nil - } + } // computeFee 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 coinData = CoinData(details: peerPullCheck) - Group { - if let balance { - let scopeInfo = balance.scopeInfo + if let balance { + Group { if scopeInfo != nil { + let coinData = CoinData(details: peerPullCheck) // let availableStr = amountAvailable.formatted(currencyInfo, isNegative: false) // let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) - let feeLabel = coinData.feeLabel(currencyInfo, - feeZero: String(localized: "No payment fee"), - isNegative: false) + let feeLabel = coinData.feeLabel(scopeInfo, feeZero: String(localized: "No payment fee"), + isNegative: false) let inputDestination = P2PSubjectV(stack: stack.push(), scope: scopeInfo, - currencyInfo: $currencyInfo, feeLabel: feeLabel, feeIsNotZero: feeIsNotZero(), outgoing: false, @@ -215,7 +210,6 @@ struct RequestPaymentContent: View { expireDays: $expireDays) let shortcutDestination = P2PSubjectV(stack: stack.push(), scope: scopeInfo, - currencyInfo: $currencyInfo, feeLabel: nil, feeIsNotZero: feeIsNotZero(), outgoing: false, @@ -225,7 +219,7 @@ struct RequestPaymentContent: View { let amountLabel = minimalistic ? String(localized: "Amount:") : String(localized: "Amount to request:") AmountInputV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scopeInfo, amountAvailable: $amountZero, // incoming needs no available amountLabel: amountLabel, amountToTransfer: $amountToTransfer, @@ -235,25 +229,23 @@ struct RequestPaymentContent: View { shortcutAction: shortcutAction, buttonAction: buttonAction, feeIsNegative: true, - computeFee: computeFeeRequest) + computeFee: computeFee) .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 - } else { // no balance - Yikes - Text("No balance. There seems to be a problem with the database...") } } // Group .task(id: balanceIndex + (1000 * controller.currencyTicker)) { // runs whenever the user changes the exchange via ScopePicker, or on new currencyInfo symLog.log("❗️ task \(balanceIndex)") - if let balance { scopeInfo = balance.scopeInfo amountToTransfer.setCurrency(scopeInfo.currency) - currencyInfo = controller.info(for: scopeInfo, controller.currencyTicker) } + } else { // no balance - Yikes + Text("No balance. There seems to be a problem with the database...") } // .task(id: amountToTransfer.value) { // if exchange == nil { diff --git a/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift b/TalerWallet1/Views/Actions/Peer2peer/SendAmountV.swift @@ -217,7 +217,6 @@ struct SendAmountContent: View { let inputDestination = P2PSubjectV(stack: stack.push(), scope: scopeInfo, - currencyInfo: $currencyInfo, feeLabel: feeLabel(feeStr), feeIsNotZero: feeIsNotZero(), outgoing: true, @@ -226,7 +225,6 @@ struct SendAmountContent: View { expireDays: $expireDays) let shortcutDestination = P2PSubjectV(stack: stack.push(), scope: scopeInfo, - currencyInfo: $currencyInfo, feeLabel: nil, feeIsNotZero: feeIsNotZero(), outgoing: true, @@ -234,7 +232,7 @@ struct SendAmountContent: View { summary: $summary, expireDays: $expireDays) AmountInputV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scopeInfo, amountAvailable: $amountAvailable, amountLabel: nil, // will use "Available: xxx", trailing amountToTransfer: $amountToTransfer, @@ -328,17 +326,16 @@ fileprivate struct Preview_Content: View { var body: some View { let amount = Amount(currency: DEMOCURRENCY, cent: 1000) let pending = Amount(currency: DEMOCURRENCY, cent: 0) - let currencyInfo = CurrencyInfo.zero(DEMOCURRENCY) + let scope = ScopeInfo.zero(DEMOCURRENCY) let exchange2 = Exchange(exchangeBaseUrl: ARS_EXP_EXCHANGE, masterPub: "masterPub", - scopeInfo: currencyInfo.scope, + scopeInfo: scope, paytoUris: [], tosStatus: .proposed, exchangeEntryStatus: .ephemeral, exchangeUpdateStatus: .ready, ageRestrictionOptions: []) - let scopeInfo = ScopeInfo(type: .exchange, url: DEMOEXCHANGE, currency: DEMOCURRENCY) - let balance = Balance(scopeInfo: scopeInfo, + let balance = Balance(scopeInfo: scope, available: amount, pendingIncoming: pending, pendingOutgoing: pending, diff --git a/TalerWallet1/Views/Balances/BalanceCellV.swift b/TalerWallet1/Views/Balances/BalanceCellV.swift @@ -10,8 +10,8 @@ import taler_swift import SymLog struct BalanceCellV: View { - let stack: CallStack? - @Binding var currencyInfo: CurrencyInfo + let stack: CallStack + let scope: ScopeInfo let amount: Amount // let sizeCategory: ContentSizeCategory // let rowAction: () -> Void @@ -24,8 +24,8 @@ struct BalanceCellV: View { /// Renders the Balance button. "Balance" leading, amountStr trailing. If it doesn't fit in one row then /// amount (trailing) goes underneath "Balance" (leading). var body: some View { - let amountV = AmountV(stack: stack?.push("AmountV"), - currencyInfo: $currencyInfo, + let amountV = AmountV(stack: stack.push("AmountV"), + scope: scope, amount: amount, isNegative: nil, // don't show the + sign strikethrough: false, diff --git a/TalerWallet1/Views/Balances/BalancesPendingRowV.swift b/TalerWallet1/Views/Balances/BalancesPendingRowV.swift @@ -9,7 +9,6 @@ import taler_swift struct BalancesPendingRowV: View { // let symLog: SymLogV? // inherited from BalancesSectionView let stack: CallStack - @Binding var currencyInfo: CurrencyInfo let balance: Balance @Binding var selectedBalance: Balance? // <- return here the balance when we go to Transactions @Binding var pendingTransactions: [Transaction] @@ -31,18 +30,18 @@ struct BalancesPendingRowV: View { VStack(spacing: 6) { if hasIncoming { - PendingRowView(currencyInfo: $currencyInfo, - amount: pendingIncoming, - incoming: true, - shouldConfirm: shouldConfirm, - needsKYC: needsKYCin) + PendingRowView(scope: balance.scopeInfo, + amount: pendingIncoming, + incoming: true, + shouldConfirm: shouldConfirm, + needsKYC: needsKYCin) } if hasOutgoing { - PendingRowView(currencyInfo: $currencyInfo, - amount: pendingOutgoing, - incoming: false, - shouldConfirm: false, - needsKYC: needsKYCout) + PendingRowView(scope: balance.scopeInfo, + amount: pendingOutgoing, + incoming: false, + shouldConfirm: false, + needsKYC: needsKYCout) } if !hasIncoming && !hasOutgoing { // should never happen - but DOES when wallet-core doesn't report P2P as pendingIncoming/pendingOutgoing @@ -60,21 +59,18 @@ struct BalancesPendingRowV: View { var body: some View { if let reloadOneAction { - let destination = LazyView { - TransactionsListView(stack: stack.push(), - balance: balance, - selectedBalance: $selectedBalance, - currencyInfo: $currencyInfo, - navTitle: String(localized: "Pending", comment: "ViewTitle of TransactionList"), - scopeInfo: balance.scopeInfo, - transactions: $pendingTransactions, - reloadAllAction: reloadPending, - reloadOneAction: reloadOneAction) - } - NavigationLink(destination: destination, label: { pendingRowLabel() }) + let destination = TransactionsListView(stack: stack.push(), + scope: balance.scopeInfo, + balance: balance, + selectedBalance: $selectedBalance, + navTitle: String(localized: "Pending", comment: "ViewTitle of TransactionList"), + transactions: $pendingTransactions, + reloadAllAction: reloadPending, + reloadOneAction: reloadOneAction) + NavigationLink(destination: destination) { pendingRowLabel() } //let _ = print("button: Pending Transactions: \(currency)") } else { // just for Preview - NavigationLink(destination: EmptyView(), label: { pendingRowLabel() }) + NavigationLink(destination: EmptyView()) { pendingRowLabel() } } } // body } // BalancesPendingRowV @@ -85,7 +81,7 @@ fileprivate struct BalancesPendingRowV_Previews: PreviewProvider { @MainActor struct BindingViewContainer: View { @State private var previewTransactions: [Transaction] = [] - @State private var currencyInfoD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) + @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) @State private var selectedBalance: Balance? = nil var body: some View { let flags: [BalanceFlag] = [.incomingConfirmation] @@ -98,7 +94,6 @@ fileprivate struct BalancesPendingRowV_Previews: PreviewProvider { flags: flags) BalancesPendingRowV(//symLog: nil, stack: CallStack("Preview"), - currencyInfo: $currencyInfoD, balance: balance, selectedBalance: $selectedBalance, pendingTransactions: $previewTransactions, diff --git a/TalerWallet1/Views/Balances/BalancesSectionView.swift b/TalerWallet1/Views/Balances/BalancesSectionView.swift @@ -94,30 +94,21 @@ extension BalancesSectionView: View { #endif let scopeInfo = balance.scopeInfo let currency = scopeInfo.currency - let _ = symLog.log("BalancesSectionView❗️ \(currencyInfo.scope.currency) used as currencyInfo") let balanceDest = LazyView { TransactionsListView(stack: stack.push("\(Self.className())()"), + scope: scopeInfo, balance: balance, selectedBalance: $selectedBalance, - currencyInfo: $currencyInfo, navTitle: currency, // String(localized: "Transactions", comment: "ViewTitle of TransactionList"), - scopeInfo: scopeInfo, transactions: $completedTransactions, reloadAllAction: loadCompleted, reloadOneAction: reloadOneAction) } Section { - if scopeInfo.type == .exchange { - let baseURL = scopeInfo.url?.trimURL ?? String(localized: "Unknown payment provider", comment: "exchange url") - Text(baseURL) - .talerFont(.subheadline) - .foregroundColor(.secondary) -// .listRowSeparator(.hidden) - } BalanceCellV(stack: stack.push("BalanceCell"), - currencyInfo: $currencyInfo, + scope: balance.scopeInfo, amount: balance.available, // sizeCategory: sizeCategory, // rowAction: { buttonSelected = 3 }, // trigger TransactionList NavigationLink @@ -128,7 +119,6 @@ extension BalancesSectionView: View { if pendingTransactions.count > 0 { BalancesPendingRowV(//symLog: symLog, stack: stack.push(), - currencyInfo: $currencyInfo, balance: balance, selectedBalance: $selectedBalance, pendingTransactions: $pendingTransactions, @@ -138,8 +128,7 @@ extension BalancesSectionView: View { } } header: { BarGraphHeader(stack: stack.push(), - scopeInfo: scopeInfo, - currencyInfo: $currencyInfo, + scope: scopeInfo, shouldReloadBalances: $shouldReloadBalances) }.id(sectionID) .listRowSeparator(.hidden) @@ -158,8 +147,7 @@ extension BalancesSectionView: View { let _ = symLog.log("recent transactions") TransactionsArraySliceV(symLog: symLog, stack: stack.push(), - currencyInfo: $currencyInfo, - scopeInfo: scopeInfo, + scope: scopeInfo, transactions: $recentTransactions, reloadAllAction: loadRecent, reloadOneAction: reloadOneAction) diff --git a/TalerWallet1/Views/Balances/PendingRowView.swift b/TalerWallet1/Views/Balances/PendingRowView.swift @@ -10,7 +10,7 @@ import taler_swift /// This view shows a pending transaction row in a currency section struct PendingRowView: View { - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo let amount: Amount let incoming: Bool let shouldConfirm: Bool @@ -37,7 +37,7 @@ struct PendingRowView: View { let outTitle = minimalistic ? outTitle0 : outTitle1 let pendingTitle = incoming ? inTitle : outTitle - let amountText = AmountV($currencyInfo, amount, isNegative: !incoming) + let amountText = AmountV(scope, amount, isNegative: !incoming) .foregroundColor(pendingColor) // this is the default view for iOS 15 @@ -70,34 +70,32 @@ struct PendingRowView: View { } // MARK: - #if DEBUG -@MainActor -fileprivate struct Preview_Content: View { - @State private var currencyInfoD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) - @State private var currencyInfoT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) - var body: some View { - let test = Amount(currency: TESTCURRENCY, cent: 123) - let demo = Amount(currency: DEMOCURRENCY, cent: 123456) - List { - PendingRowView(currencyInfo: $currencyInfoD, - amount: test, incoming: true, shouldConfirm: true, needsKYC: false) - PendingRowView(currencyInfo: $currencyInfoT, - amount: demo, incoming: false, shouldConfirm: false, needsKYC: true) - } - } -} -fileprivate struct Previews: PreviewProvider { - @MainActor - struct StateContainer: View { -// @StateObject private var controller = Controller.shared - var body: some View { - let hello = "Hello" - Text(hello) -// Preview_Content() -// .environmentObject(controller) - } - } - static var previews: some View { - StateContainer() - } -} +//@MainActor +//fileprivate struct Preview_Content: View { +// @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) +// @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) +// var body: some View { +// let test = Amount(currency: TESTCURRENCY, cent: 123) +// let demo = Amount(currency: DEMOCURRENCY, cent: 123456) +// List { +// PendingRowView(amount: test, incoming: true, shouldConfirm: true, needsKYC: false) +// PendingRowView(amount: demo, incoming: false, shouldConfirm: false, needsKYC: true) +// } +// } +//} +//fileprivate struct Previews: PreviewProvider { +// @MainActor +// struct StateContainer: View { +//// @StateObject private var controller = Controller.shared +// var body: some View { +// let hello = "Hello" +// Text(hello) +//// Preview_Content() +//// .environmentObject(controller) +// } +// } +// static var previews: some View { +// StateContainer() +// } +//} #endif diff --git a/TalerWallet1/Views/HelperViews/AmountInputV.swift b/TalerWallet1/Views/HelperViews/AmountInputV.swift @@ -32,7 +32,7 @@ struct ComputeFeeResult { struct AmountInputV: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo? @Binding var amountAvailable: Amount let amountLabel: String? @Binding var amountToTransfer: Amount @@ -87,21 +87,19 @@ struct AmountInputV: View { let currency = amountToTransfer.currencyStr // let insufficientLabel = String(localized: "You don't have enough \(currency).") VStack(alignment: .trailing) { - CurrencyInputView(currencyInfo: currencyInfo, - amount: $amountToTransfer, - amountLastUsed: amountLastUsed, - available: amountAvailable, - title: amountLabel, - shortcutAction: shortcutAction) + CurrencyInputView(scope: scope, + amount: $amountToTransfer, + amountLastUsed: amountLastUsed, + available: amountAvailable, + title: amountLabel, + shortcutAction: shortcutAction) // .accessibility(sortPriority: 2) let coinData = CoinData(coins: numCoins, fee: feeAmount) - QuiteSomeCoins(currencyInfo: $currencyInfo, -// amountEffective: amountEffective, - currency: currency, - coinData: coinData, - shouldShowFee: true, // TODO: set to false if we never charge withdrawal fees - feeIsNegative: feeIsNegative) + QuiteSomeCoins(scope: scope, + coinData: coinData, + shouldShowFee: true, // TODO: set to false if we never charge withdrawal fees + feeIsNegative: feeIsNegative) let flags = checkAvailable(coinData) Button(coinData.invalid ? "Amount too small" : "Next") { buttonAction() } .buttonStyle(TalerButtonStyle(type: .prominent, disabled: flags.disabled)) diff --git a/TalerWallet1/Views/HelperViews/AmountRowV.swift b/TalerWallet1/Views/HelperViews/AmountRowV.swift @@ -11,9 +11,9 @@ import taler_swift // Title and Amount struct AmountRowV: View { let stack: CallStack? - @Binding var currencyInfo: CurrencyInfo let title: String let amount: Amount + let scope: ScopeInfo? let isNegative: Bool? // show fee with minus (or plus) sign, or no sign if nil let color: Color let large: Bool // set to false for QR or IBAN @@ -23,7 +23,7 @@ struct AmountRowV: View { .multilineTextAlignment(.leading) .talerFont(.body) let amountV = AmountV(stack: stack?.push(), - currencyInfo: $currencyInfo, + scope: scope, amount: amount, isNegative: isNegative, strikethrough: false, @@ -63,11 +63,11 @@ struct AmountRowV: View { } } extension AmountRowV { - init(_ currencyInfo: Binding<CurrencyInfo>, title: String, amount: Amount, isNegative: Bool, color: Color) { + init(_ title: String, amount: Amount, scope: ScopeInfo?, isNegative: Bool, color: Color) { self.stack = nil - self._currencyInfo = currencyInfo self.title = title self.amount = amount + self.scope = scope self.isNegative = isNegative self.color = color self.large = true @@ -87,15 +87,15 @@ fileprivate func talerFromStr(_ from: String) -> Amount { #if DEBUG @MainActor fileprivate struct BindingViewContainer: View { - @State private var currencyInfoD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) + @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) var body: some View { - let fee = Amount(currency: "Taler", cent: 20) - let currencyInfo = CurrencyInfo.zero("Taler") - AmountRowV(stack: nil, currencyInfo: $currencyInfoD, title: "Fee", amount: fee, isNegative: true, color: Color("Outgoing"), large: false) - let cents = Amount(currency: "Taler", cent: 480) - AmountRowV($currencyInfoD, title: "Cents", amount: cents, isNegative: false, color: Color("Incoming")) + let scope = ScopeInfo.zero(DEMOCURRENCY) + let fee = Amount(currency: DEMOCURRENCY, cent: 20) + AmountRowV("Fee", amount: fee, scope: scope, isNegative: true, color: Color("Outgoing")) + let cents = Amount(currency: DEMOCURRENCY, cent: 480) + AmountRowV("Cents", amount: cents, scope: scope, isNegative: false, color: Color("Incoming")) let amount = talerFromStr("Taler:4.80") - AmountRowV($currencyInfoD, title: "Chosen amount to withdraw", amount: amount, isNegative: false, color: Color("Incoming")) + AmountRowV("Chosen amount to withdraw", amount: amount, scope: scope, isNegative: false, color: Color("Incoming")) } } diff --git a/TalerWallet1/Views/HelperViews/AmountV.swift b/TalerWallet1/Views/HelperViews/AmountV.swift @@ -10,46 +10,106 @@ import taler_swift struct AmountV: View { let stack: CallStack? - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo? let amount: Amount let isNegative: Bool? // if true, show a "-" before the amount + let useISO: Bool let strikethrough: Bool let large: Bool // set to false for QR or IBAN - + let a11y: String? + @EnvironmentObject private var controller: Controller - var body: some View { + @State private var currencyInfo: CurrencyInfo? + @State private var showBanknotes = false + + private var dismissAlertButton: some View { + Button("Cancel", role: .cancel) { + showBanknotes = false + } + } + + private func currencyTickerChanged() async { + if let scope { + currencyInfo = controller.info(for: scope) // might be nil! + } + } + + private func amountStr(_ currencyInfo : CurrencyInfo?) -> String { let dontShow = (isNegative == nil) let showSign: Bool = isNegative ?? false - let amountFormatted = amount.formatted(currencyInfo, isNegative: false) + let amountFormatted: String + if let currencyInfo { + amountFormatted = amount.formatted(currencyInfo, isNegative: false, + useISO: useISO, a11y: a11y) + } else { + amountFormatted = amount.readableDescription + } let amountStr = dontShow ? amountFormatted : showSign ? "- \(amountFormatted)" : "+ \(amountFormatted)" - Text(amountStr) + return amountStr + } + + var body: some View { + Text(amountStr(currencyInfo)) .strikethrough(strikethrough, color: .red) .multilineTextAlignment(.center) .talerFont(large ? .title : .title2) // .fontWeight(large ? .medium : .regular) // @available(iOS 16.0, *) .monospacedDigit() - .accessibilityLabel(amount.readableDescription) + .accessibilityLabel(amount.readableDescription) // TODO: locale.leadingCurrencySymbol + .task(id: controller.currencyTicker) { await currencyTickerChanged() } +// .onLongPressGesture(minimumDuration: 0.3) { +// showValue = true +// } +// .alert("Bla", +// isPresented: $showBanknotes, +// actions: { dismissAlertButton }, +// message: { Text("Blub") }) } } extension AmountV { - init(_ currencyInfo: Binding<CurrencyInfo>, _ amount: Amount, isNegative: Bool?) { + init(_ scope: ScopeInfo?, _ amount: Amount) { self.stack = nil - self._currencyInfo = currencyInfo + self.scope = scope + self.amount = amount + self.isNegative = false + self.useISO = false + self.strikethrough = false + self.large = false + self.a11y = nil + } + init(_ scope: ScopeInfo?, _ amount: Amount, isNegative: Bool?) { + self.stack = nil + self.scope = scope self.amount = amount self.isNegative = isNegative + self.useISO = false self.strikethrough = false self.large = false + self.a11y = nil } - init(_ currencyInfo: Binding<CurrencyInfo>, _ amount: Amount, isNegative: Bool?, strikethrough: Bool) { + init(_ scope: ScopeInfo?, _ amount: Amount, isNegative: Bool?, strikethrough: Bool) { self.stack = nil - self._currencyInfo = currencyInfo + self.scope = scope + self.amount = amount + self.isNegative = isNegative + self.useISO = false + self.strikethrough = strikethrough + self.large = false + self.a11y = nil + } + init(stack: CallStack?, scope: ScopeInfo?, amount: Amount, isNegative: Bool?, + strikethrough: Bool, large: Bool = false) { + self.stack = stack + self.scope = scope self.amount = amount self.isNegative = isNegative + self.useISO = false self.strikethrough = strikethrough self.large = false + self.a11y = nil } } // MARK: - diff --git a/TalerWallet1/Views/HelperViews/BarGraph.swift b/TalerWallet1/Views/HelperViews/BarGraph.swift @@ -10,8 +10,7 @@ let MAXBARS = 15 struct BarGraphHeader: View { private let symLog = SymLogV(0) let stack: CallStack - let scopeInfo: ScopeInfo? - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo @Binding var shouldReloadBalances: Int @EnvironmentObject private var model: WalletModel @@ -24,27 +23,25 @@ struct BarGraphHeader: View { @ScaledMetric var barHeight = 9 // relative to fontSize var body: some View { + let currencyInfo = controller.info(for: scope, controller.currencyTicker) HStack (alignment: .center, spacing: 10) { if !minimalistic || currencyInfo.hasSymbol { Text(currencyInfo.name) .talerFont(.title2) .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) } - if let scopeInfo { - BarGraph(transactions: $completedTransactions, - maxBars: MAXBARS, barHeight: barHeight) - } + BarGraph(transactions: $completedTransactions, + maxBars: MAXBARS, barHeight: barHeight) } // .headerProminence(.increased) // unfortunately this is not useful .task(id: shouldReloadBalances + 2_000_000) { - if let scopeInfo { - symLog.log(".task for BarGraphHeader(\(scopeInfo.currency)) - load \(MAXBARS) Transactions") - if let response = try? await model.transactionsT2(stack.push("BarGraphHeader - \(scopeInfo.url?.trimURL)"), - scopeInfo: scopeInfo, - filterByState: .done, - limit: MAXBARS) { - completedTransactions = response - } + symLog.log(".task for BarGraphHeader(\(scope.currency)) - load \(MAXBARS) Transactions") + if let response = try? await model.transactionsT2(stack.push("BarGraphHeader - \(scope.url?.trimURL)"), + scopeInfo: scope, + filterByState: .done, + limit: MAXBARS + ) { + completedTransactions = response } } } diff --git a/TalerWallet1/Views/HelperViews/CurrencyInputView.swift b/TalerWallet1/Views/HelperViews/CurrencyInputView.swift @@ -12,7 +12,7 @@ fileprivate let replaceable = 500 fileprivate let shortcutValues = [5000,2500,1000] // TODO: adapt for ¥ struct ShortcutButton: View { - let currencyInfo: CurrencyInfo + let scope: ScopeInfo? let currency: String let currencyField: CurrencyField let shortcut: Int @@ -20,12 +20,12 @@ struct ShortcutButton: View { let action: (Int, CurrencyField) -> Void func makeButton(with newShortcut: Int) -> ShortcutButton { - ShortcutButton(currencyInfo: currencyInfo, - currency: currency, - currencyField: currencyField, - shortcut: newShortcut, - available: available, - action: action) + ShortcutButton(scope: scope, + currency: currency, + currencyField: currencyField, + shortcut: newShortcut, + available: available, + action: action) } func isDisabled(shortie: Amount) -> Bool { @@ -46,7 +46,7 @@ struct ShortcutButton: View { // let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear #endif let shortie = Amount(currency: currency, cent: UInt64(shortcut)) // TODO: adapt for ¥ - let title = shortie.formatted(currencyInfo, isNegative: false) + let title = shortie.formatted(scope, isNegative: false) let shortcutLabel = String(localized: "Shortcut", comment: "VoiceOver: $50,$25,$10,$5 shortcut buttons") Button(action: { action(shortcut, currencyField)} ) { Text(title) @@ -61,14 +61,14 @@ struct ShortcutButton: View { } // MARK: - struct CurrencyInputView: View { - let currencyInfo: CurrencyInfo + let scope: ScopeInfo? @Binding var amount: Amount // the `value´ let amountLastUsed: Amount let available: Amount? let title: String? let shortcutAction: ((_ amount: Amount) -> Void)? -// @EnvironmentObject private var controller: Controller + @EnvironmentObject private var controller: Controller @State private var hasBeenShown = false @State private var showKeyboard = 0 @@ -97,12 +97,12 @@ struct CurrencyInputView: View { if !shortcutValues.contains(lastUsedI) { shortcut = lastUsedI } } } - return ShortcutButton(currencyInfo: currencyInfo, - currency: amount.currencyStr, - currencyField: currencyField, - shortcut: shortcut, - available: available, - action: action) + return ShortcutButton(scope: scope, + currency: amount.currencyStr, + currencyField: currencyField, + shortcut: shortcut, + available: available, + action: action) } @MainActor @@ -119,7 +119,7 @@ struct CurrencyInputView: View { return title } if let available { - let availableStr = available.formatted(currencyInfo, isNegative: false) + let availableStr = available.formatted(scope, isNegative: false) return String(localized: "Available: \(availableStr)") } return nil @@ -129,12 +129,20 @@ struct CurrencyInputView: View { return String(localized: "Available: \(availableStr)") } + func currencyInfo() -> CurrencyInfo { + if let scope { + return controller.info(for: scope, controller.currencyTicker) + } else { + return controller.info2(for: amount.currencyStr, controller.currencyTicker) + } + } + 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 currency = amount.currencyStr + let currencyInfo = currencyInfo() let currencyField = CurrencyField(currencyInfo, amount: $amount) VStack (alignment: .center) { // center shortcut buttons if let heading = heading() { @@ -156,7 +164,7 @@ struct CurrencyInputView: View { .textFieldStyle(.roundedBorder) .onTapGesture { if useShortcut != 0 { - amount = Amount.zero(currency: currency) + amount = Amount.zero(currency: amount.currencyStr) useShortcut = 0 } showKeyboard += 1 @@ -219,23 +227,23 @@ struct CurrencyInputView: View { } // MARK: - #if DEBUG -fileprivate struct Previews: PreviewProvider { - @MainActor - struct StateContainer: View { - @State var amountToPreview = Amount(currency: LONGCURRENCY, cent: 0) - @State var amountLastUsed = Amount(currency: LONGCURRENCY, cent: 170) - @State private var currencyInfoL: CurrencyInfo = CurrencyInfo.zero(LONGCURRENCY) - var body: some View { - CurrencyInputView(currencyInfo: currencyInfoL, - amount: $amountToPreview, - amountLastUsed: amountLastUsed, - available: Amount(currency: LONGCURRENCY, cent: 2000), - title: "Amount to withdraw:", - shortcutAction: nil) - } - } - static var previews: some View { - StateContainer() - } -} +//fileprivate struct Previews: PreviewProvider { +// @MainActor +// struct StateContainer: View { +// @State var amountToPreview = Amount(currency: LONGCURRENCY, cent: 0) +// @State var amountLastUsed = Amount(currency: LONGCURRENCY, cent: 170) +// @State private var previewL: CurrencyInfo = CurrencyInfo.zero(LONGCURRENCY) +// var body: some View { +// CurrencyInputView(amount: $amountToPreview, +// scope: <#ScopeInfo#>, +// amountLastUsed: amountLastUsed, +// available: Amount(currency: LONGCURRENCY, cent: 2000), +// title: "Amount to withdraw:", +// shortcutAction: nil) +// } +// } +// static var previews: some View { +// StateContainer() +// } +//} #endif diff --git a/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift b/TalerWallet1/Views/HelperViews/QRCodeDetailView.swift @@ -14,11 +14,28 @@ struct QRCodeDetailView: View { let talerCopyShare: String let incoming: Bool let amount: Amount - let specs: CurrencySpecification + let scope: ScopeInfo @EnvironmentObject private var controller: Controller @AppStorage("minimalistic") var minimalistic: Bool = false + @State private var currencyInfo: CurrencyInfo? + + private func currencyTickerChanged() async { + currencyInfo = controller.info(for: scope) + } + + private func amountStr(_ currencyInfo : CurrencyInfo?) -> String { + let amountFormatted: String + if let currencyInfo { + amountFormatted = amount.formatted(currencyInfo, isNegative: false, + useISO: false, a11y: nil) + } else { + amountFormatted = amount.readableDescription + } + return amountFormatted + } + var body: some View { if talerURI.count > 10 { Section { @@ -39,7 +56,7 @@ struct QRCodeDetailView: View { .accessibilityLabel("QR Code") .listRowSeparator(.hidden) if !minimalistic { - let amountStr = amount.formatted(specs: specs, isNegative: false) + let amountStr = amountStr(currencyInfo) let scanLong = incoming ? String(localized: "(payer) 1", defaultValue: "Let the payer scan this QR code to pay \(amountStr).", comment: "e.g. '5,3 €'") @@ -50,6 +67,7 @@ struct QRCodeDetailView: View { .multilineTextAlignment(.leading) .talerFont(.title3) .listRowSeparator(.hidden) + .task(id: controller.currencyTicker) { await currencyTickerChanged() } } CopyShare(textToCopy: talerCopyShare) .disabled(false) diff --git a/TalerWallet1/Views/HelperViews/SubjectInputV.swift b/TalerWallet1/Views/HelperViews/SubjectInputV.swift @@ -12,7 +12,6 @@ import SymLog struct SubjectInputV<TargetView: View>: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo // the scanned URL let url: URL? let amountAvailable: Amount? // TODO: getMaxDepositAmountM, getMaxPeerPushDebitAmountM @@ -35,7 +34,6 @@ struct SubjectInputV<TargetView: View>: View { let navTitle = String(localized: "Custom Summary", comment:"pay merchant") - @State private var preparePayResult: PreparePayResult? = nil @FocusState private var isFocused: Bool var body: some View { @@ -44,16 +42,16 @@ struct SubjectInputV<TargetView: View>: View { // let insufficientLabel = String(localized: "You don't have enough \(currency).") // let feeLabel = insufficient ? insufficientLabel // : feeLabel(feeStr) - let available = amountAvailable?.formatted(currencyInfo, isNegative: false) ?? nil +// let available = amountAvailable?.formatted(scope, isNegative: false) ?? nil // let disabled = insufficient || summary.count == 0 let disabled = summary.count == 0 ScrollView { VStack(alignment: .leading) { - if let available { - Text("Available:\t\(available)") - .talerFont(.title3) - .padding(.bottom, 2) -// .accessibility(sortPriority: 3) - } +// if let available { +// Text("Available:\t\(available)") +// .talerFont(.title3) +// .padding(.bottom, 2) +//// .accessibility(sortPriority: 3) +// } if !minimalistic { Text("Enter subject:") // Purpose @@ -69,7 +67,7 @@ struct SubjectInputV<TargetView: View>: View { } else { TextField("Subject", text: $summary) .focused($isFocused) - // .lineLimit(2...5) // lineLimit' is only available in iOS 16.0 or newer +// .lineLimit(2...5) // lineLimit' is only available in iOS 16.0 or newer } } // Group for iOS16+ & iOS15 .talerFont(.title2) .foregroundColor(WalletColors().fieldForeground) // text color @@ -84,16 +82,16 @@ struct SubjectInputV<TargetView: View>: View { } } } - HStack { - Text(amountToTransfer.formatted(currencyInfo, isNegative: false)) - // TODO: hasFees? -// Text(feeLabel) - } - .talerFont(.body) -// .foregroundColor(insufficient ? .red : WalletColors().secondary(colorScheme, colorSchemeContrast)) - .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) -// .accessibility(sortPriority: 1) - .padding(4) +// HStack { +// Text(amountToTransfer.formatted(currencyInfo, isNegative: false)) +// // TODO: hasFees? +//// Text(feeLabel) +// } +// .talerFont(.body) +//// .foregroundColor(insufficient ? .red : WalletColors().secondary(colorScheme, colorSchemeContrast)) +// .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) +//// .accessibility(sortPriority: 1) +// .padding(4) // if insufficient { // Text(insufficientLabel) // .talerFont(.body) diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -331,10 +331,6 @@ extension MainView { let depositDest = DepositIbanV(stack: stack.push(Self.name), selectedBalance: $selectedBalance, - feeLabel: nil, - feeIsNotZero: nil, - // depositIBAN: $depositIBAN, - // accountHolder: $accountHolder, amountLastUsed: $amountLastUsed) let manualWithdrawDest = ManualWithdraw(stack: stack.push(Self.name), diff --git a/TalerWallet1/Views/Settings/Exchange/ExchangeSectionView.swift b/TalerWallet1/Views/Settings/Exchange/ExchangeSectionView.swift @@ -131,8 +131,7 @@ struct ExchangeSectionView: View { }) } header: { BarGraphHeader(stack: stack.push(), - scopeInfo: scopeInfo, - currencyInfo: $currencyInfo, + scope: scopeInfo, shouldReloadBalances: $shouldReloadBalances) } .task { await viewDidLoad() } diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pPayURIView.swift @@ -21,7 +21,6 @@ struct P2pPayURIView: View { @Environment(\.colorSchemeContrast) private var colorSchemeContrast @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic - @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) @State private var peerPullDebitResponse: PreparePeerPullDebitResponse? let navTitle = String(localized: "Pay P2P", comment: "Nav Title") @@ -32,10 +31,11 @@ struct P2pPayURIView: View { List { let raw = peerPullDebitResponse.amountRaw let effective = peerPullDebitResponse.amountEffective + let scope = peerPullDebitResponse.scopeInfo let currency = raw.currencyStr let fee = try! Amount.diff(raw, effective) ThreeAmountsSection(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, topTitle: String(localized: "Amount to pay:"), topAbbrev: String(localized: "Pay:", comment: "mini"), topAmount: raw, @@ -60,20 +60,20 @@ struct P2pPayURIView: View { } .listStyle(myListStyle.style).anyView .navigationTitle(navTitle) - .task(id: controller.currencyTicker) { - let currency = peerPullDebitResponse.amountRaw.currencyStr - currencyInfo = controller.info2(for: currency, controller.currencyTicker) - } - - NavigationLink(destination: LazyView { - P2pAcceptDone(stack: stack.push(), - transactionId: peerPullDebitResponse.transactionId, - incoming: false) - }) { +// .task(id: controller.currencyTicker) { +// let currency = peerPullDebitResponse.amountRaw.currencyStr +// currencyInfo = controller.info2(for: currency, controller.currencyTicker) +// } + .safeAreaInset(edge: .bottom) { + let destination = P2pAcceptDone(stack: stack.push(), + transactionId: peerPullDebitResponse.transactionId, + incoming: false) + NavigationLink(destination: destination) { Text("Confirm Payment", comment:"pay P2P request/invoice") // SHEET_PAY_P2P } .buttonStyle(TalerButtonStyle(type: .prominent)) .padding(.horizontal) + } } else { #if DEBUG let message = url.host diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pReceiveURIView.swift @@ -44,7 +44,7 @@ struct P2pReceiveURIView: View { let currency = raw.currencyStr let fee = try! Amount.diff(raw, effective) ThreeAmountsSection(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: peerPushCreditResponse.scopeInfo, topTitle: String(localized: "Gross Amount to receive:"), topAbbrev: String(localized: "Receive gross:", comment: "mini"), topAmount: raw, diff --git a/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift b/TalerWallet1/Views/Sheets/Payment/PayTemplateV.swift @@ -39,8 +39,8 @@ struct PayTemplateV: View { @State private var buttonSelected2 = false @State private var summaryIsEditable = false @State private var summary: String = EMPTYSTRING // templateParam + @State private var scope: ScopeInfo? = nil - @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) // @State private var feeAmount: Amount? = nil // @State private var feeStr: String = EMPTYSTRING @@ -107,7 +107,6 @@ struct PayTemplateV: View { let inputDestination = LazyView { SubjectInputV(stack: stack.push(), - currencyInfo: $currencyInfo, url: url, amountAvailable: nil, amountToTransfer: $amountToTransfer, @@ -120,7 +119,6 @@ struct PayTemplateV: View { } // destination to subject input let shortcutDestination = LazyView { SubjectInputV(stack: stack.push(), - currencyInfo: $currencyInfo, url: url, amountAvailable: nil, amountToTransfer: $amountShortcut, @@ -134,7 +132,7 @@ struct PayTemplateV: View { Group { if amountIsEditable { // template contract amount is not fixed => let the user input an amount first let amountInput = AmountInputV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, amountAvailable: $amountAvailable, amountLabel: amountLabel, amountToTransfer: $amountToTransfer, @@ -181,10 +179,6 @@ struct PayTemplateV: View { } .navigationTitle(navTitle) .frame(maxWidth: .infinity, alignment: .leading) .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) - .task(id: controller.currencyTicker) { - let currency = amountToTransfer.currencyStr - currencyInfo = controller.info2(for: currency, controller.currencyTicker) - } .onAppear() { symLog.log("onAppear") DebugViewC.shared.setSheetID(SHEET_PAY_TEMPLATE) diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentDone.swift b/TalerWallet1/Views/Sheets/Payment/PaymentDone.swift @@ -11,7 +11,7 @@ import SymLog struct PaymentDone: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo? let transactionId: String @EnvironmentObject private var controller: Controller @@ -32,7 +32,7 @@ struct PaymentDone: View { if paymentDone { let navTitle = String(localized: "Paid", comment: "Title, short") TransactionSummaryV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, transactionId: transactionId, reloadAction: reloadOneAction, navTitle: navTitle, diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentView.swift b/TalerWallet1/Views/Sheets/Payment/PaymentView.swift @@ -69,7 +69,7 @@ func templateFee(ppCheck: PreparePayResult?) -> Amount? { // both from the shop's website - or even from a printed QR code. // We show the payment details in a sheet, and a "Confirm payment" / "Pay now" button. // This is also the final view after the user entered data of a <pay-template>. -struct PaymentView: View { +struct PaymentView: View, Sendable { private let symLog = SymLogV(0) let stack: CallStack @@ -90,6 +90,14 @@ struct PaymentView: View { func checkCurrencyInfo(for result: PreparePayResult) async { + let scopes = result.scopes + if scopes.count > 0 { + for scope in scopes { + await controller.checkInfo(for: scope, model: model) + } + return + } + // else fallback to contractTerms.exchanges let exchanges = result.contractTerms.exchanges for exchange in exchanges { let baseUrl = exchange.url @@ -107,10 +115,12 @@ struct PaymentView: View { Group { if let preparePayResult { let status = preparePayResult.status + let firstScope = preparePayResult.scopes.first let raw = preparePayResult.amountRaw let currency = raw.currencyStr let effective = preparePayResult.amountEffective let terms = preparePayResult.contractTerms + let exchanges = terms.exchanges let baseURL = terms.exchanges.first?.url let paid = status == .alreadyConfirmed let navTitle = paid ? String(localized: "Already paid", comment:"pay merchant navTitle") @@ -138,12 +148,13 @@ struct PaymentView: View { if let effective { let fee = try! Amount.diff(raw, effective) // TODO: different currencies ThreeAmountsSection(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: firstScope, topTitle: topTitle, topAbbrev: topAbbrev, topAmount: raw, noFees: nil, // TODO: check baseURL for fees fee: fee, + feeIsNegative: nil, bottomTitle: bottomTitle, bottomAbbrev: String(localized: "Effective:", comment: "mini"), bottomAmount: effective, @@ -159,12 +170,13 @@ struct PaymentView: View { Text("You don't have enough \(currency).") .talerFont(.headline) ThreeAmountsSection(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: firstScope, topTitle: topTitle, topAbbrev: topAbbrev, topAmount: raw, noFees: nil, // TODO: check baseURL for fees fee: nil, + feeIsNegative: nil, bottomTitle: String(localized: "Amount available:"), bottomAbbrev: String(localized: "Available:", comment: "mini"), bottomAmount: balanceDetails.balanceAvailable, @@ -186,17 +198,18 @@ struct PaymentView: View { if !paid { if let effective { VStack { - NavigationLink(destination: LazyView { - PaymentDone(stack: stack.push(), - currencyInfo: $currencyInfo, - transactionId: preparePayResult.transactionId) - }) { + let destination = PaymentDone(stack: stack.push(), + scope: firstScope, // TODO: let user choose which currency + transactionId: preparePayResult.transactionId) + NavigationLink(destination: destination) { let formatted = raw.formatted(currencyInfo, isNegative: false) Text("Pay \(formatted) now") } .buttonStyle(TalerButtonStyle(type: .prominent)) .padding(.horizontal) - Text("Payment is made in \(currencyInfo.scope.currency)") + let currency = currencyInfo.currency +// let currency = amountToTransfer.currencyStr + Text("Payment is made in \(currency)") .talerFont(.callout) } } else { @@ -211,28 +224,33 @@ struct PaymentView: View { .navigationTitle(navTitle) .task(id: controller.currencyTicker) { let currency = amountToTransfer.currencyStr - currencyInfo = controller.info2(for: currency, controller.currencyTicker) - symLog.log("Info(for: \(currency)) loaded: \(currencyInfo.scope.currency)") + if let resultScope = preparePayResult.scopes.first { // TODO: let user choose which currency + currencyInfo = controller.info(for: resultScope, controller.currencyTicker) + } else { + currencyInfo = controller.info2(for: currency, controller.currencyTicker) + } + symLog.log("Info(for: \(currency)) loaded: \(currencyInfo.name)") } } else { LoadingView(scopeInfo: nil, message: url.host) - .task { // this runs only once - symLog.log(".task") - if template { - if let payResponse = try? await model.preparePayForTemplateM(url.absoluteString, - amount: amountIsEditable ? amountToTransfer : nil, - summary: summaryIsEditable ? summary : nil) { - await checkCurrencyInfo(for: payResponse) - preparePayResult = payResponse - } - } else { - if let result = try? await model.preparePayForUriM(url.absoluteString) { - amountToTransfer = result.amountRaw - await checkCurrencyInfo(for: result) - preparePayResult = result + .task { // this runs only once + symLog.log(".task") + if template { + if let payResponse = try? await model.preparePayForTemplateM(url.absoluteString, + amount: amountIsEditable ? amountToTransfer : nil, + summary: summaryIsEditable ? summary : nil) { + await checkCurrencyInfo(for: payResponse) + preparePayResult = payResponse + } + } else { + if let result = try? await model.preparePayForUriM(url.absoluteString) { + amountToTransfer = result.amountRaw + await checkCurrencyInfo(for: result) + preparePayResult = result + } } } - } +// .task { await viewDidLoad() } } }.onAppear() { symLog.log("onAppear") diff --git a/TalerWallet1/Views/Sheets/Refund/RefundURIView.swift b/TalerWallet1/Views/Sheets/Refund/RefundURIView.swift @@ -30,11 +30,12 @@ struct RefundURIView: View { var body: some View { if let refundTransactionId, let transaction { let common = transaction.common + let scope = common.scopes[0] // TODO: tx could span multiple scopes let raw = common.amountRaw let currency = raw.currencyStr -// let currencyInfo = controller.info(for: currency, controller.currencyTicker) + TransactionSummaryV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, transactionId: refundTransactionId, reloadAction: reloadOneAction, navTitle: nil, // navTitle, diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift @@ -12,8 +12,7 @@ import SymLog struct WithdrawAcceptDone: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo - + let scope: ScopeInfo let exchangeBaseUrl: String? let url: URL let amountToTransfer: Amount? @@ -40,7 +39,7 @@ struct WithdrawAcceptDone: View { Group { if let transactionId { TransactionSummaryV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, transactionId: transactionId, reloadAction: reloadOneAction, navTitle: navTitle, @@ -86,27 +85,27 @@ struct WithdrawAcceptDone: View { } // MARK: - #if DEBUG -struct WithdrawAcceptDone_Previews: PreviewProvider { - @MainActor - struct StateContainer: View { - @State private var currencyInfoD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) - @State private var currencyInfoT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) - - var body: some View { -// let test = Amount(currency: TESTCURRENCY, cent: 123) -// let demo = Amount(currency: DEMOCURRENCY, cent: 123456) - - WithdrawAcceptDone(stack: CallStack("Preview"), - currencyInfo: $currencyInfoD, - exchangeBaseUrl: DEMOEXCHANGE, - url: URL(string: DEMOSHOP)!, - amountToTransfer: nil) - } - } - - static var previews: some View { - StateContainer() -// .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings - } -} +//struct WithdrawAcceptDone_Previews: PreviewProvider { +// @MainActor +// struct StateContainer: View { +// @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) +// @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) +// +// var body: some View { +//// let test = Amount(currency: TESTCURRENCY, cent: 123) +//// let demo = Amount(currency: DEMOCURRENCY, cent: 123456) +// +// WithdrawAcceptDone(stack: CallStack("Preview"), +// scope: previewD.scope, +// exchangeBaseUrl: DEMOEXCHANGE, +// url: URL(string: DEMOSHOP)!, +// amountToTransfer: nil) +// } +// } +// +// static var previews: some View { +// StateContainer() +//// .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings +// } +//} #endif diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptView.swift @@ -12,11 +12,11 @@ import SymLog struct WithdrawAcceptView: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo let navTitle = String(localized: "Withdrawal") // the URL from the bank website let url: URL + let scope: ScopeInfo @Binding var amountToTransfer: Amount @Binding var wireFee: Amount? @Binding var exchange: Exchange? @@ -25,7 +25,8 @@ struct WithdrawAcceptView: View { @EnvironmentObject private var model: WalletModel @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic - @State private var withdrawalAmountDetails: WithdrawalAmountDetails? = nil + @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) + @State private var withdrawalDetails: WithdrawalDetailsForAmount? = nil func reloadExchange() async -> Void { // TODO: throws? if let exchange { @@ -36,7 +37,7 @@ struct WithdrawAcceptView: View { } var body: some View { - if let exchange, let withdrawalAmountDetails { + if let exchange, let withdrawalDetails { VStack { let tosAccepted = exchange.tosStatus == .accepted if !tosAccepted { @@ -47,16 +48,15 @@ struct WithdrawAcceptView: View { acceptAction: reloadExchange) } List { - let raw = withdrawalAmountDetails.amountRaw - let effective = withdrawalAmountDetails.amountEffective + let raw = withdrawalDetails.amountRaw + let effective = withdrawalDetails.amountEffective let currency = raw.currencyStr - let currencyInfo = controller.info2(for: currency, controller.currencyTicker) let fee = try! Amount.diff(raw, effective) let outColor = WalletColors().transactionColor(false) let inColor = WalletColors().transactionColor(true) ThreeAmountsSection(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, topTitle: String(localized: "Chosen amount to withdraw:"), topAbbrev: String(localized: "Withdraw:", comment: "Chosen amount to withdraw:"), topAmount: raw, @@ -81,13 +81,12 @@ struct WithdrawAcceptView: View { .listStyle(myListStyle.style).anyView .navigationTitle(navTitle) if tosAccepted { - NavigationLink(destination: LazyView { - WithdrawAcceptDone(stack: stack.push(), - currencyInfo: $currencyInfo, - exchangeBaseUrl: exchange.exchangeBaseUrl, - url: url, - amountToTransfer: amountToTransfer) - }) { + let destination = WithdrawAcceptDone(stack: stack.push(), + scope: scope, + exchangeBaseUrl: exchange.exchangeBaseUrl, + url: url, + amountToTransfer: amountToTransfer) + NavigationLink(destination: destination) { Text("Confirm Withdrawal") // SHEET_WITHDRAW_ACCEPT } .buttonStyle(TalerButtonStyle(type: .prominent)) @@ -108,14 +107,15 @@ struct WithdrawAcceptView: View { .task(id: exchange?.id) { symLog.log(".task \(exchange?.id ?? "nil")") if let exchange { - if let details = try? await model.getWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, - amount: amountToTransfer) { - withdrawalAmountDetails = details + if let details = try? await model.getWithdrawalDetailsForAmountM(amountToTransfer, + baseUrl: exchange.exchangeBaseUrl, + scope: nil) { // TODO: scope + withdrawalDetails = details } // agePicker.setAges(ages: details?.ageRestrictionOptions) } else { // TODO: error symLog.log("no exchangeBaseUrl or no exchange") - withdrawalAmountDetails = nil + withdrawalDetails = nil } } } diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawURIView.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawURIView.swift @@ -43,7 +43,7 @@ struct WithdrawURIView: View { @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) // @State private var feeAmount: Amount? = nil - @State private var withdrawalAmountDetails: WithdrawalAmountDetails? = nil + @State private var withdrawalDetails: WithdrawalDetailsForAmount? = nil let navTitle = String(localized: "Withdrawal") @@ -67,8 +67,9 @@ struct WithdrawURIView: View { private func computeFeeWithdraw(_ amount: Amount) async -> ComputeFeeResult? { if let exchange { - if let details = try? await model.getWithdrawalDetailsForAmountM(exchange.exchangeBaseUrl, - amount: amount) { + if let details = try? await model.getWithdrawalDetailsForAmountM(amount, + baseUrl: exchange.exchangeBaseUrl, + scope: nil) { // TODO: scope let fee = try? details.amountRaw - details.amountEffective let feeStr = fee?.formatted(currencyInfo, isNegative: true) ?? "nix" symLog.log("Fee = \(feeStr)") @@ -77,7 +78,7 @@ struct WithdrawURIView: View { } else { false } - withdrawalAmountDetails = details + withdrawalDetails = details return ComputeFeeResult(insufficient: insufficient, feeAmount: fee, feeStr: feeLabel(feeStr), numCoins: details.numCoins) } @@ -87,12 +88,37 @@ struct WithdrawURIView: View { return nil } + private func viewDidLoad() async { + symLog.log(".task") + do { + let uriInfoResponse = try await model.getWithdrawalDetailsForUriM(url.absoluteString) + let amount = uriInfoResponse.amount + let currency = amount?.currencyStr ?? uriInfoResponse.currency + amountToTransfer = amount ?? Amount.zero(currency: currency) + amountIsEditable = uriInfoResponse.editableAmount + amountAvailable = uriInfoResponse.maxAmount // may be nil + wireFee = uriInfoResponse.wireFee // may be nil + let baseUrl = uriInfoResponse.defaultExchangeBaseUrl + ?? uriInfoResponse.possibleExchanges[0].exchangeBaseUrl + defaultExchangeBaseUrl = uriInfoResponse.defaultExchangeBaseUrl + possibleExchanges = uriInfoResponse.possibleExchanges + + await loadExchange(baseUrl) + symLog.log("\(baseUrl.trimURL) loaded") + + await controller.checkCurrencyInfo(for: baseUrl, model: model) + symLog.log("Info(for: \(baseUrl.trimURL) loaded") + } catch { + // TODO: error, dismiss + } + } + 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 possibleExchanges.count > 0 { + if let exchange { if let defaultBaseUrl = defaultExchangeBaseUrl ?? possibleExchanges.first?.exchangeBaseUrl { VStack { let title = String(localized: "using:", comment: "using: exchange.taler.net") @@ -105,45 +131,36 @@ struct WithdrawURIView: View { } .talerFont(.title3) .pickerStyle(.menu) - .onAppear() { - withAnimation { selectedExchange = defaultBaseUrl } - } + .onAppear() { selectedExchange = defaultBaseUrl } .onChange(of: selectedExchange) { selected in - Task { - await loadExchange(selected) - } + Task { await loadExchange(selected) } } - } else { + } else { // TODO: don't show exchange for global currency HStack { Text(title) Text(defaultBaseUrl.trimURL) } .talerFont(.body) -// .task { -// await loadExchange(defaultBaseUrl) -// } } // load defaultBaseUrl let acceptDestination = WithdrawAcceptView(stack: stack.push(), - currencyInfo: $currencyInfo, url: url, + scope: exchange.scopeInfo, amountToTransfer: $amountToTransfer, wireFee: $wireFee, exchange: $exchange) if amountIsEditable { ScrollView { - let shortcutDestination = LazyView { - WithdrawAcceptView(stack: stack.push(), - currencyInfo: $currencyInfo, - url: url, - amountToTransfer: $amountShortcut, - wireFee: $wireFee, - exchange: $exchange) - } + let shortcutDestination = WithdrawAcceptView(stack: stack.push(), + url: url, + scope: exchange.scopeInfo, + amountToTransfer: $amountShortcut, + wireFee: $wireFee, + exchange: $exchange) // TODO: input amount, then let amountLabel = minimalistic ? String(localized: "Amount:") : String(localized: "Amount to withdraw:") AmountInputV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: exchange.scopeInfo, amountAvailable: $amountZero, amountLabel: amountLabel, amountToTransfer: $amountToTransfer, @@ -155,11 +172,11 @@ struct WithdrawURIView: View { feeIsNegative: true, computeFee: computeFeeWithdraw) .background(NavigationLink(destination: shortcutDestination, isActive: $shortcutSelected) - { EmptyView() }.frame(width: 0).opacity(0).hidden() - ) // shortcutDestination + { EmptyView() }.frame(width: 0).opacity(0).hidden() + ) // shortcutDestination .background(NavigationLink(destination: acceptDestination, isActive: $buttonSelected) - { EmptyView() }.frame(width: 0).opacity(0).hidden() - ) // acceptDestination + { EmptyView() }.frame(width: 0).opacity(0).hidden() + ) // acceptDestination } // ScrollView } else { acceptDestination @@ -167,16 +184,13 @@ struct WithdrawURIView: View { } .navigationTitle(navTitle) .task(id: controller.currencyTicker) { - let currency = amountToTransfer.currencyStr - currencyInfo = controller.info2(for: currency, controller.currencyTicker) + currencyInfo = controller.info(for: exchange.scopeInfo, controller.currencyTicker) } .onAppear() { symLog.log("onAppear") DebugViewC.shared.setSheetID(SHEET_WITHDRAWAL) } -// agePicker.setAges(ages: details?.ageRestrictionOptions) - } else { // TODO: error -// symLog.log("no exchangeBaseUrl or no exchange") +// agePicker.setAges(ages: details?.ageRestrictionOptions) } @@ -187,27 +201,7 @@ struct WithdrawURIView: View { let message: String? = nil #endif LoadingView(scopeInfo: nil, message: message) - .task { // this runs only once - symLog.log(".task") - if let uriInfoResponse = try? await model.getWithdrawalDetailsForUriM(url.absoluteString) { - let amount = uriInfoResponse.amount - let currency = amount?.currencyStr ?? uriInfoResponse.currency - amountToTransfer = amount ?? Amount.zero(currency: currency) - amountIsEditable = uriInfoResponse.editableAmount - amountAvailable = uriInfoResponse.maxAmount // may be nil - wireFee = uriInfoResponse.wireFee // may be nil - let baseUrl = uriInfoResponse.defaultExchangeBaseUrl - ?? uriInfoResponse.possibleExchanges[0].exchangeBaseUrl - defaultExchangeBaseUrl = uriInfoResponse.defaultExchangeBaseUrl - possibleExchanges = uriInfoResponse.possibleExchanges - await loadExchange(baseUrl) - symLog.log("\(baseUrl.trimURL) loaded") - await controller.checkCurrencyInfo(for: baseUrl, model: model) - symLog.log("Info(for: \(baseUrl.trimURL) loaded") - } - - // TODO: amount = nil ==> show amount input - } // getWithdrawalDetailsForUriM + .task { await viewDidLoad() } } } } diff --git a/TalerWallet1/Views/Transactions/ManualDetailsV.swift b/TalerWallet1/Views/Transactions/ManualDetailsV.swift @@ -256,6 +256,7 @@ struct ManualDetails_Previews: PreviewProvider { amountRaw: Amount(currency: LONGCURRENCY, cent: 220), transactionId: "someTxID", timestamp: Timestamp(from: 1_666_666_000_000), + scopes: [], txActions: []) let payto = "payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company" let details = WithdrawalDetails(type: .manual, diff --git a/TalerWallet1/Views/Transactions/ThreeAmountsSection.swift b/TalerWallet1/Views/Transactions/ThreeAmountsSection.swift @@ -10,7 +10,7 @@ import taler_swift struct ThreeAmountsSheet: View { // should be in a separate file let stack: CallStack - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo? var common: TransactionCommon var topAbbrev: String var topTitle: String @@ -50,7 +50,7 @@ struct ThreeAmountsSheet: View { // should be in a separate file let txStateLcl = developerMode && pending ? (common.txState.minor?.localizedState ?? majorLcl) : majorLcl ThreeAmountsSection(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, topTitle: topTitle, topAbbrev: topAbbrev, topAmount: raw, @@ -72,7 +72,7 @@ struct ThreeAmountsSheet: View { // should be in a separate file // MARK: - struct ThreeAmountsSection: View { let stack: CallStack - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo? var topTitle: String var topAbbrev: String var topAmount: Amount @@ -113,9 +113,9 @@ struct ThreeAmountsSection: View { .padding(.bottom) } AmountRowV(stack: stack.push(), - currencyInfo: $currencyInfo, title: minimalistic ? topAbbrev : topTitle, amount: topAmount, + scope: scope, isNegative: nil, color: labelColor, large: false) @@ -125,9 +125,9 @@ struct ThreeAmountsSection: View { let title = minimalistic ? String(localized: "Exchange fee (short):", defaultValue: "Fee:", comment: "short version") : String(localized: "Exchange fee (long):", defaultValue: "Fee:", comment: "long version") AmountRowV(stack: stack.push(), - currencyInfo: $currencyInfo, title: title, amount: fee, + scope: scope, isNegative: fee.isZero ? nil : feeIsNegative, color: labelColor, large: false) @@ -135,21 +135,22 @@ struct ThreeAmountsSection: View { } if let bottomAmount { AmountRowV(stack: stack.push(), - currencyInfo: $currencyInfo, title: minimalistic ? bottomAbbrev : bottomTitle, amount: bottomAmount, + scope: scope, isNegative: nil, color: foreColor, large: large) } } - if let baseURL { + let serviceURL = scope?.url ?? baseURL + if let serviceURL { VStack(alignment: .leading) { // TODO: "Issued by" for withdrawals - Text(minimalistic ? "Payment provider:" : "Using payment service provider:") + Text(minimalistic ? "Payment service:" : "Using payment service:") .multilineTextAlignment(.leading) .talerFont(.body) - Text(baseURL.trimURL) + Text(serviceURL.trimURL) .frame(maxWidth: .infinity, alignment: .trailing) .multilineTextAlignment(.center) .talerFont(large ? .title3 : .body) @@ -175,25 +176,30 @@ struct ThreeAmountsSection: View { struct ThreeAmounts_Previews: PreviewProvider { @MainActor struct StateContainer: View { - @State private var currencyInfoD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) - @State private var currencyInfoT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) +// @State private var previewD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) +// @State private var previewT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) var body: some View { + let scope = ScopeInfo.zero(LONGCURRENCY) let common = TransactionCommon(type: .withdrawal, txState: TransactionState(major: .done), amountEffective: Amount(currency: LONGCURRENCY, cent: 10), amountRaw: Amount(currency: LONGCURRENCY, cent: 20), transactionId: "someTxID", timestamp: Timestamp(from: 1_666_666_000_000), - txActions: []) + scopes: [scope], + txActions: [], + kycUrl: nil) // let test = Amount(currency: TESTCURRENCY, cent: 123) // let demo = Amount(currency: DEMOCURRENCY, cent: 123456) - List { ThreeAmountsSheet(stack: CallStack("Preview"), - currencyInfo: $currencyInfoD, - common: common, topAbbrev: "Withdrawal", - topTitle: "Withdrawal", baseURL: DEMOEXCHANGE, noFees: false, + scope: scope, + common: common, + topAbbrev: "Withdrawal", + topTitle: "Withdrawal", + baseURL: DEMOEXCHANGE, + noFees: false, large: 1==0, summary: nil, merchant: nil) .safeAreaInset(edge: .bottom) { Button(String("Preview")) {} diff --git a/TalerWallet1/Views/Transactions/TransactionRowView.swift b/TalerWallet1/Views/Transactions/TransactionRowView.swift @@ -9,7 +9,7 @@ import SwiftUI import taler_swift struct TransactionRowView: View { - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo let transaction : Transaction @Environment(\.sizeCategory) var sizeCategory @@ -56,7 +56,7 @@ struct TransactionRowView: View { let iconBadge = TransactionIconBadge(type: common.type, foreColor: foreColor, done: done, incoming: incoming, shouldConfirm: shouldConfirm, needsKYC: needsKYC) - let amountV = AmountV($currencyInfo, isZero ? common.amountRaw : common.amountEffective, + let amountV = AmountV(scope, isZero ? common.amountRaw : common.amountEffective, isNegative: isZero ? nil : !incoming, strikethrough: !doneOrPending) .foregroundColor(foreColor) @@ -182,14 +182,14 @@ struct TransactionRow_Previews: PreviewProvider { time: Timestamp(from: 1_666_666_000_000)) @MainActor struct StateContainer: View { - @State private var currencyInfoD: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY) - @State private var currencyInfoT: CurrencyInfo = CurrencyInfo.zero(TESTCURRENCY) + @State private var previewD = CurrencyInfo.zero(DEMOCURRENCY) + @State private var previewT = CurrencyInfo.zero(TESTCURRENCY) var body: some View { + let scope = ScopeInfo.zero(DEMOCURRENCY) List { - TransactionRowView(currencyInfo: $currencyInfoD, transaction: withdrawal) - TransactionRowView(currencyInfo: $currencyInfoT, transaction: payment) - + TransactionRowView(scope: scope, transaction: withdrawal) + TransactionRowView(scope: scope, transaction: payment) } } } @@ -202,15 +202,20 @@ struct TransactionRow_Previews: PreviewProvider { // MARK: - extension Transaction { // for PreViews init(incoming: Bool, pending: Bool, id: String, time: Timestamp) { - let effective = incoming ? 480 : 520 + let txState = TransactionState(major: pending ? TransactionMajorState.pending + : TransactionMajorState.done) + let raw = Amount(currency: LONGCURRENCY, cent: 500) + let eff = Amount(currency: LONGCURRENCY, cent: incoming ? 480 : 520) let common = TransactionCommon(type: incoming ? .withdrawal : .payment, txState: TransactionState(major: pending ? TransactionMajorState.pending : TransactionMajorState.done), - amountEffective: Amount(currency: LONGCURRENCY, cent: UInt64(effective)), - amountRaw: Amount(currency: LONGCURRENCY, cent: 5), + amountEffective: eff, + amountRaw: raw, transactionId: id, timestamp: time, - txActions: [.abort]) + scopes: [], + txActions: [.abort], + kycUrl: nil) if incoming { // if pending then manual else bank-integrated let payto = "payto://iban/SANDBOXX/DE159593?receiver-name=Exchange+Company&amount=KUDOS%3A9.99&message=Taler+Withdrawal+J41FQPJGAP1BED1SFSXHC989EN8HRDYAHK688MQ228H6SKBMV0AG" diff --git a/TalerWallet1/Views/Transactions/TransactionSummaryV.swift b/TalerWallet1/Views/Transactions/TransactionSummaryV.swift @@ -19,7 +19,9 @@ extension Transaction { // for Dummys amountRaw: amount, transactionId: EMPTYSTRING, timestamp: now, - txActions: []) + scopes: [], + txActions: [], + kycUrl: nil) self = .dummy(DummyTransaction(common: common)) } } @@ -27,7 +29,7 @@ extension Transaction { // for Dummys struct TransactionSummaryV: View { private let symLog = SymLogV(0) let stack: CallStack - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo? let transactionId: String let reloadAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Transaction) let navTitle: String? @@ -38,6 +40,7 @@ struct TransactionSummaryV: View { let suspendAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Void)? let resumeAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Void)? + @EnvironmentObject private var controller: Controller @Environment(\.colorScheme) private var colorScheme @Environment(\.colorSchemeContrast) private var colorSchemeContrast @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> @@ -179,7 +182,7 @@ struct TransactionSummaryV: View { } TypeDetail(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, transaction: $transaction, hasDone: hasDone) @@ -289,7 +292,7 @@ struct TransactionSummaryV: View { struct TypeDetail: View { let stack: CallStack - @Binding var currencyInfo: CurrencyInfo + let scope: ScopeInfo? @Binding var transaction: Transaction let hasDone: Bool @Environment(\.colorScheme) private var colorScheme @@ -346,7 +349,7 @@ struct TransactionSummaryV: View { details: details) } // ManualDetails or Confirm now (with bank) ThreeAmountsSheet(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, common: common, topAbbrev: String(localized: "Chosen:", comment: "mini"), topTitle: String(localized: "Chosen amount to withdraw:"), @@ -360,7 +363,7 @@ struct TransactionSummaryV: View { case .deposit(let depositTransaction): Group { let details = depositTransaction.details ThreeAmountsSheet(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, common: common, topAbbrev: String(localized: "Deposit:", comment: "mini"), topTitle: String(localized: "Amount to deposit:"), @@ -375,7 +378,7 @@ struct TransactionSummaryV: View { let details = paymentTransaction.details TransactionPayDetailV(paymentTx: paymentTransaction) ThreeAmountsSheet(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, common: common, topAbbrev: String(localized: "Price:", comment: "mini"), topTitle: String(localized: "Price (net):"), @@ -389,7 +392,7 @@ struct TransactionSummaryV: View { case .refund(let refundTransaction): Group { let details = refundTransaction.details // TODO: more details ThreeAmountsSheet(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, common: common, topAbbrev: String(localized: "Refunded:", comment: "mini"), topTitle: String(localized: "Refunded amount:"), @@ -409,17 +412,17 @@ struct TransactionSummaryV: View { .talerFont(.title) let input = details.refreshInputAmount AmountRowV(stack: stack.push(), - currencyInfo: $currencyInfo, title: minimalistic ? "Refreshed:" : "Refreshed amount:", amount: input, + scope: scope, isNegative: nil, color: labelColor, large: true) if let fee = refreshFee(input: input, output: details.refreshOutputAmount) { AmountRowV(stack: stack.push(), - currencyInfo: $currencyInfo, title: minimalistic ? "Fee:" : "Refreshed fee:", amount: fee, + scope: scope, isNegative: fee.isZero ? nil : true, color: labelColor, large: true) @@ -463,7 +466,7 @@ struct TransactionSummaryV: View { // TODO: isSendCoins should show QR only while not yet expired - either set timer or wallet-core should do so and send a state-changed notification if pending { if transaction.isPendingReady { - QRCodeDetails(transaction: transaction, specs: currencyInfo.specs) + QRCodeDetails(transaction: transaction) if hasDone { Text("QR code and link can also be scanned or copied / shared from Transactions later.") .multilineTextAlignment(.leading) @@ -480,7 +483,7 @@ struct TransactionSummaryV: View { let localizedType = transaction.isDone ? transaction.localizedTypePast : transaction.localizedType ThreeAmountsSheet(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, common: common, topAbbrev: localizedType + colon, topTitle: localizedType + colon, @@ -495,7 +498,7 @@ struct TransactionSummaryV: View { case .recoup(let recoupTransaction): Group { let details = recoupTransaction.details // TODO: more details ThreeAmountsSheet(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, common: common, topAbbrev: String(localized: "Recoup:", comment: "mini"), topTitle: String(localized: "Recoup:"), @@ -509,7 +512,7 @@ struct TransactionSummaryV: View { case .denomLoss(let denomLossTransaction): Group { let details = denomLossTransaction.details // TODO: more details ThreeAmountsSheet(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, common: common, topAbbrev: String(localized: "Lost:", comment: "mini"), topTitle: String(localized: "Money lost:"), @@ -527,7 +530,6 @@ struct TransactionSummaryV: View { struct QRCodeDetails: View { var transaction : Transaction - var specs: CurrencySpecification var body: some View { let details = transaction.detailsToShow() let keys = details.keys @@ -538,7 +540,7 @@ struct TransactionSummaryV: View { talerCopyShare: talerURI, incoming: transaction.isP2pIncoming, amount: transaction.common.amountRaw, - specs: specs) + scope: transaction.common.scopes[0]) } } } else if keys.contains(EXCHANGEBASEURL) { diff --git a/TalerWallet1/Views/Transactions/TransactionsListView.swift b/TalerWallet1/Views/Transactions/TransactionsListView.swift @@ -16,12 +16,11 @@ fileprivate let showUpDown = 25 // show up+down buttons in the menubar if li struct TransactionsListView: View { private let symLog = SymLogV(0) let stack: CallStack + let scope: ScopeInfo let balance: Balance // this is the currency to be used @Binding var selectedBalance: Balance? // <- return here the balance when we go to Transactions - @Binding var currencyInfo: CurrencyInfo - let navTitle: String + let navTitle: String? - let scopeInfo: ScopeInfo @Binding var transactions: [Transaction] let reloadAllAction: (_ stack: CallStack) async -> () @@ -41,8 +40,7 @@ struct TransactionsListView: View { List { TransactionsArraySliceV(symLog: symLog, stack: stack.push(), - currencyInfo: $currencyInfo, - scopeInfo: scopeInfo, + scope: scope, transactions: $transactions, reloadAllAction: reloadAllAction, reloadOneAction: reloadOneAction) @@ -70,7 +68,7 @@ struct TransactionsListView: View { }) } } // ScrollViewReader - .navigationTitle(navTitle) + .navigationTitle(navTitle ?? scope.currency) .accessibilityHint(String(localized: "Transaction list")) .task { symLog.log("❗️.task List❗️") @@ -78,7 +76,7 @@ struct TransactionsListView: View { } .overlay { if transactions.isEmpty { - TransactionsEmptyView(stack: stack.push(), currency: scopeInfo.currency) + TransactionsEmptyView(stack: stack.push(), currency: scope.currency) } } .onAppear { @@ -92,8 +90,7 @@ struct TransactionsListView: View { struct TransactionsArraySliceV: View { let symLog: SymLogV? let stack: CallStack - @Binding var currencyInfo: CurrencyInfo - let scopeInfo: ScopeInfo + let scope: ScopeInfo @Binding var transactions: [Transaction] let reloadAllAction: (_ stack: CallStack) async -> () let reloadOneAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Transaction) @@ -113,7 +110,7 @@ struct TransactionsArraySliceV: View { ForEach(transactions, id: \.self) { transaction in let destination = TransactionSummaryV(stack: stack.push(), - currencyInfo: $currencyInfo, + scope: scope, transactionId: transaction.id, reloadAction: reloadOneAction, navTitle: nil, @@ -124,7 +121,7 @@ struct TransactionsArraySliceV: View { suspendAction: suspendAction, resumeAction: resumeAction) let row = NavigationLink { destination } label: { - TransactionRowView(currencyInfo: $currencyInfo, transaction: transaction) + TransactionRowView(scope: scope, transaction: transaction) }.id(transaction.id) if transaction.isDeleteable { row.swipeActions(edge: .trailing) {