diff --git a/TalerWallet1/Views/Actions/Banking/ManualWithdrawDone.swift b/TalerWallet1/Views/Actions/Banking/ManualWithdrawDone.swift index 270b29b1..b1c10184 100755 --- a/TalerWallet1/Views/Actions/Banking/ManualWithdrawDone.swift +++ b/TalerWallet1/Views/Actions/Banking/ManualWithdrawDone.swift @@ -22,8 +22,15 @@ struct ManualWithdrawDone: View { @EnvironmentObject private var controller: Controller @EnvironmentObject private var model: WalletModel + @AppStorage("developerMode") var developerMode: Bool = false @State private var acceptManualWithdrawalResult: AcceptManualWithdrawalResult? @State private var transactionId: String? + /// Why the withdrawal was not accepted, if it was not. + /// + /// `transactionId` stayed nil on failure, so `body` kept taking its + /// `else` and re-rendered the `LoadingView` whose `.task` had already + /// run: a spinner that could never resolve. Issue 11985. + @State private var acceptError: Error? = nil @State private var talerTX: TalerTransaction = TalerTransaction(dummyCurrency: DEMOCURRENCY) let navTitle = String(localized: "Wire Transfer") @@ -31,14 +38,24 @@ struct ManualWithdrawDone: View { @MainActor private func viewDidLoad() async { if transactionId == nil { - if let result = try? await model.acceptManualWithdrawal(amountToTransfer, + do { + let result = try await model.acceptManualWithdrawal(amountToTransfer, baseUrl: baseURL, restrictAge: nil) // No age restriction - { if let url { controller.removeURL(url) } - transactionId = result.transactionId + transactionId = result?.transactionId + if transactionId == nil { + // Success with no body: handled explicitly, because + // falling through is how the permanent spinner comes + // back. + symLog.log("acceptManualWithdrawal returned no response") + acceptError = WalletBackendError.deserializationError + } + } catch { + symLog.log("acceptManualWithdrawal failed: \(error)") + acceptError = error } } } @@ -59,6 +76,9 @@ struct ManualWithdrawDone: View { showDone: .prominent, url: nil, withActions: false) + } else if let acceptError { + ErrorView(stack.push(), error: acceptError, + devMode: developerMode) { dismissTop(stack.push()) } } else { LoadingView(stack: stack.push(), scopeInfo: nil, message: baseURL.trimURL) .task { await viewDidLoad() } diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift index 31952719..ae5f1c5e 100644 --- a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift +++ b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift @@ -21,22 +21,40 @@ struct P2pAcceptDone: View { @EnvironmentObject private var controller: Controller @EnvironmentObject private var model: WalletModel + @AppStorage("developerMode") var developerMode: Bool = false + + /// Why the accept did not go through, if it did not. + /// + /// There was no such state and no branch for it: `body` was + /// unconditionally a `LoadingView` with + /// `navigationBarBackButtonHidden(true)`, and the only way off the + /// screen was the `dismissTop` inside the success path. So a failure + /// assigned nothing, dismissed nothing, and left "Accepting..." up for + /// good -- the `interactiveDismissDisabled()` line below is commented + /// out, which is the only reason a swipe still worked. Issue 11985. + /// + /// The error itself was never silent: both calls use the default + /// `viewHandles: false`, so `sendRequest`'s catch calls `setError` and + /// the global presentation appears. What was missing is that + /// dismissing *that* returned the user to a screen whose `.task` had + /// already run and which could therefore never change again. + @State private var acceptError: Error? = nil + @MainActor private func viewDidLoad() async { - if incoming { - if let _ = try? await model.acceptPeerPushCredit(transactionId) { - if let url { - controller.removeURL(url) - } - dismissTop(stack.push()) + do { + if incoming { + _ = try await model.acceptPeerPushCredit(transactionId) + } else { + _ = try await model.confirmPeerPullDebit(transactionId) } - } else { - if let _ = try? await model.confirmPeerPullDebit(transactionId) { - if let url { - controller.removeURL(url) - } - dismissTop(stack.push()) + if let url { + controller.removeURL(url) } + dismissTop(stack.push()) + } catch { + symLog.log("accept failed: \(error)") + acceptError = error } } @@ -48,11 +66,21 @@ struct P2pAcceptDone: View { let navTitle = incoming ? String(localized: "Received", comment: "Nav Title, short") : String(localized: "Paid", comment: "Nav Title, short") let message = String(localized: "Accepting...", comment: "loading") - LoadingView(stack: stack.push(), scopeInfo: nil, message: message) - .navigationBarBackButtonHidden(true) -// .interactiveDismissDisabled() // can only use "Done" button to dismiss + ZStack { + if let acceptError { + // A way out, which this screen did not have: the back + // button is hidden while it is loading, and on failure the + // loading was permanent. + ErrorView(stack.push(), error: acceptError, + devMode: developerMode) { dismissTop(stack.push()) } + } else { + LoadingView(stack: stack.push(), scopeInfo: nil, message: message) + .navigationBarBackButtonHidden(true) +// .interactiveDismissDisabled() // can only use "Done" button to dismiss + .task { await viewDidLoad() } + } + } .navigationTitle(navTitle) - .task { await viewDidLoad() } .onAppear() { symLog.log("onAppear") DebugViewC.shared.setSheetID(incoming ? SHEET_RCV_P2P_ACCEPT