diff --git a/TalerWallet1/Controllers/Controller.swift b/TalerWallet1/Controllers/Controller.swift index 67f56ba1..213f8134 100755 --- a/TalerWallet1/Controllers/Controller.swift +++ b/TalerWallet1/Controllers/Controller.swift @@ -137,6 +137,8 @@ class Controller: ObservableObject { @Published var subscriptions: [TalerToken] = [] @Published var defaultExchanges: [DefaultExchange] = [] @Published var scannedURLs: [ScannedURL] = [] + /// the scanned code the URL sheet is currently showing, so that an error raised for it can offer to remove it + var openedScannedURL: URL? = nil @Published var contacts: [ContactEntry] = [] @Published var mailboxConfiguration: MailboxConfiguration? = nil @Published var messages: [MailboxMessage] = [] diff --git a/TalerWallet1/Views/Actions/ActionsSheet.swift b/TalerWallet1/Views/Actions/ActionsSheet.swift index d91e7e16..faabadd7 100755 --- a/TalerWallet1/Views/Actions/ActionsSheet.swift +++ b/TalerWallet1/Views/Actions/ActionsSheet.swift @@ -30,6 +30,7 @@ struct ScannedURLs: View { controller.talerURI = scannedURL.url // instead pass it via .onChange(of: controller.talerURI) directly to MainView } } + HStack(spacing: 8) { Button(action: action) { HStack { Text(scannedURL.command.localizedCommand) @@ -50,18 +51,21 @@ struct ScannedURLs: View { .padding(.horizontal, 8) } .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center)) - .swipeActions(edge: .trailing) { - Button { - // symLog?.log("deleteAction") - Task { // runs on MainActor - controller.removeURL(scannedURL.url) - } - } label: { - Label("Delete", systemImage: "trash") + // .swipeActions only works on the rows of a List. These rows live in a + // VStack, so the swipe that used to be here could never be triggered + // and a scanned code could not be removed at all. + Button(role: .destructive) { + withAnimation { + controller.removeURL(scannedURL.url) } - .tint(WalletColors().negative) + } label: { + Image(systemName: "trash") + .foregroundColor(WalletColors().negative) } - + .buttonStyle(TalerButtonStyle(type: .bordered, narrow: true, aligned: .center)) + .accessibilityLabel(Text("Remove this scanned code", comment: "a11y")) + .accessibilityIdentifier("button.scanned.remove") + } } }.padding(.bottom, 20) } diff --git a/TalerWallet1/Views/Sheets/Sheet.swift b/TalerWallet1/Views/Sheets/Sheet.swift index 48fbc684..1ca8eaf8 100644 --- a/TalerWallet1/Views/Sheets/Sheet.swift +++ b/TalerWallet1/Views/Sheets/Sheet.swift @@ -95,10 +95,29 @@ struct Sheet: View { .accessibilityValue(idString) } if let error2 = model.error2 { - ErrorView(stack.push(), data: error2, devMode: developerMode) { - model.setError(nil) - logger.log("ErrorSheet dismissTop") - dismissTop(stack.push()) + VStack(spacing: 0) { + ErrorView(stack.push(), data: error2, devMode: developerMode) { + model.setError(nil) + logger.log("ErrorSheet dismissTop") + dismissTop(stack.push()) + } + // An error is all a scanned code ever shows when it cannot be acted on + // (expired, malformed, unknown to the payment service). "OK" closes the + // sheet and leaves the code in the Actions menu, so offer to remove it here. + if let scanned = controller.openedScannedURL, + controller.scannedURLs.contains(where: { $0.url == scanned }) { + Button(role: .destructive) { + controller.removeURL(scanned) + model.setError(nil) + dismissTop(stack.push()) + } label: { + Label(String(localized: "Remove this scanned code", comment: "button"), systemImage: "trash") + } + .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center)) + .accessibilityIdentifier("button.scanned.remove") + .padding(.horizontal) + .padding(.bottom) + } } .background(FullBackground()) } diff --git a/TalerWallet1/Views/Sheets/URLSheet.swift b/TalerWallet1/Views/Sheets/URLSheet.swift index 634207ec..00c955dc 100644 --- a/TalerWallet1/Views/Sheets/URLSheet.swift +++ b/TalerWallet1/Views/Sheets/URLSheet.swift @@ -46,7 +46,41 @@ struct URLSheet: View { } } + /// the scanned code this sheet was opened for, as long as the Actions menu still lists it + private var savedURL: URL? { + guard let passedURL else { return nil } + return controller.scannedURLs.contains(where: { $0.url == passedURL }) ? passedURL : nil + } + var body: some View { + content + .onChange(of: passedURL) { newURL in + controller.openedScannedURL = newURL + } + .onDisappear { + controller.openedScannedURL = nil + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + // A code that cannot be acted on (expired, malformed, unknown to the + // payment service) only ever shows its error here. Without this the + // user could neither use it nor get rid of it. + if let savedURL { + Button(role: .destructive) { + controller.removeURL(savedURL) + dismissTop(stack.push()) + } label: { + Image(systemName: "trash") + } + .accessibilityLabel(Text("Remove this scanned code", comment: "a11y")) + .accessibilityIdentifier("button.scanned.remove.toolbar") + } + } + } + } + + @ViewBuilder + private var content: some View { #if PRINT_CHANGES let _ = Self._printChanges() let _ = symLog.vlog(urlToOpen?.absoluteString) // just to get the # to compare it with .onAppear & onDisappear