From 827076078959cc349ffa683161813bb80ebb5293 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 11 Sep 2024 17:37:54 +0100 Subject: [PATCH 1/8] 13879 CardPaymentService: Vend connection status As preparation for immediately dismissing reader connection modals, we need to provide more granular detail about the current state of the connection process from the CardPresentPaymentService. The first step of that is to make it responsible for the `disconnecting` state, so that we can provide the similar `connectionCancelled` state when a user cancels the connection. The goal is to disable the button while the cancellation of the connection continues, even after the modal has been dismissed. --- .../CardPresentPaymentCardReader.swift | 2 +- .../CardPresentPaymentFacade.swift | 4 +-- .../CardPresentPaymentPreviewService.swift | 3 +- ...PresentPaymentReaderConnectionStatus.swift | 7 ++++ .../CardPresentPaymentService.swift | 36 +++++++++++++------ .../CardReaderConnectionStatusView.swift | 6 ---- .../CardReaderConnectionViewModel.swift | 10 ++---- .../POS/ViewModels/TotalsViewModel.swift | 22 +++++++----- .../ViewModels/TotalsViewModelProtocol.swift | 4 +-- .../WooCommerce.xcodeproj/project.pbxproj | 4 +++ .../Mocks/MockCardPresentPaymentService.swift | 10 ++++-- .../POS/Mocks/MockTotalsViewModel.swift | 4 +-- 12 files changed, 69 insertions(+), 43 deletions(-) create mode 100644 WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentCardReader.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentCardReader.swift index 3760c69a9b9..057637263e1 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentCardReader.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentCardReader.swift @@ -1,6 +1,6 @@ import Foundation -struct CardPresentPaymentCardReader { +struct CardPresentPaymentCardReader: Equatable { let name: String /// The reader's battery level, if available. diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentFacade.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentFacade.swift index 3f028b2e42e..2bcb2fb2b27 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentFacade.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentFacade.swift @@ -8,9 +8,9 @@ protocol CardPresentPaymentFacade { /// This is a long lasting stream, and will not finish during the life of the façade, instead it will publish events for each payment attempt. var paymentEventPublisher: AnyPublisher { get } - /// `connectedReaderPublisher` provides the latest CardReader that was connected. + /// `readerConnectionStatusPublisher` provides the latest connection status for the card reader. /// This is a long lasting stream, and will not finish during the life of the façade. - var connectedReaderPublisher: AnyPublisher { get } + var readerConnectionStatusPublisher: AnyPublisher { get } /// Attempts to a card reader of the specified type. /// If another type of reader is already connected, this will be disconnected automatically. diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift index 305f801afa4..093ff7553df 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentPreviewService.swift @@ -7,7 +7,8 @@ import struct Yosemite.Order struct CardPresentPaymentPreviewService: CardPresentPaymentFacade { let paymentEventPublisher: AnyPublisher = Just(.idle).eraseToAnyPublisher() - let connectedReaderPublisher: AnyPublisher = Just(nil).eraseToAnyPublisher() + let readerConnectionStatusPublisher: AnyPublisher = Just(.disconnected) + .eraseToAnyPublisher() func connectReader(using connectionMethod: CardReaderConnectionMethod) async throws -> CardPresentPaymentReaderConnectionResult { .connected(CardPresentPaymentCardReader(name: "Test reader", batteryLevel: 0.85)) diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift new file mode 100644 index 00000000000..65d608bdf5e --- /dev/null +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift @@ -0,0 +1,7 @@ +import Foundation + +enum CardPresentPaymentReaderConnectionStatus: Equatable { + case disconnected + case connected(CardPresentPaymentCardReader) + case disconnecting +} diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift index 9b309ea764d..c2acb75058e 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift @@ -9,11 +9,13 @@ import protocol Yosemite.StoresManager final class CardPresentPaymentService: CardPresentPaymentFacade { let paymentEventPublisher: AnyPublisher - let connectedReaderPublisher: AnyPublisher + let readerConnectionStatusPublisher: AnyPublisher + + private let connectedReaderPublisher: AnyPublisher private let paymentEventSubject = PassthroughSubject() - private let connectedReaderSubject = PassthroughSubject() + private let readerConnectionStatusSubject = PassthroughSubject() private let onboardingAdaptor: CardPresentPaymentsOnboardingPresenterAdaptor @@ -57,8 +59,19 @@ final class CardPresentPaymentService: CardPresentPaymentFacade { .receive(on: DispatchQueue.main) // These will be used for UI changes, so moving to the Main thread helps. .eraseToAnyPublisher() - connectedReaderPublisher = await Self.createCardReaderConnectionPublisher(stores: stores) - .merge(with: connectedReaderSubject) + let connectedReaderPublisher = await Self.createCardReaderConnectionPublisher(stores: stores) + self.connectedReaderPublisher = connectedReaderPublisher + + readerConnectionStatusPublisher = connectedReaderPublisher + .map({ reader -> CardPresentPaymentReaderConnectionStatus? in + if let reader { + return .connected(reader) + } else { + return nil + } + }) + .compactMap { $0 } + .merge(with: readerConnectionStatusSubject) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } @@ -86,6 +99,8 @@ final class CardPresentPaymentService: CardPresentPaymentFacade { @MainActor func disconnectReader() async { + readerConnectionStatusSubject.send(.disconnecting) + cancelPayment() connectionControllerManager.knownReaderProvider.forgetCardReader() @@ -93,13 +108,12 @@ final class CardPresentPaymentService: CardPresentPaymentFacade { return await withCheckedContinuation { continuation in var nillableContinuation: CheckedContinuation? = continuation - let action = CardPresentPaymentAction.disconnect { [weak self] result in - if case .failure = result { - // Disconnections typically fail because the reader is not connected in the first place. - // Assuming we're disconnected allows further connection attempts, which can resolve the situation. - // Connection attempts with a reader already connected succeed immediately. - self?.connectedReaderSubject.send(nil) - } + let action = CardPresentPaymentAction.disconnect { [weak self] _ in + // Disconnections typically fail because the reader is not connected in the first place. + // Assuming we're disconnected allows further connection attempts, which can resolve the situation. + // Connection attempts with a reader already connected succeed immediately. + // Therefore, send disconnected for both success and failure results. + self?.readerConnectionStatusSubject.send(.disconnected) nillableContinuation?.resume() nillableContinuation = nil } diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift index b43575e07ae..22240964496 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift @@ -1,11 +1,5 @@ import SwiftUI -enum CardReaderConnectionStatus { - case connected - case disconnecting - case disconnected -} - struct CardReaderConnectionStatusView: View { @Environment(\.posBackgroundAppearance) var backgroundAppearance @ObservedObject private var connectionViewModel: CardReaderConnectionViewModel diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift index 3f27880286b..7d3909093c2 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionViewModel.swift @@ -1,7 +1,7 @@ import SwiftUI final class CardReaderConnectionViewModel: ObservableObject { - @Published private(set) var connectionStatus: CardReaderConnectionStatus = .disconnected + @Published private(set) var connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected private let cardPresentPayment: CardPresentPaymentFacade init(cardPresentPayment: CardPresentPaymentFacade) { @@ -23,10 +23,9 @@ final class CardReaderConnectionViewModel: ObservableObject { } func disconnectReader() { - guard connectionStatus == .connected else { + guard case .connected = connectionStatus else { return } - connectionStatus = .disconnecting Task { @MainActor in await cardPresentPayment.disconnectReader() } @@ -35,10 +34,7 @@ final class CardReaderConnectionViewModel: ObservableObject { private extension CardReaderConnectionViewModel { func observeConnectedReaderForStatus() { - cardPresentPayment.connectedReaderPublisher - .map { connectedReader in - connectedReader == nil ? .disconnected: .connected - } + cardPresentPayment.readerConnectionStatusPublisher .assign(to: &$connectionStatus) } } diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 141c931f7d1..31c2ec6175f 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -32,7 +32,7 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { @Published private(set) var paymentState: PaymentState - @Published private(set) var connectionStatus: CardReaderConnectionStatus = .disconnected + @Published private(set) var connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected @Published var formattedCartTotalPrice: String? @Published var formattedOrderTotalPrice: String? @@ -96,7 +96,7 @@ final class TotalsViewModel: ObservableObject, TotalsViewModelProtocol { var paymentStatePublisher: Published.Publisher { $paymentState } var cardPresentPaymentAlertViewModelPublisher: Published.Publisher { $cardPresentPaymentAlertViewModel } var cardPresentPaymentEventPublisher: Published.Publisher { $cardPresentPaymentEvent } - var connectionStatusPublisher: Published.Publisher { $connectionStatus } + var connectionStatusPublisher: Published.Publisher { $connectionStatus } var formattedCartTotalPricePublisher: Published.Publisher { $formattedCartTotalPrice } var formattedOrderTotalPricePublisher: Published.Publisher { $formattedOrderTotalPrice } var formattedOrderTotalTaxPricePublisher: Published.Publisher { $formattedOrderTotalTaxPrice } @@ -238,11 +238,7 @@ private extension TotalsViewModel { private extension TotalsViewModel { func observeConnectedReaderForStatus() { - cardPresentPaymentService.connectedReaderPublisher - .map { connectedReader in - // Note that this does not cover when a reader is disconnecting - connectedReader == nil ? .disconnected: .connected - } + cardPresentPaymentService.readerConnectionStatusPublisher .assign(to: &$connectionStatus) Publishers.CombineLatest4($connectionStatus, $orderState, $cardPresentPaymentInlineMessage, $order) @@ -278,8 +274,16 @@ private extension TotalsViewModel { } func startPaymentWhenReaderConnected() async { - guard connectionStatus == .connected else { - return startPaymentOnReaderConnection = $connectionStatus.filter { $0 == .connected } + guard case .connected = connectionStatus else { + return startPaymentOnReaderConnection = $connectionStatus + .filter { status in + switch status { + case .connected: + return true + case .disconnected, .disconnecting: + return false + } + } .removeDuplicates() .sink { _ in Task { @MainActor [weak self] in diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift index b8eac129cad..30ac8f34f8b 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModelProtocol.swift @@ -6,7 +6,7 @@ protocol TotalsViewModelProtocol { var paymentState: TotalsViewModel.PaymentState { get } var cardPresentPaymentAlertViewModel: PointOfSaleCardPresentPaymentAlertType? { get } var cardPresentPaymentEvent: CardPresentPaymentEvent { get } - var connectionStatus: CardReaderConnectionStatus { get } + var connectionStatus: CardPresentPaymentReaderConnectionStatus { get } var formattedCartTotalPrice: String? { get } var formattedOrderTotalPrice: String? { get } var formattedOrderTotalTaxPrice: String? { get } @@ -15,7 +15,7 @@ protocol TotalsViewModelProtocol { var paymentStatePublisher: Published.Publisher { get } var cardPresentPaymentAlertViewModelPublisher: Published.Publisher { get } var cardPresentPaymentEventPublisher: Published.Publisher { get } - var connectionStatusPublisher: Published.Publisher { get } + var connectionStatusPublisher: Published.Publisher { get } var formattedCartTotalPricePublisher: Published.Publisher { get } var formattedOrderTotalPricePublisher: Published.Publisher { get } var formattedOrderTotalTaxPricePublisher: Published.Publisher { get } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index bc2d179adf4..58e1223c902 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -766,6 +766,7 @@ 2024966A2B0CC97100EE527D /* MockWooPaymentsDepositService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202496692B0CC97100EE527D /* MockWooPaymentsDepositService.swift */; }; 2026ECE92C25D21F00BEF7E4 /* CardPresentPaymentInvalidatablePaymentOrchestrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2026ECE82C25D21F00BEF7E4 /* CardPresentPaymentInvalidatablePaymentOrchestrator.swift */; }; 2027F74F2C8F0858004BDF73 /* PointOfSaleCardPresentPaymentConnectionSuccessAlertViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2027F74E2C8F0858004BDF73 /* PointOfSaleCardPresentPaymentConnectionSuccessAlertViewModelTests.swift */; }; + 2027F7562C90B013004BDF73 /* CardPresentPaymentReaderConnectionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2027F7552C90B013004BDF73 /* CardPresentPaymentReaderConnectionStatus.swift */; }; 202C6C562C7F667700413107 /* POSTextButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202C6C552C7F667700413107 /* POSTextButtonStyle.swift */; }; 202D2A5A2AC5933100E4ABC0 /* TopTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202D2A592AC5933100E4ABC0 /* TopTabView.swift */; }; 203163A92C1B5AA7001C96DA /* PointOfSaleCardPresentPaymentBluetoothRequiredAlertViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 203163A82C1B5AA7001C96DA /* PointOfSaleCardPresentPaymentBluetoothRequiredAlertViewModel.swift */; }; @@ -3849,6 +3850,7 @@ 202496692B0CC97100EE527D /* MockWooPaymentsDepositService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWooPaymentsDepositService.swift; sourceTree = ""; }; 2026ECE82C25D21F00BEF7E4 /* CardPresentPaymentInvalidatablePaymentOrchestrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentInvalidatablePaymentOrchestrator.swift; sourceTree = ""; }; 2027F74E2C8F0858004BDF73 /* PointOfSaleCardPresentPaymentConnectionSuccessAlertViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentConnectionSuccessAlertViewModelTests.swift; sourceTree = ""; }; + 2027F7552C90B013004BDF73 /* CardPresentPaymentReaderConnectionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentPaymentReaderConnectionStatus.swift; sourceTree = ""; }; 202C6C552C7F667700413107 /* POSTextButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSTextButtonStyle.swift; sourceTree = ""; }; 202D2A592AC5933100E4ABC0 /* TopTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopTabView.swift; sourceTree = ""; }; 203163A82C1B5AA7001C96DA /* PointOfSaleCardPresentPaymentBluetoothRequiredAlertViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentBluetoothRequiredAlertViewModel.swift; sourceTree = ""; }; @@ -7686,6 +7688,7 @@ 2004E2C32C076D3800D62521 /* CardPresentPaymentEvent.swift */, 2004E2C52C076D4500D62521 /* CardPresentPaymentResult.swift */, 2004E2C92C07771400D62521 /* CardPresentPaymentReaderConnectionResult.swift */, + 2027F7552C90B013004BDF73 /* CardPresentPaymentReaderConnectionStatus.swift */, 2004E2CB2C07795E00D62521 /* CardPresentPaymentError.swift */, 2004E2CD2C077B0B00D62521 /* CardPresentPaymentCardReader.swift */, 2004E2D12C07878E00D62521 /* CardReaderConnectionMethod.swift */, @@ -15902,6 +15905,7 @@ 209AD3D02AC1EDDA00825D76 /* WooPaymentsDepositsCurrencyOverviewViewModel.swift in Sources */, AE3AA88B290C30B900BE422D /* WebViewControllerConfiguration.swift in Sources */, 26E1BECA251BE5390096D0A1 /* RefundItemTableViewCell.swift in Sources */, + 2027F7562C90B013004BDF73 /* CardPresentPaymentReaderConnectionStatus.swift in Sources */, 26A7C8792BE91F3D00382627 /* WatchDependenciesSynchronizer.swift in Sources */, DE7B479527A38B8F0018742E /* CouponDetailsViewModel.swift in Sources */, E16058F9285876E600E471D4 /* LeftImageTitleSubtitleTableViewCell.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift index f95ab53dc89..6a08e2c73e8 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockCardPresentPaymentService.swift @@ -15,8 +15,14 @@ final class MockCardPresentPaymentService: CardPresentPaymentFacade { $paymentEvent.eraseToAnyPublisher() } - var connectedReaderPublisher: AnyPublisher { - $connectedReader.eraseToAnyPublisher() + var readerConnectionStatusPublisher: AnyPublisher { + $connectedReader.map { reader -> CardPresentPaymentReaderConnectionStatus in + guard let reader else { + return .disconnected + } + return .connected(reader) + } + .eraseToAnyPublisher() } func connectReader(using connectionMethod: CardReaderConnectionMethod) async throws -> CardPresentPaymentReaderConnectionResult { diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift index e75b0945c68..2080e076a9f 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockTotalsViewModel.swift @@ -12,7 +12,7 @@ final class MockTotalsViewModel: TotalsViewModelProtocol { @Published var paymentState: TotalsViewModel.PaymentState = .idle @Published var cardPresentPaymentAlertViewModel: PointOfSaleCardPresentPaymentAlertType? @Published var cardPresentPaymentEvent: CardPresentPaymentEvent = .idle - @Published var connectionStatus: CardReaderConnectionStatus = .disconnected + @Published var connectionStatus: CardPresentPaymentReaderConnectionStatus = .disconnected @Published var formattedCartTotalPrice: String? @Published var formattedOrderTotalPrice: String? @Published var formattedOrderTotalTaxPrice: String? @@ -22,7 +22,7 @@ final class MockTotalsViewModel: TotalsViewModelProtocol { var paymentStatePublisher: Published.Publisher { $paymentState } var cardPresentPaymentAlertViewModelPublisher: Published.Publisher { $cardPresentPaymentAlertViewModel } var cardPresentPaymentEventPublisher: Published.Publisher { $cardPresentPaymentEvent } - var connectionStatusPublisher: Published.Publisher { $connectionStatus } + var connectionStatusPublisher: Published.Publisher { $connectionStatus } var formattedCartTotalPricePublisher: Published.Publisher { $formattedCartTotalPrice } var formattedOrderTotalPricePublisher: Published.Publisher { $formattedOrderTotalPrice } var formattedOrderTotalTaxPricePublisher: Published.Publisher { $formattedOrderTotalTaxPrice } From 3daaa848033522adcd84791e123dc02e2250c64f Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 11 Sep 2024 17:39:15 +0100 Subject: [PATCH 2/8] 13879 Show Please wait when cancelling connection By adding a `cancellingConnection` status for reader connection, we can show `Please wait` and a spinner in the card reader status floating view on the POS --- ...PresentPaymentReaderConnectionStatus.swift | 1 + .../CardPresentPaymentService.swift | 2 + ...PresentPaymentsAlertPresenterAdaptor.swift | 9 ++++ .../CardReaderConnectionStatusView.swift | 45 +++++++++++++------ .../Classes/POS/Presentation/TotalsView.swift | 2 +- .../POS/ViewModels/TotalsViewModel.swift | 4 +- 6 files changed, 46 insertions(+), 17 deletions(-) diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift index 65d608bdf5e..9151432edeb 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentReaderConnectionStatus.swift @@ -3,5 +3,6 @@ import Foundation enum CardPresentPaymentReaderConnectionStatus: Equatable { case disconnected case connected(CardPresentPaymentCardReader) + case cancellingConnection case disconnecting } diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift index c2acb75058e..3d96bf7a8d4 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentService.swift @@ -71,6 +71,7 @@ final class CardPresentPaymentService: CardPresentPaymentFacade { } }) .compactMap { $0 } + .merge(with: paymentAlertsPresenterAdaptor.readerConnectionStatusPublisher) .merge(with: readerConnectionStatusSubject) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() @@ -92,6 +93,7 @@ final class CardPresentPaymentService: CardPresentPaymentFacade { }))) return .connected(connectedReader) case .canceled: + readerConnectionStatusSubject.send(.disconnected) paymentEventSubject.send(.idle) return .canceled } diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift index cd4d38c39f0..9a3df57f0b6 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift @@ -5,13 +5,17 @@ import enum Yosemite.ServerSidePaymentCaptureError final class CardPresentPaymentsAlertPresenterAdaptor: CardPresentPaymentAlertsPresenting { typealias AlertDetails = CardPresentPaymentEventDetails let paymentEventPublisher: AnyPublisher + let readerConnectionStatusPublisher: AnyPublisher private let paymentEventSubject: PassthroughSubject = PassthroughSubject() + private let readerConnectionStatusSubject: PassthroughSubject = PassthroughSubject() + private var latestReaderConnectionHandler: ((String?) -> Void)? init() { paymentEventPublisher = paymentEventSubject.eraseToAnyPublisher() + readerConnectionStatusPublisher = readerConnectionStatusSubject.eraseToAnyPublisher() } func present(viewModel eventDetails: CardPresentPaymentEventDetails) { @@ -35,6 +39,11 @@ final class CardPresentPaymentsAlertPresenterAdaptor: CardPresentPaymentAlertsPr cancelPayment() self?.paymentEventSubject.send(.idle) }))) + case .scanningForReaders(let endSearch): + paymentEventSubject.send(.show(eventDetails: .scanningForReaders(endSearch: { [weak self] in + self?.readerConnectionStatusSubject.send(.cancellingConnection) + endSearch() + }))) default: paymentEventSubject.send(.show(eventDetails: eventDetails)) } diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift index 22240964496..4a43c58af0f 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift @@ -39,17 +39,9 @@ struct CardReaderConnectionStatusView: View { .frame(maxHeight: .infinity) } case .disconnecting: - HStack(spacing: Constants.buttonImageAndTextSpacing) { - ProgressView() - .progressViewStyle(POSProgressViewStyle( - size: Constants.disconnectingProgressIndicatorDimension * scale, - lineWidth: Constants.disconnectingProgressIndicatorLineWidth * scale - )) - Text(Localization.readerDisconnecting) - .foregroundColor(connectedFontColor) - } - .padding(.horizontal, Constants.horizontalPadding) - .frame(maxHeight: .infinity) + progressIndicatingCardReaderStatus(title: Localization.readerDisconnecting) + case .cancellingConnection: + progressIndicatingCardReaderStatus(title: Localization.pleaseWait) case .disconnected: Button { connectionViewModel.connectReader() @@ -76,6 +68,23 @@ struct CardReaderConnectionStatusView: View { } } +private extension CardReaderConnectionStatusView { + @ViewBuilder + func progressIndicatingCardReaderStatus(title: String) -> some View { + HStack(spacing: Constants.buttonImageAndTextSpacing) { + ProgressView() + .progressViewStyle(POSProgressViewStyle( + size: Constants.progressIndicatorDimension * scale, + lineWidth: Constants.progressIndicatorLineWidth * scale + )) + Text(title) + .foregroundColor(connectedFontColor) + } + .padding(.horizontal, Constants.horizontalPadding) + .frame(maxHeight: .infinity) + } +} + private extension CardReaderConnectionStatusView { var connectedFontColor: Color { switch backgroundAppearance { @@ -100,8 +109,8 @@ private extension CardReaderConnectionStatusView { enum Constants { static let buttonImageAndTextSpacing: CGFloat = 12 static let imageDimension: CGFloat = 12 - static let disconnectingProgressIndicatorDimension: CGFloat = 10 - static let disconnectingProgressIndicatorLineWidth: CGFloat = 2 + static let progressIndicatorDimension: CGFloat = 10 + static let progressIndicatorLineWidth: CGFloat = 2 static let font = POSFontStyle.posDetailEmphasized static let horizontalPadding: CGFloat = 24 static let overlayRadius: CGFloat = 4 @@ -137,7 +146,15 @@ private extension CardReaderConnectionStatusView { static let disconnectCardReader = NSLocalizedString( "pointOfSale.floatingButtons.disconnectCardReader.button.title", value: "Disconnect Reader", - comment: "The title of the menu button to disconnect a connected card reader, as confirmation.") + comment: "The title of the menu button to disconnect a connected card reader, as confirmation." + ) + + static let pleaseWait = NSLocalizedString( + "pointOfSale.floatingButtons.cancellingConnection.pleaseWait.title", + value: "Please wait", + comment: "The title of the floating button to indicate that the reader is not ready for another " + + "connection, usually because a connection has just been cancelled" + ) } } diff --git a/WooCommerce/Classes/POS/Presentation/TotalsView.swift b/WooCommerce/Classes/POS/Presentation/TotalsView.swift index 237ae471f65..069057e8121 100644 --- a/WooCommerce/Classes/POS/Presentation/TotalsView.swift +++ b/WooCommerce/Classes/POS/Presentation/TotalsView.swift @@ -231,7 +231,7 @@ private extension TotalsView { @ViewBuilder private var cardReaderView: some View { switch viewModel.connectionStatus { - case .connected, .disconnecting: + case .connected, .disconnecting, .cancellingConnection: if let inlinePaymentMessage = viewModel.cardPresentPaymentInlineMessage { HStack(alignment: .center) { Spacer() diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 31c2ec6175f..5caeebad031 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -253,7 +253,7 @@ private extension TotalsViewModel { } switch connectionStatus { - case .connected, .disconnecting: + case .connected, .disconnecting, .cancellingConnection: return message != nil case .disconnected: // Since the reader is disconnected, this will show the "Connect your reader" CTA button view. @@ -280,7 +280,7 @@ private extension TotalsViewModel { switch status { case .connected: return true - case .disconnected, .disconnecting: + case .disconnected, .disconnecting, .cancellingConnection: return false } } From ba0b7f3a08093a68c9e529fb8a09699f24e3f39c Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Wed, 11 Sep 2024 17:44:46 +0100 Subject: [PATCH 3/8] 13912 Cancel reader connection on modal dismiss --- ...intOfSaleCardPresentPaymentAlertType.swift | 48 +++++++++++++++++-- .../PointOfSaleDashboardView.swift | 5 +- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/Card Present Payments/Connection Alerts/PointOfSaleCardPresentPaymentAlertType.swift b/WooCommerce/Classes/POS/Presentation/Card Present Payments/Connection Alerts/PointOfSaleCardPresentPaymentAlertType.swift index 1dd82c056bf..2ec59c38b84 100644 --- a/WooCommerce/Classes/POS/Presentation/Card Present Payments/Connection Alerts/PointOfSaleCardPresentPaymentAlertType.swift +++ b/WooCommerce/Classes/POS/Presentation/Card Present Payments/Connection Alerts/PointOfSaleCardPresentPaymentAlertType.swift @@ -26,11 +26,49 @@ enum PointOfSaleCardPresentPaymentAlertType: Hashable, Identifiable { case connectingFailedUpdatePostalCode(viewModel: PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeAlertViewModel) case connectionSuccess(viewModel: PointOfSaleCardPresentPaymentConnectionSuccessAlertViewModel) - var isDismissDisabled: Bool { - if case .connectionSuccess = self { - return false - } else { - return true + var onDismiss: (() -> Void)? { + switch self { + case .scanningForReaders(let viewModel): + return viewModel.buttonViewModel.actionHandler + case .scanningFailed(let viewModel): + return viewModel.buttonViewModel.actionHandler + case .bluetoothRequired(let viewModel): + return viewModel.dismissButtonViewModel.actionHandler + case .foundReader(let viewModel): + return viewModel.cancelSearchButton.actionHandler + case .foundMultipleReaders(let viewModel): + return viewModel.cancelSearch + case .requiredReaderUpdateInProgress(let viewModel): + return viewModel.cancelReaderUpdate + case .optionalReaderUpdateInProgress(let viewModel): + return viewModel.cancelReaderUpdate + case .readerUpdateCompletion: + // We only support in-line updates at the moment, and they automatically move on to connecting the reader. + return nil + case .updateFailed(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .updateFailedNonRetryable(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .updateFailedLowBattery(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .connectingToReader: + return nil + case .connectingFailed(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .connectingFailedNonRetryable(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .connectingFailedChargeReader(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .connectingFailedUpdateAddress(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .connectingFailedUpdatePostalCode(let viewModel): + return viewModel.cancelButtonViewModel.actionHandler + case .connectionSuccess(let viewModel): + return viewModel.buttonViewModel.actionHandler } } + + var isDismissDisabled: Bool { + onDismiss == nil + } } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 30b4182cf4d..fcac9413517 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -58,7 +58,10 @@ struct PointOfSaleDashboardView: View { .animation(.easeInOut(duration: Constants.connectivityAnimationDuration), value: viewModel.showsConnectivityError) .background(Color.posPrimaryBackground) .navigationBarBackButtonHidden(true) - .posModal(item: $totalsViewModel.cardPresentPaymentAlertViewModel) { alertType in + .posModal(item: $totalsViewModel.cardPresentPaymentAlertViewModel, + onDismiss: { + totalsViewModel.cardPresentPaymentAlertViewModel?.onDismiss?() + }) { alertType in PointOfSaleCardPresentPaymentAlert(alertType: alertType) .posInteractiveDismissDisabled(alertType.isDismissDisabled) } From 6ddf02a74837e3f94ac95dc03244c2f96ae0c08f Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Thu, 19 Sep 2024 16:07:21 +0100 Subject: [PATCH 4/8] 13879 Dismiss modals without waiting for cancellation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader connection modals were previously dismissed by `idle` being sent to the payment events stream when the cancellation action completed. This meant that the `x` button in the ui would appear to take a second or two to dismiss the modal. Now that we show `Please wait` while there’s a cancellation in progress, there’s less risk of trying to connect to a reader while a cancellation’s ongoing. That means we can dismiss the connection modal immediately, and allow the merchant to do other things. --- ...PresentPaymentEventPresentationStyle.swift | 71 +++++++++++++++---- .../POS/ViewModels/TotalsViewModel.swift | 6 +- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyle.swift b/WooCommerce/Classes/POS/Presentation/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyle.swift index 9462b166e95..87ec641b092 100644 --- a/WooCommerce/Classes/POS/Presentation/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyle.swift +++ b/WooCommerce/Classes/POS/Presentation/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyle.swift @@ -10,6 +10,7 @@ enum PointOfSaleCardPresentPaymentEventPresentationStyle { let formattedOrderTotalPrice: String? let paymentCaptureErrorTryAgainAction: () -> Void let paymentCaptureErrorNewOrderAction: () -> Void + let dismissReaderConnectionModal: () -> Void } /// Determines the appropriate payment alert/message type and creates its view model. @@ -28,19 +29,28 @@ enum PointOfSaleCardPresentPaymentEventPresentationStyle { /// Connection alerts case .scanningForReaders(let endSearch): self = .alert(.scanningForReaders( - viewModel: PointOfSaleCardPresentPaymentScanningForReadersAlertViewModel(endSearchAction: endSearch))) + viewModel: PointOfSaleCardPresentPaymentScanningForReadersAlertViewModel(endSearchAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .scanningFailed(let error, let endSearch): self = .alert(.scanningFailed( viewModel: PointOfSaleCardPresentPaymentScanningFailedAlertViewModel( error: error, - endSearchAction: endSearch))) + endSearchAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .bluetoothRequired(let error, let endSearch): self = .alert(.bluetoothRequired( viewModel: PointOfSaleCardPresentPaymentBluetoothRequiredAlertViewModel( error: error, - endSearch: endSearch))) + endSearch: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .connectingToReader: self = .alert(.connectingToReader(viewModel: PointOfSaleCardPresentPaymentConnectingToReaderAlertViewModel())) @@ -50,25 +60,37 @@ enum PointOfSaleCardPresentPaymentEventPresentationStyle { viewModel: PointOfSaleCardPresentPaymentConnectingFailedAlertViewModel( error: error, retryButtonAction: retrySearch, - cancelButtonAction: endSearch))) + cancelButtonAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .connectingFailedNonRetryable(let error, let endSearch): self = .alert(.connectingFailedNonRetryable( viewModel: PointOfSaleCardPresentPaymentConnectingFailedNonRetryableAlertViewModel( error: error, - cancelAction: endSearch))) + cancelAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .connectingFailedUpdatePostalCode(let retrySearch, let endSearch): self = .alert(.connectingFailedUpdatePostalCode( viewModel: PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeAlertViewModel( retryButtonAction: retrySearch, - cancelButtonAction: endSearch))) + cancelButtonAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .connectingFailedChargeReader(let retrySearch, let endSearch): self = .alert(.connectingFailedChargeReader( viewModel: PointOfSaleCardPresentPaymentConnectingFailedChargeReaderAlertViewModel( retryButtonAction: retrySearch, - cancelButtonAction: endSearch))) + cancelButtonAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .connectingFailedUpdateAddress(let wcSettingsAdminURL, let showsInAuthenticatedWebView, let retrySearch, let endSearch): self = .alert(.connectingFailedUpdateAddress( @@ -76,7 +98,10 @@ enum PointOfSaleCardPresentPaymentEventPresentationStyle { settingsAdminUrl: wcSettingsAdminURL, showsInAuthenticatedWebView: showsInAuthenticatedWebView, retrySearchAction: retrySearch, - cancelSearchAction: endSearch))) + cancelSearchAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .foundReader(let name, let connect, let continueSearch, let endSearch): self = .alert(.foundReader( @@ -84,7 +109,10 @@ enum PointOfSaleCardPresentPaymentEventPresentationStyle { readerName: name, connectAction: connect, continueSearchAction: continueSearch, - endSearchAction: endSearch))) + endSearchAction: { + endSearch() + dependencies.dismissReaderConnectionModal() + }))) case .foundMultipleReaders(let readerIDs, let selectionHandler): self = .alert(.foundMultipleReaders( @@ -100,11 +128,17 @@ enum PointOfSaleCardPresentPaymentEventPresentationStyle { .alert(.requiredReaderUpdateInProgress( viewModel: PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressAlertViewModel( progress: progress, - cancel: cancelUpdate))) : + cancel: { + cancelUpdate?() + dependencies.dismissReaderConnectionModal() + }))) : .alert(.optionalReaderUpdateInProgress( viewModel: PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressAlertViewModel( progress: progress, - cancel: cancelUpdate))) + cancel: { + cancelUpdate?() + dependencies.dismissReaderConnectionModal() + }))) } @@ -112,18 +146,27 @@ enum PointOfSaleCardPresentPaymentEventPresentationStyle { self = .alert(.updateFailed( viewModel: PointOfSaleCardPresentPaymentReaderUpdateFailedAlertViewModel( retryAction: tryAgain, - cancelUpdateAction: cancelUpdate))) + cancelUpdateAction: { + cancelUpdate() + dependencies.dismissReaderConnectionModal() + }))) case .updateFailedNonRetryable(let cancelUpdate): self = .alert(.updateFailedNonRetryable( viewModel: PointOfSaleCardPresentPaymentReaderUpdateFailedNonRetryableAlertViewModel( - cancelUpdateAction: cancelUpdate))) + cancelUpdateAction: { + cancelUpdate() + dependencies.dismissReaderConnectionModal() + }))) case .updateFailedLowBattery(let batteryLevel, let cancelUpdate): self = .alert(.updateFailedLowBattery( viewModel: PointOfSaleCardPresentPaymentReaderUpdateFailedLowBatteryAlertViewModel( batteryLevel: batteryLevel, - cancelUpdateAction: cancelUpdate))) + cancelUpdateAction: { + cancelUpdate() + dependencies.dismissReaderConnectionModal() + }))) case .connectionSuccess(let done): self = .alert(.connectionSuccess( diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 5caeebad031..51a850e877f 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -372,7 +372,11 @@ private extension TotalsViewModel { paymentCaptureErrorTryAgainAction: cancelThenCollectPaymentWithWeakSelf, paymentCaptureErrorNewOrderAction: { [weak self] in self?.startNewOrder() - }) + }, + dismissReaderConnectionModal: { [weak self] in + self?.cardPresentPaymentAlertViewModel = nil + } + ) } func cancelThenCollectPayment() { From eee5bac7ba856e13348657bd62b07cfecd7bfffe Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 20 Sep 2024 12:57:18 +0100 Subject: [PATCH 5/8] 13879 Add documentation as suggested in PR review --- WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 51a850e877f..bd4996c8462 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -272,7 +272,10 @@ private extension TotalsViewModel { } } } - + + /// Starts a payment immediately if a reader is connected. + /// Otherwise, schedules a payment to start the next time a reader connects. + /// Note that any schedlued payments are cancelled by `cancelReaderPreparation` when the TotalsView goes offscreen. func startPaymentWhenReaderConnected() async { guard case .connected = connectionStatus else { return startPaymentOnReaderConnection = $connectionStatus From 0ae141a94498e0dd951e639ea7ad7b6326143b0d Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Fri, 20 Sep 2024 13:31:35 +0100 Subject: [PATCH 6/8] 13879 `Please Wait` on cancel from found reader --- .../CardPresentPaymentsAlertPresenterAdaptor.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift index 9a3df57f0b6..6da0248685a 100644 --- a/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift +++ b/WooCommerce/Classes/POS/Card Present Payments/CardPresentPaymentsAlertPresenterAdaptor.swift @@ -44,6 +44,14 @@ final class CardPresentPaymentsAlertPresenterAdaptor: CardPresentPaymentAlertsPr self?.readerConnectionStatusSubject.send(.cancellingConnection) endSearch() }))) + case .foundReader(let name, let connect, let continueSearch, let endSearch): + paymentEventSubject.send(.show(eventDetails: .foundReader(name: name, + connect: connect, + continueSearch: continueSearch, + endSearch: { [weak self] in + self?.readerConnectionStatusSubject.send(.cancellingConnection) + endSearch() + }))) default: paymentEventSubject.send(.show(eventDetails: eventDetails)) } From cb1286121e4b6bd04f70f46b9cd7df3e21f320f5 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Sat, 21 Sep 2024 07:49:50 +0100 Subject: [PATCH 7/8] Fix whitespace --- .../CardReaderConnection/CardReaderConnectionStatusView.swift | 2 +- WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift index 4a43c58af0f..8be89e5047b 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/CardReaderConnectionStatusView.swift @@ -153,7 +153,7 @@ private extension CardReaderConnectionStatusView { "pointOfSale.floatingButtons.cancellingConnection.pleaseWait.title", value: "Please wait", comment: "The title of the floating button to indicate that the reader is not ready for another " + - "connection, usually because a connection has just been cancelled" + "connection, usually because a connection has just been cancelled" ) } } diff --git a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift index 40c632a4e7b..42099ca636e 100644 --- a/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift +++ b/WooCommerce/Classes/POS/ViewModels/TotalsViewModel.swift @@ -276,7 +276,7 @@ private extension TotalsViewModel { } } } - + /// Starts a payment immediately if a reader is connected. /// Otherwise, schedules a payment to start the next time a reader connects. /// Note that any schedlued payments are cancelled by `cancelReaderPreparation` when the TotalsView goes offscreen. From d93634450374a8d6775698b754b997a310d5f1d6 Mon Sep 17 00:00:00 2001 From: Josh Heald Date: Sat, 21 Sep 2024 08:26:54 +0100 Subject: [PATCH 8/8] 13879 Add test for dismiss handling --- ...ntPaymentEventPresentationStyleTests.swift | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyleTests.swift b/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyleTests.swift index e942dc5754e..b8d9c6fc487 100644 --- a/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyleTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Card Present Payments/PointOfSaleCardPresentPaymentEventPresentationStyleTests.swift @@ -65,7 +65,7 @@ final class PointOfSaleCardPresentPaymentEventPresentationStyleTests: XCTestCase let dependencies = createPresentationStyleDependencies(nonRetryableErrorExitAction: { spyTryAnotherPaymentMethod = true }) // When - let presentationStyle = PointOfSaleCardPresentPaymentEventPresentationStyle( + let presentationStyle = PointOfSaleCardPresentPaymentEventPresentationStyle( for: eventDetails, dependencies: dependencies) @@ -126,18 +126,44 @@ final class PointOfSaleCardPresentPaymentEventPresentationStyleTests: XCTestCase XCTAssertTrue(spyPaymentCaptureErrorNewOrderCalled) } + func test_presentationStyle_for_scanningForReader_is_alert_scanningForReader_with_correctActions() { + // Given + let eventDetails = CardPresentPaymentEventDetails.scanningForReaders(endSearch: {}) + var spyDismissReaderConnectionModalActionCalled = false + let dependencies = createPresentationStyleDependencies( + dismissReaderConnectionModalAction: { + spyDismissReaderConnectionModalActionCalled = true + } + ) + + // When + let presentationStyle = PointOfSaleCardPresentPaymentEventPresentationStyle( + for: eventDetails, + dependencies: dependencies) + + // Then + guard case .alert(.scanningForReaders(let viewModel)) = presentationStyle else { + return XCTFail("Expected scanning for readers alert not found") + } + + viewModel.buttonViewModel.actionHandler() + XCTAssertTrue(spyDismissReaderConnectionModalActionCalled) + } + func createPresentationStyleDependencies( tryPaymentAgainBackToCheckoutAction: @escaping () -> Void = {}, nonRetryableErrorExitAction: @escaping () -> Void = {}, formattedOrderTotalPrice: String? = nil, paymentCaptureErrorTryAgainAction: @escaping () -> Void = {}, - paymentCaptureErrorNewOrderAction: @escaping () -> Void = {}) -> PointOfSaleCardPresentPaymentEventPresentationStyle.Dependencies { + paymentCaptureErrorNewOrderAction: @escaping () -> Void = {}, + dismissReaderConnectionModalAction: @escaping () -> Void = {}) -> PointOfSaleCardPresentPaymentEventPresentationStyle.Dependencies { PointOfSaleCardPresentPaymentEventPresentationStyle.Dependencies( tryPaymentAgainBackToCheckoutAction: tryPaymentAgainBackToCheckoutAction, nonRetryableErrorExitAction: nonRetryableErrorExitAction, formattedOrderTotalPrice: formattedOrderTotalPrice, paymentCaptureErrorTryAgainAction: paymentCaptureErrorTryAgainAction, - paymentCaptureErrorNewOrderAction: paymentCaptureErrorNewOrderAction + paymentCaptureErrorNewOrderAction: paymentCaptureErrorNewOrderAction, + dismissReaderConnectionModal: dismissReaderConnectionModalAction ) }