taler-ios

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

commit 2eec5f04339403829bb9fd40e8e2db5dc487aa0c
parent 7ce06672376df083997693aba832afba5c82ac1e
Author: Marc Stibane <marc@taler.net>
Date:   Thu, 16 Jul 2026 09:45:53 +0200

use ErrorBanner & TimeView

Diffstat:
MTalerWallet1/Controllers/Controller.swift | 5+++++
MTalerWallet1/Views/Main/MainView.swift | 51++++++++++++++++++++++++++++++---------------------
MTalerWallet1/Views/Sheets/URLSheet.swift | 126++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
3 files changed, 124 insertions(+), 58 deletions(-)

diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift @@ -135,6 +135,11 @@ class Controller: ObservableObject { @Published var userAction: Int = 0 // make Action button jump @Published var isConnected: Bool = true + @Published var networkUnavailable: Bool = false + @Published var slowConnection: Bool = false + @Published var stalledConnection: Bool = false + @Published var lastProgressError: RequestProgressError? = nil + @Published var lastProgressPhase: RequestProgressPhase? = nil @Published var oimModeActive: Bool = false @Published var oimSheetActive: Bool = false @Published var diagnosticModeEnabled: Bool = false diff --git a/TalerWallet1/Views/Main/MainView.swift b/TalerWallet1/Views/Main/MainView.swift @@ -40,7 +40,6 @@ struct MainView: View { // @State private var showCameraAlert: Bool = false @State private var scannedCode: Bool = false @State private var innerHeight: CGFloat = .zero - @State private var networkUnavailable = false @State private var backgrounded: Date? // time we go into background @Namespace private var namespace @@ -211,22 +210,11 @@ struct MainView: View { }.animation(.linear(duration: LAUNCHDURATION), value: controller.backendState) VStack { - if networkUnavailable { - Text("Network unavailable!") - .foregroundStyle(.white) - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - .background { - RoundedRectangle(cornerRadius: 15) - .fill(Color.red) - } - .padding(.horizontal) - .transition(.asymmetric( - insertion: .move(edge: .top), - removal: .move(edge: .top) - )) - } // red bar on top - + if controller.networkUnavailable { + ErrorBanner(.networkUnavailable) // red bar on top + } else if controller.slowConnection { + ErrorBanner(.warning5sec) // yellow bar on top + } mainGroup .environmentObject(tabBarModel) // .animation(.default, value: model.error2 == nil) @@ -317,14 +305,35 @@ struct MainView: View { showUrlSheet = true // raise sheet } } } + .onNotification(.RequestProgressPhase) { notification in + if let progress = notification.userInfo?[NOTIFICATIONPHASE] as? RequestProgressPhase { +// print(progress) + switch progress.phase { + case .delayed: + withAnimation(.easeOut(duration: BANNERDURATION)) { + controller.slowConnection = true + controller.stalledConnection = false + } + case .stalled: + withAnimation(.easeIn(duration: BANNERDURATION)) { + controller.slowConnection = false + controller.stalledConnection = true + } + default: + withAnimation(.easeIn(duration: BANNERDURATION)) { + controller.slowConnection = false + controller.stalledConnection = false + } + } } } + .onChange(of: controller.isConnected) { isConnected in if isConnected { - withAnimation(.easeIn(duration: 1.0)) { - networkUnavailable = false + withAnimation(.easeIn(duration: BANNERDURATION)) { + controller.networkUnavailable = false } } else { - withAnimation(.easeOut(duration: 1.0)) { - networkUnavailable = true + withAnimation(.easeOut(duration: BANNERDURATION)) { + controller.networkUnavailable = true } } } .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification, object: nil)) { _ in diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift b/TalerWallet1/Views/Sheets/URLSheet.swift @@ -1,5 +1,5 @@ /* - * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. + * This file is part of GNU Taler, ©2022-26 Taler Systems S.A. * See LICENSE.md */ /** @@ -25,6 +25,7 @@ struct URLSheet: View { @State private var summary = EMPTYSTRING @State private var urlCommand: UrlCommand? @State private var passedURL: URL? + @State private var showAlert: Bool = false init(_ stack: CallStack, selectedBalance: Balance? = nil, urlToOpen: Binding<URL?>? = nil, @@ -48,52 +49,103 @@ struct URLSheet: View { } } + private var dismissAlertButton: some View { + Button("Cancel", role: .cancel) { + showAlert = false + if let lastError = controller.lastProgressError { + controller.lastProgressError = nil + // TODO: cancel wallet-core operation + } + } + } + + private var retryButton: some View { + Button("Retry now") { + showAlert = false + // TODO: retry wallet-core operation + } + } + var body: some View { #if PRINT_CHANGES let _ = Self._printChanges() let _ = symLog.vlog(urlToOpen?.absoluteString) // just to get the # to compare it with .onAppear & onDisappear #endif - if let urlCommand, let passedURL { - switch urlCommand { - case .addExchange: // TODO: just check the ToS - ExchangeListView(stack: stack.push(), url: $passedURL) - case .withdraw: - WithdrawURIView(stack: stack.push(), url: passedURL) - case .withdrawExchange: - WithdrawExchangeV(stack: stack.push(), - selectedBalance: selectedBalance, - url: passedURL) - case .pay: - PaymentView(stack: stack.push(), url: passedURL, - template: false, amountToTransfer: $amountToTransfer, summary: $summary, - amountIsEditable: false, summaryIsEditable: false) - case .payPull: - P2pPayURIView(stack: stack.push(), url: passedURL) - case .payPush: - P2pReceiveURIView(stack: stack.push(), url: passedURL, oimEuro: oimEuro) - case .payTemplate: - PayTemplateV(stack: stack.push(), url: passedURL) - case .refund: - RefundURIView(stack: stack.push(), url: passedURL) + VStack { + if controller.networkUnavailable { + ErrorBanner(.networkUnavailable) // red bar on top + } else if controller.slowConnection { + ErrorBanner(.warning5sec) // yellow bar on top + } + if let urlCommand, let passedURL { + switch urlCommand { + case .addExchange: // TODO: just check the ToS + ExchangeListView(stack: stack.push(), url: $passedURL) + case .withdraw: + WithdrawURIView(stack: stack.push(), url: passedURL) + case .withdrawExchange: + WithdrawExchangeV(stack: stack.push(), + selectedBalance: selectedBalance, + url: passedURL) + case .pay: + PaymentView(stack: stack.push(), url: passedURL, + template: false, amountToTransfer: $amountToTransfer, summary: $summary, + amountIsEditable: false, summaryIsEditable: false) + case .payPull: + P2pPayURIView(stack: stack.push(), url: passedURL) + case .payPush: + P2pReceiveURIView(stack: stack.push(), url: passedURL, oimEuro: oimEuro) + case .payTemplate: + PayTemplateV(stack: stack.push(), url: passedURL) + case .refund: + RefundURIView(stack: stack.push(), url: passedURL) #if GNU_TALER || TALER_NIGHTLY - case .devExperiment: - DevExperimentView(stack: stack.push(), url: passedURL) + case .devExperiment: + DevExperimentView(stack: stack.push(), url: passedURL) #endif - default: // TODO: Error view - let unknown = String(localized: "Unknown command: \(passedURL.absoluteString)") - VStack { - let _ = symLog.log("Unknown command❗️ \(passedURL.absoluteString)") - Text(controller.messageForSheet ?? unknown) - .talerFont(.title) + default: // TODO: Error view + let unknown = String(localized: "Unknown command: \(passedURL.absoluteString)") + VStack { + let _ = symLog.log("Unknown command❗️ \(passedURL.absoluteString)") + Text(controller.messageForSheet ?? unknown) + .talerFont(.title) + } + .navigationTitle(unknown) + } + } else { + let message = String(localized: "Scanning...", comment: "loading") + LoadingView(stack: stack.push(), scopeInfo: nil, message: message) + .task(id: urlToOpen) { + passUrlOnce() } - .navigationTitle(unknown) } - } else { - let message = String(localized: "Scanning...", comment: "loading") - LoadingView(stack: stack.push(), scopeInfo: nil, message: message) - .task(id: urlToOpen) { - passUrlOnce() + } + .alert("Couldn't establish a connection", isPresented: $showAlert, + actions: { + if let lastPhase = controller.lastProgressPhase { + if lastPhase.phase == .retryable { + retryButton + } } + dismissAlertButton + }, message: { + let lastError = controller.lastProgressError + let fallback = String(localized: "It seems that there is a problem with the network.") + Text(lastError?.error.hint ?? fallback) + if let lastPhase = controller.lastProgressPhase { + if lastPhase.phase == .retryable { + if let lastError, let delay = lastError.nextRetryDelay { + TimeView("Next retry in:", seconds: Int(delay.toSeconds())) + .padding(.vertical) + } + } + } + } + ) + .onChange(of: controller.stalledConnection) { isStalled in + if isStalled { + showAlert = true + } } } }