Skip to content

Commit

Permalink
[Woo POS] Dismiss connection modals immediately while cancelling conn…
Browse files Browse the repository at this point in the history
…ection (#14000)
  • Loading branch information
joshheald authored Sep 21, 2024
2 parents 8e732a3 + d936344 commit b2d380c
Show file tree
Hide file tree
Showing 18 changed files with 261 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

struct CardPresentPaymentCardReader {
struct CardPresentPaymentCardReader: Equatable {
let name: String

/// The reader's battery level, if available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CardPresentPaymentEvent, Never> { 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<CardPresentPaymentCardReader?, Never> { get }
var readerConnectionStatusPublisher: AnyPublisher<CardPresentPaymentReaderConnectionStatus, Never> { get }

/// Attempts to a card reader of the specified type.
/// If another type of reader is already connected, this will be disconnected automatically.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import struct Yosemite.Order
struct CardPresentPaymentPreviewService: CardPresentPaymentFacade {
let paymentEventPublisher: AnyPublisher<CardPresentPaymentEvent, Never> = Just(.idle).eraseToAnyPublisher()

let connectedReaderPublisher: AnyPublisher<CardPresentPaymentCardReader?, Never> = Just(nil).eraseToAnyPublisher()
let readerConnectionStatusPublisher: AnyPublisher<CardPresentPaymentReaderConnectionStatus, Never> = Just(.disconnected)
.eraseToAnyPublisher()

func connectReader(using connectionMethod: CardReaderConnectionMethod) async throws -> CardPresentPaymentReaderConnectionResult {
.connected(CardPresentPaymentCardReader(name: "Test reader", batteryLevel: 0.85))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

enum CardPresentPaymentReaderConnectionStatus: Equatable {
case disconnected
case connected(CardPresentPaymentCardReader)
case cancellingConnection
case disconnecting
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import protocol Yosemite.StoresManager
final class CardPresentPaymentService: CardPresentPaymentFacade {
let paymentEventPublisher: AnyPublisher<CardPresentPaymentEvent, Never>

let connectedReaderPublisher: AnyPublisher<CardPresentPaymentCardReader?, Never>
let readerConnectionStatusPublisher: AnyPublisher<CardPresentPaymentReaderConnectionStatus, Never>

private let connectedReaderPublisher: AnyPublisher<CardPresentPaymentCardReader?, Never>

private let paymentEventSubject = PassthroughSubject<CardPresentPaymentEvent, Never>()

private let connectedReaderSubject = PassthroughSubject<CardPresentPaymentCardReader?, Never>()
private let readerConnectionStatusSubject = PassthroughSubject<CardPresentPaymentReaderConnectionStatus, Never>()

private let onboardingAdaptor: CardPresentPaymentsOnboardingPresenterAdaptor

Expand Down Expand Up @@ -57,8 +59,20 @@ 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: paymentAlertsPresenterAdaptor.readerConnectionStatusPublisher)
.merge(with: readerConnectionStatusSubject)
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
Expand All @@ -79,27 +93,29 @@ final class CardPresentPaymentService: CardPresentPaymentFacade {
})))
return .connected(connectedReader)
case .canceled:
readerConnectionStatusSubject.send(.disconnected)
paymentEventSubject.send(.idle)
return .canceled
}
}

@MainActor
func disconnectReader() async {
readerConnectionStatusSubject.send(.disconnecting)

cancelPayment()

connectionControllerManager.knownReaderProvider.forgetCardReader()

return await withCheckedContinuation { continuation in
var nillableContinuation: CheckedContinuation<Void, Never>? = 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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import enum Yosemite.ServerSidePaymentCaptureError
final class CardPresentPaymentsAlertPresenterAdaptor: CardPresentPaymentAlertsPresenting {
typealias AlertDetails = CardPresentPaymentEventDetails
let paymentEventPublisher: AnyPublisher<CardPresentPaymentEvent, Never>
let readerConnectionStatusPublisher: AnyPublisher<CardPresentPaymentReaderConnectionStatus, Never>

private let paymentEventSubject: PassthroughSubject<CardPresentPaymentEvent, Never> = PassthroughSubject()

private let readerConnectionStatusSubject: PassthroughSubject<CardPresentPaymentReaderConnectionStatus, Never> = PassthroughSubject()

private var latestReaderConnectionHandler: ((String?) -> Void)?

init() {
paymentEventPublisher = paymentEventSubject.eraseToAnyPublisher()
readerConnectionStatusPublisher = readerConnectionStatusSubject.eraseToAnyPublisher()
}

func present(viewModel eventDetails: CardPresentPaymentEventDetails) {
Expand All @@ -35,6 +39,19 @@ 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()
})))
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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()))
Expand All @@ -50,41 +60,59 @@ 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(
viewModel: PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressAlertViewModel(
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(
viewModel: PointOfSaleCardPresentPaymentFoundReaderAlertViewModel(
readerName: name,
connectAction: connect,
continueSearchAction: continueSearch,
endSearchAction: endSearch)))
endSearchAction: {
endSearch()
dependencies.dismissReaderConnectionModal()
})))

case .foundMultipleReaders(let readerIDs, let selectionHandler):
self = .alert(.foundMultipleReaders(
Expand All @@ -100,30 +128,45 @@ 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()
})))

}

case .updateFailed(let tryAgain, let cancelUpdate):
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(
Expand Down
Loading

0 comments on commit b2d380c

Please sign in to comment.