diff --git a/TalerWallet1/Views/Sheets/Payment/PaymentScan.swift b/TalerWallet1/Views/Sheets/Payment/PaymentScan.swift index 6cf7b899..95c8d184 100644 --- a/TalerWallet1/Views/Sheets/Payment/PaymentScan.swift +++ b/TalerWallet1/Views/Sheets/Payment/PaymentScan.swift @@ -103,7 +103,8 @@ struct PaymentScan: View, Sendable { hasDone: true, // conclude payment showDone: .prominent, url: url, - withActions: false) + withActions: false, + expectPayDialog: true) // reserve the commit slot #if TALER_NIGHTLY2 // if let preparePayResult { let status = preparePayResult.status diff --git a/TalerWallet1/Views/Transactions/TransactionSummaryList.swift b/TalerWallet1/Views/Transactions/TransactionSummaryList.swift index d475186d..7a609769 100755 --- a/TalerWallet1/Views/Transactions/TransactionSummaryList.swift +++ b/TalerWallet1/Views/Transactions/TransactionSummaryList.swift @@ -35,6 +35,14 @@ struct TransactionSummaryList: View { let showDone: TalerButtonStyleType? let url: URL? // the scanned talerURL from PaymentView let withActions: Bool // true if called from pending or history, or refund + /// true when the caller already knows this sheet is going to be a payment + /// dialog, before wallet-core has said so. + /// + /// Without it the bar cannot reserve the commit slot: for the first few + /// hundred milliseconds `talerTX` is still the dummy transaction, so + /// `isPayDialog` is false and the bar shows "Done" -- in the very place + /// that is about to hold "Pay now". + var expectPayDialog: Bool = false @EnvironmentObject private var controller: Controller @EnvironmentObject private var model: WalletModel @@ -346,26 +354,58 @@ struct TransactionSummaryList: View { .listStyle(myListStyle.style).anyView .background(FullBackground()) .navigationBarBackButtonHidden(hasDone) - .interactiveDismissDisabled(hasDone && !isPayDialog) // can only use "Done" button to dismiss + // "Done" is the only way out of a concluded transaction -- but a + // payment sheet whose commit button is still disabled must not be a + // trap, so a sheet the caller expects to become a pay dialog stays + // swipe-dismissable. + .interactiveDismissDisabled(hasDone && !isPayDialog && !expectPayDialog) .safeAreaInset(edge: .bottom) { let cancelButton = Button("Cancel") { dismissTop(stack.push()) } .buttonStyle(TalerButtonStyle(type: .bordered)) .padding(is26 ? .horizontal : .all) + // A payment sheet's bar has two slots that never trade places: + // the commit action on top, the dismiss action below it. + // + // It used to have one slot, and that slot showed "Done", then + // "Cancel", then "Pay now" as wallet-core answered -- + // 118 ms apart in a recorded run, which is less than the time + // between deciding to press something and pressing it. Both + // directions hurt: aiming for "Done" and paying, or aiming for + // "Pay" and instead filing the order away unpaid among the + // scanned codes, where it is hard to find again. + // + // So the commit slot exists from the start and is merely + // disabled until the amount is known, and "Cancel" has a place + // of its own instead of borrowing the commit slot. + let payPlaceholder = Button(String(localized: "Pay…", + comment: "disabled until wallet-core returns the amount")) { } + .buttonStyle(TalerButtonStyle(type: .prominent, disabled: true)) + .disabled(true) + .accessibilityHint(Text("Waiting for the amount to pay.", + comment: "a11y")) + .padding(is26 ? .horizontal : .all) let bottomButton = ZStack { - if isPayDialog, case .payment(let paymentTransaction) = talerTX { - let details = paymentTransaction.details - if let effective, let terms = details.contractTerms, isFinished == false { - let formatted = effective.formatted(currencyInfo) - PaySafeArea(symLog: symLog, - stack: stack.push(), - terms: terms, - amountString: formatted.0, - amountA11y: formatted.1, - isFinished: $isFinished, - payNow: $payNow) - } else { + if expectPayDialog || isPayDialog { + VStack(spacing: 0) { + if isPayDialog, case .payment(let paymentTransaction) = talerTX { + let details = paymentTransaction.details + if let effective, let terms = details.contractTerms, isFinished == false { + let formatted = effective.formatted(currencyInfo) + PaySafeArea(symLog: symLog, + stack: stack.push(), + terms: terms, + amountString: formatted.0, + amountA11y: formatted.1, + isFinished: $isFinished, + payNow: $payNow) + } else { + payPlaceholder + } + } else { + payPlaceholder + } cancelButton - } // Cancel + } } else if let showDone { Button("Done") { dismissTop(stack.push()) } .buttonStyle(TalerButtonStyle(type: showDone))