commit 2eec5f04339403829bb9fd40e8e2db5dc487aa0c
parent 7ce06672376df083997693aba832afba5c82ac1e
Author: Marc Stibane <marc@taler.net>
Date: Thu, 16 Jul 2026 09:45:53 +0200
use ErrorBanner & TimeView
Diffstat:
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
+ }
}
}
}