diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt index be5cdd0c130f..3fac768cd984 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewModel.kt @@ -15,6 +15,8 @@ import com.woocommerce.android.ui.payments.cardreader.CardReaderCountryConfigPro import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderOnboardingChecker import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentController import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentEvent +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentStateProvider +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderTrackCanceledFlowAction import com.woocommerce.android.ui.payments.receipt.PaymentReceiptHelper import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.tracking.CardReaderTrackingInfoKeeper @@ -41,6 +43,7 @@ class CardReaderPaymentViewModel @Inject constructor( paymentCollectibilityChecker: CardReaderPaymentCollectibilityChecker, interacRefundableChecker: CardReaderInteracRefundableChecker, tracker: PaymentsFlowTracker, + trackCancelledFlow: CardReaderTrackCanceledFlowAction, currencyFormatter: CurrencyFormatter, errorMapper: CardReaderPaymentErrorMapper, interacRefundErrorMapper: CardReaderInteracRefundErrorMapper, @@ -48,6 +51,7 @@ class CardReaderPaymentViewModel @Inject constructor( dispatchers: CoroutineDispatchers, cardReaderTrackingInfoKeeper: CardReaderTrackingInfoKeeper, cardReaderPaymentReaderTypeStateProvider: CardReaderPaymentReaderTypeStateProvider, + paymentStateProvider: CardReaderPaymentStateProvider, cardReaderPaymentOrderHelper: CardReaderPaymentOrderHelper, paymentReceiptHelper: PaymentReceiptHelper, cardReaderOnboardingChecker: CardReaderOnboardingChecker, @@ -71,6 +75,7 @@ class CardReaderPaymentViewModel @Inject constructor( paymentCollectibilityChecker = paymentCollectibilityChecker, interacRefundableChecker = interacRefundableChecker, tracker = tracker, + trackCancelledFlow = trackCancelledFlow, currencyFormatter = currencyFormatter, errorMapper = errorMapper, interacRefundErrorMapper = interacRefundErrorMapper, @@ -78,6 +83,7 @@ class CardReaderPaymentViewModel @Inject constructor( dispatchers = dispatchers, cardReaderTrackingInfoKeeper = cardReaderTrackingInfoKeeper, cardReaderPaymentReaderTypeStateProvider = cardReaderPaymentReaderTypeStateProvider, + paymentStateProvider = paymentStateProvider, cardReaderPaymentOrderHelper = cardReaderPaymentOrderHelper, paymentReceiptHelper = paymentReceiptHelper, cardReaderOnboardingChecker = cardReaderOnboardingChecker, @@ -88,6 +94,7 @@ class CardReaderPaymentViewModel @Inject constructor( isTTPPaymentInProgress = ::isTTPPaymentInProgress, ) + @Suppress("DEPRECATION") val viewStateData: LiveData = paymentController.viewStateData override val event: LiveData = diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentController.kt index 8b337ce9afe6..3ad1533d934e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentController.kt @@ -60,13 +60,10 @@ import com.woocommerce.android.ui.payments.cardreader.payment.CardReaderPaymentC import com.woocommerce.android.ui.payments.cardreader.payment.CardReaderPaymentErrorMapper import com.woocommerce.android.ui.payments.cardreader.payment.CardReaderPaymentOrderHelper import com.woocommerce.android.ui.payments.cardreader.payment.CardReaderPaymentReaderTypeStateProvider -import com.woocommerce.android.ui.payments.cardreader.payment.InteracRefundFlow import com.woocommerce.android.ui.payments.cardreader.payment.InteracRefundFlowError -import com.woocommerce.android.ui.payments.cardreader.payment.PaymentFlow import com.woocommerce.android.ui.payments.cardreader.payment.PaymentFlowError import com.woocommerce.android.ui.payments.cardreader.payment.ViewState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderCollectPaymentState -import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderFailedPaymentState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.CollectRefundState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderCollectPaymentState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.FailedRefundState @@ -76,6 +73,9 @@ import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.Processi import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ReFetchingOrderState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundLoadingDataState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundSuccessfulState +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderInteracRefundState +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.PaymentFailed.BuiltInReaderFailedPayment import com.woocommerce.android.ui.payments.receipt.PaymentReceiptHelper import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.tracking.CardReaderTrackingInfoKeeper @@ -92,6 +92,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.store.WooCommerceStore @@ -110,6 +112,7 @@ class CardReaderPaymentController( private val paymentCollectibilityChecker: CardReaderPaymentCollectibilityChecker, private val interacRefundableChecker: CardReaderInteracRefundableChecker, private val tracker: PaymentsFlowTracker, + private val trackCancelledFlow: CardReaderTrackCanceledFlowAction, private val currencyFormatter: CurrencyFormatter, private val errorMapper: CardReaderPaymentErrorMapper, private val interacRefundErrorMapper: CardReaderInteracRefundErrorMapper, @@ -117,6 +120,7 @@ class CardReaderPaymentController( private val dispatchers: CoroutineDispatchers, private val cardReaderTrackingInfoKeeper: CardReaderTrackingInfoKeeper, private val cardReaderPaymentReaderTypeStateProvider: CardReaderPaymentReaderTypeStateProvider, + private val paymentStateProvider: CardReaderPaymentStateProvider, private val cardReaderPaymentOrderHelper: CardReaderPaymentOrderHelper, private val paymentReceiptHelper: PaymentReceiptHelper, private val cardReaderOnboardingChecker: CardReaderOnboardingChecker, @@ -127,8 +131,23 @@ class CardReaderPaymentController( private val isTTPPaymentInProgress: KMutableProperty0, ) { private val viewState = MutableLiveData(LoadingDataState(::onCancelPaymentFlow)) + + @Deprecated( + level = DeprecationLevel.WARNING, + message = "Use [CardReaderPaymentController.paymentState] and map to ViewState in the target [ViewModel]" + ) val viewStateData: LiveData = viewState + private val _paymentState: MutableStateFlow = + MutableStateFlow(CardReaderPaymentState.LoadingData(::onCancelPaymentFlow)) + + /** + * Exposes payment collection state. + * + * Returns observable of [CardReaderPaymentOrRefundState]. + */ + val paymentState: StateFlow = _paymentState + private var paymentFlowJob: Job? = null private var refundFlowJob: Job? = null private var paymentDataForRetry: PaymentData? = null @@ -163,12 +182,17 @@ class CardReaderPaymentController( if (isVMKilledWhenTTPActivityInForeground) { tracker.trackPaymentFailed("VM killed when TTP activity in foreground") viewState.postValue( - buildFailedPaymentState( + buildFailedPaymentViewState( PaymentFlowError.BuiltInReader.AppKilledWhileInBackground, "", {} ) ) + _paymentState.value = buildFailedPaymentState( + PaymentFlowError.BuiltInReader.AppKilledWhileInBackground, + "", + {} + ) } else { when (paymentOrRefund) { is CardReaderFlowParam.PaymentOrRefund.Payment -> { @@ -195,6 +219,7 @@ class CardReaderPaymentController( when (message) { is BluetoothCardReaderMessages.CardReaderDisplayMessage -> { handleAdditionalInfo(message.message) + updatePaymentState(message.message) } is BluetoothCardReaderMessages.CardReaderInputMessage -> { @@ -211,6 +236,7 @@ class CardReaderPaymentController( private fun initPaymentFlow(isRetry: Boolean) { paymentFlowJob = scope.launch { viewState.postValue((LoadingDataState(::onCancelPaymentFlow))) + _paymentState.value = CardReaderPaymentState.LoadingData(::onCancelPaymentFlow) if (isRetry) { delay(ARTIFICIAL_RETRY_DELAY) } @@ -238,6 +264,12 @@ class CardReaderPaymentController( onPrimaryActionClicked = { initPaymentFlow(isRetry = true) } ) ) + _paymentState.value = paymentStateProvider.provideFailedPaymentState( + cardReaderType = cardReaderType, + errorType = PaymentFlowError.FetchingOrderFailed, + amountWithCurrencyLabel = null, + onRetry = { initPaymentFlow(isRetry = true) }, + ) } } } @@ -271,6 +303,11 @@ class CardReaderPaymentController( onPrimaryActionClicked = { initRefundFlow(isRetry = true) } ) ) + _paymentState.value = CardReaderInteracRefundState.InteracRefundFailure( + errorType = InteracRefundFlowError.FetchingOrderFailed, + amountWithCurrencyLabel = null, + onRetry = { initRefundFlow(isRetry = true) }, + ) } } } @@ -278,6 +315,7 @@ class CardReaderPaymentController( fun retry(orderId: Long, billingEmail: String, paymentData: PaymentData, amountLabel: String) { paymentFlowJob = scope.launch { viewState.postValue(LoadingDataState(::onCancelPaymentFlow)) + _paymentState.value = CardReaderPaymentState.LoadingData(::onCancelPaymentFlow) delay(ARTIFICIAL_RETRY_DELAY) cardReaderManager.retryCollectPayment(orderId, paymentData).collect { paymentStatus -> onPaymentStatusChanged(orderId, billingEmail, paymentStatus, amountLabel) @@ -324,6 +362,7 @@ class CardReaderPaymentController( } } + @Suppress("LongMethod") private suspend fun onPaymentStatusChanged( orderId: Long, billingEmail: String, @@ -332,22 +371,40 @@ class CardReaderPaymentController( ) { paymentDataForRetry = null when (paymentStatus) { - InitializingPayment -> viewState.postValue(LoadingDataState(::onCancelPaymentFlow)) - CollectingPayment -> viewState.postValue( - cardReaderPaymentReaderTypeStateProvider.provideCollectPaymentState( + InitializingPayment -> { + viewState.postValue(LoadingDataState(::onCancelPaymentFlow)) + _paymentState.value = + CardReaderPaymentState.LoadingData(::onCancelPaymentFlow) + } + CollectingPayment -> { + viewState.postValue( + cardReaderPaymentReaderTypeStateProvider.provideCollectPaymentState( + cardReaderType, + amountLabel, + ::onCancelPaymentFlow + ) + ) + _paymentState.value = paymentStateProvider.provideCollectingPaymentState( cardReaderType, amountLabel, ::onCancelPaymentFlow ) - ) + } - ProcessingPayment -> viewState.postValue( - cardReaderPaymentReaderTypeStateProvider.provideProcessingPaymentState( + ProcessingPayment -> { + viewState.postValue( + cardReaderPaymentReaderTypeStateProvider.provideProcessingPaymentState( + cardReaderType, + amountLabel, + ::onCancelPaymentFlow + ) + ) + _paymentState.value = paymentStateProvider.provideProcessingPaymentState( cardReaderType, amountLabel, ::onCancelPaymentFlow ) - ) + } is ProcessingPaymentCompleted -> { cardReaderTrackingInfoKeeper.setPaymentMethodType(paymentStatus.paymentMethodType.stringRepresentation) @@ -358,12 +415,18 @@ class CardReaderPaymentController( } } - CapturingPayment -> viewState.postValue( - cardReaderPaymentReaderTypeStateProvider.provideCapturingPaymentState( + CapturingPayment -> { + viewState.postValue( + cardReaderPaymentReaderTypeStateProvider.provideCapturingPaymentState( + cardReaderType, + amountLabel, + ) + ) + _paymentState.value = paymentStateProvider.provideCapturingPaymentState( cardReaderType, - amountLabel, + amountLabel ) - ) + } is PaymentCompleted -> { tracker.trackPaymentSucceeded() @@ -435,17 +498,30 @@ class CardReaderPaymentController( amountLabel: String ) { when (refundStatus) { - InitializingInteracRefund -> viewState.postValue(RefundLoadingDataState(::onCancelPaymentFlow)) - CollectingInteracRefund -> viewState.postValue( - CollectRefundState( - amountLabel, - onSecondaryActionClicked = ::onCancelPaymentFlow + InitializingInteracRefund -> { + viewState.postValue(RefundLoadingDataState(::onCancelPaymentFlow)) + _paymentState.value = CardReaderInteracRefundState.LoadingData(::onCancelPaymentFlow) + } + CollectingInteracRefund -> { + viewState.postValue( + CollectRefundState( + amountLabel, + onSecondaryActionClicked = ::onCancelPaymentFlow + ) ) - ) + _paymentState.value = CardReaderInteracRefundState.CollectingInteracRefund( + amountWithCurrencyLabel = amountLabel, + onCancel = ::onCancelPaymentFlow + ) + } - ProcessingInteracRefund -> viewState.postValue(ProcessingRefundState(amountLabel)) + ProcessingInteracRefund -> { + viewState.postValue(ProcessingRefundState(amountLabel)) + _paymentState.value = CardReaderInteracRefundState.ProcessingInteracRefund(amountLabel) + } is InteracRefundSuccess -> { viewState.postValue(RefundSuccessfulState(amountLabel)) + _paymentState.value = CardReaderInteracRefundState.InteracRefundSuccessful(amountLabel) triggerEvent(CardReaderPaymentEvent.InteracRefundSuccessful) } @@ -493,7 +569,7 @@ class CardReaderPaymentController( R.string.card_reader_refetching_order_failed ) ) - if (viewState.value == ReFetchingOrderState) { + if (_paymentState.value == CardReaderPaymentState.ReFetchingOrder) { triggerEvent(CardReaderPaymentEvent.Exit) } } @@ -504,17 +580,18 @@ class CardReaderPaymentController( } private fun emitFailedInteracRefundState( - amountLabel: String?, + amountLabel: String, error: InteracRefundFailure ) { WooLog.e(WooLog.T.CARD_READER, "Refund failed: ${error.errorMessage}") cardReaderOnboardingChecker.invalidateCache() val onRetryClicked = { retryInteracRefund() } val errorType = interacRefundErrorMapper.mapRefundErrorToUiError(error.type) - viewState.postValue(buildInteracRefundFailedState(errorType, amountLabel, onRetryClicked)) + viewState.postValue(buildInteracRefundFailedViewState(errorType, amountLabel, onRetryClicked)) + _paymentState.value = buildInteracRefundFailedState(errorType, amountLabel, onRetryClicked) } - private fun buildInteracRefundFailedState( + private fun buildInteracRefundFailedViewState( errorType: InteracRefundFlowError, amountLabel: String?, onRetryClicked: () -> Unit @@ -547,6 +624,38 @@ class CardReaderPaymentController( ) } + private fun buildInteracRefundFailedState( + errorType: InteracRefundFlowError, + amountLabel: String, + onRetryClicked: () -> Unit + ) = when (errorType) { + is InteracRefundFlowError.ContactSupportError -> + CardReaderInteracRefundState.InteracRefundFailure( + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + cta = CardReaderPaymentOrRefundState.CallToAction( + label = R.string.support_contact, + onCallToActionTapped = { onContactSupportClicked() } + ), + onCancel = ::onBackPressed + ) + + is InteracRefundFlowError.NonRetryableError -> + CardReaderInteracRefundState.InteracRefundFailure( + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + onCancel = ::onBackPressed + ) + + else -> + CardReaderInteracRefundState.InteracRefundFailure( + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + onRetry = onRetryClicked, + onCancel = ::onBackPressed + ) + } + private suspend fun emitFailedPaymentState( orderId: Long, billingEmail: String, @@ -571,13 +680,64 @@ class CardReaderPaymentController( config, cardReaderType == CardReaderType.BUILT_IN ) - viewState.postValue(buildFailedPaymentState(errorType, amountLabel, onRetryClicked)) + viewState.postValue(buildFailedPaymentViewState(errorType, amountLabel, onRetryClicked)) + _paymentState.value = buildFailedPaymentState(errorType, amountLabel, onRetryClicked) } private fun buildFailedPaymentState( errorType: PaymentFlowError, amountLabel: String, onRetryClicked: () -> Unit + ): CardReaderPaymentState.PaymentFailed = when (errorType) { + is PaymentFlowError.ContactSupportError -> paymentStateProvider.provideFailedPaymentState( + cardReaderType = cardReaderType, + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + cta = CardReaderPaymentOrRefundState.CallToAction( + label = R.string.support_contact, + onCallToActionTapped = { onContactSupportClicked() } + ), + onCancel = ::onBackPressed + ) + is PaymentFlowError.BuiltInReader.NfcDisabled -> paymentStateProvider.provideFailedPaymentState( + cardReaderType = cardReaderType, + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + onCancel = ::onBackPressed, + cta = CardReaderPaymentOrRefundState.CallToAction( + label = R.string.card_reader_payment_failed_nfc_disabled_enable_nfc, + onCallToActionTapped = { onEnableNfcClicked() } + ) + ) + is PaymentFlowError.NonRetryableError -> paymentStateProvider.provideFailedPaymentState( + cardReaderType = cardReaderType, + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + onCancel = ::onBackPressed, + ) + is PaymentFlowError.PurchaseHardwareReaderError -> paymentStateProvider.provideFailedPaymentState( + cardReaderType = cardReaderType, + cta = CardReaderPaymentOrRefundState.CallToAction( + label = R.string.card_reader_payment_payment_failed_purchase_hardware_reader, + onCallToActionTapped = { onPurchaseCardReaderClicked() } + ), + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + onCancel = ::onBackPressed, + ) + else -> paymentStateProvider.provideFailedPaymentState( + cardReaderType = cardReaderType, + errorType = errorType, + amountWithCurrencyLabel = amountLabel, + onRetry = onRetryClicked, + onCancel = ::onBackPressed, + ) + } + + private fun buildFailedPaymentViewState( + errorType: PaymentFlowError, + amountLabel: String, + onRetryClicked: () -> Unit ) = when (errorType) { is PaymentFlowError.ContactSupportError -> @@ -656,6 +816,13 @@ class CardReaderPaymentController( onSaveUserClicked ) ) + _paymentState.value = paymentStateProvider.providePaymentSuccessState( + cardReaderType = cardReaderType, + amountLabel = amountLabel, + onPrintReceiptClicked = onPrintReceiptClicked, + onSendReceiptClicked = onSendReceiptClicked, + onSaveUserClicked = onSaveUserClicked + ) } else { val receiptSentHint = UiStringRes( R.string.card_reader_payment_reader_receipt_sent, @@ -671,6 +838,13 @@ class CardReaderPaymentController( onSaveUserClicked ) ) + _paymentState.value = paymentStateProvider.providePaymentSuccessfulReceiptSentAutomaticallyState( + cardReaderType = cardReaderType, + amountLabel = amountLabel, + recipientEmail = order.billingAddress.email, + onPrintReceiptClicked = onPrintReceiptClicked, + onSaveUserClicked = onSaveUserClicked + ) } } } @@ -693,9 +867,26 @@ class CardReaderPaymentController( hintLabel = type.toHintLabel(false) ) + else -> Unit + } + } + + private fun updatePaymentState(type: AdditionalInfoType) { + when (val state = _paymentState.value) { + is CardReaderInteracRefundState.CollectingInteracRefund -> { + _paymentState.value = state.copy( + cardReaderHint = type.toHintLabel(true) + ) + } + + is CardReaderPaymentState.CollectingPayment.BuiltInReaderCollectPaymentState -> + _paymentState.value = state.copy( + cardReaderHint = type.toHintLabel(false) + ) + else -> WooLog.e( WooLog.T.CARD_READER, - "Got SDK message when cardReaderPaymentViewModel is in ${viewState.value}" + "Got SDK message when cardReaderPaymentController is in ${_paymentState.value}" ) } } @@ -728,13 +919,14 @@ class CardReaderPaymentController( private fun onPrintReceiptClicked(amountWithCurrencyLabel: String) { scope.launch { viewState.value = PrintingReceiptState(amountWithCurrencyLabel) + _paymentState.value = CardReaderPaymentState.PrintingReceipt(amountWithCurrencyLabel) tracker.trackPrintReceiptTapped() startPrintingFlow() } } fun onViewCreated() { - if (viewState.value is PrintingReceiptState) { + if (_paymentState.value is CardReaderPaymentState.PrintingReceipt) { startPrintingFlow() } } @@ -761,8 +953,10 @@ class CardReaderPaymentController( private fun onSendReceiptClicked() { scope.launch { tracker.trackEmailReceiptTapped() - val stateBeforeLoading = viewState.value!! + val viewStateBeforeLoading = viewState.value!! + val paymentStateBeforeLoading = _paymentState.value viewState.postValue(ViewState.SharingReceiptState) + _paymentState.value = CardReaderPaymentState.SharingReceipt val receiptResult = paymentReceiptHelper.getReceiptUrl(paymentOrRefund.orderId) if (receiptResult.isSuccess) { @@ -806,7 +1000,8 @@ class CardReaderPaymentController( triggerEvent(CardReaderPaymentEvent.ShowErrorMessage(R.string.receipt_fetching_error)) } - viewState.postValue(stateBeforeLoading) + viewState.postValue(viewStateBeforeLoading) + _paymentState.value = paymentStateBeforeLoading } } @@ -857,16 +1052,15 @@ class CardReaderPaymentController( private fun onCancelPaymentFlow() { if (refetchOrderJob?.isActive == true) { - if (viewState.value != ReFetchingOrderState) { + if (_paymentState.value != CardReaderPaymentState.ReFetchingOrder) { viewState.value = ReFetchingOrderState + _paymentState.value = CardReaderPaymentState.ReFetchingOrder } else { // show "data might be outdated" and exit the flow when the user presses back on FetchingOrder screen exitWithSnackbar(R.string.card_reader_refetching_order_failed) } } else { - viewState.value?.let { state -> - trackCancelledFlow(state) - } + trackCancelledFlow(_paymentState.value) triggerEvent(CardReaderPaymentEvent.Exit) } } @@ -875,27 +1069,16 @@ class CardReaderPaymentController( val readerStatus = cardReaderManager.readerStatus.value if (readerStatus is CardReaderStatus.Connected) { if (ReaderType.isBuiltInReaderType(readerStatus.cardReader.type) && - (viewState.value is BuiltInReaderFailedPaymentState || viewState.value is FailedRefundState) + ( + _paymentState.value is BuiltInReaderFailedPayment || + _paymentState.value is CardReaderInteracRefundState.InteracRefundFailure + ) ) { scope.launch { cardReaderManager.disconnectReader() } } } } - private fun trackCancelledFlow(state: ViewState) { - when (state) { - is PaymentFlow -> { - tracker.trackPaymentCancelled(state.nameForTracking) - } - - is InteracRefundFlow -> { - tracker.trackInteracRefundCancelled(state.nameForTracking) - } - - else -> WooLog.e(WooLog.T.CARD_READER, "Invalid state received") - } - } - private fun exitWithSnackbar(@StringRes message: Int) { triggerEvent(CardReaderPaymentEvent.ShowErrorMessage(message)) triggerEvent(CardReaderPaymentEvent.Exit) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentOrRefundState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentOrRefundState.kt new file mode 100644 index 000000000000..47ddec3371b7 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentOrRefundState.kt @@ -0,0 +1,153 @@ +package com.woocommerce.android.ui.payments.cardreader.payment.controller + +import androidx.annotation.StringRes +import com.woocommerce.android.ui.payments.cardreader.payment.InteracRefundFlowError +import com.woocommerce.android.ui.payments.cardreader.payment.PaymentFlowError + +sealed class CardReaderPaymentOrRefundState { + sealed class CardReaderPaymentState : CardReaderPaymentOrRefundState() { + data class LoadingData(val onCancel: () -> Unit) : CardReaderPaymentState() + + data object ReFetchingOrder : CardReaderPaymentState() + + sealed class CollectingPayment( + open val amountWithCurrencyLabel: String, + @StringRes open val cardReaderHint: Int? = null, + ) : CardReaderPaymentState() { + data class BuiltInReaderCollectPaymentState( + override val amountWithCurrencyLabel: String, + override val cardReaderHint: Int? = null, + ) : CollectingPayment(amountWithCurrencyLabel, cardReaderHint) + + data class ExternalReaderCollectPaymentState( + override val amountWithCurrencyLabel: String, + override val cardReaderHint: Int? = null, + val onCancel: (() -> Unit) + ) : CollectingPayment(amountWithCurrencyLabel, cardReaderHint) + } + + sealed class ProcessingPayment( + open val amountWithCurrencyLabel: String, + ) : CardReaderPaymentState() { + data class BuiltInReaderProcessingPayment(override val amountWithCurrencyLabel: String) : + ProcessingPayment(amountWithCurrencyLabel) + + data class ExternalReaderProcessingPayment( + override val amountWithCurrencyLabel: String, + val onCancel: () -> Unit + ) : ProcessingPayment(amountWithCurrencyLabel) + } + + data class PrintingReceipt(val amountWithCurrencyLabel: String) : CardReaderPaymentState() + + sealed class PaymentCapturing(open val amountWithCurrencyLabel: String) : + CardReaderPaymentState() { + data class BuiltInReaderPaymentCapturing(override val amountWithCurrencyLabel: String) : + PaymentCapturing(amountWithCurrencyLabel) + + data class ExternalReaderPaymentCapturing(override val amountWithCurrencyLabel: String) : + PaymentCapturing(amountWithCurrencyLabel) + } + + sealed class PaymentSuccessful( + open val amountWithCurrencyLabel: String, + ) : CardReaderPaymentState() { + data class BuiltInReaderPaymentSuccessful( + override val amountWithCurrencyLabel: String, + val onPrintReceiptClicked: () -> Unit, + val onSendReceiptClicked: () -> Unit, + val onSaveUserClicked: () -> Unit + ) : PaymentSuccessful(amountWithCurrencyLabel) + + data class ExternalReaderPaymentSuccessful( + override val amountWithCurrencyLabel: String, + val onPrintReceiptClicked: () -> Unit, + val onSendReceiptClicked: () -> Unit, + val onSaveUserClicked: () -> Unit + ) : PaymentSuccessful(amountWithCurrencyLabel) + + data class BuiltInReaderPaymentSuccessfulReceiptSentAutomatically( + override val amountWithCurrencyLabel: String, + val recipientEmail: String, + val onPrintReceiptClicked: () -> Unit, + val onSaveUserClicked: () -> Unit + ) : PaymentSuccessful(amountWithCurrencyLabel) + + data class ExternalReaderPaymentSuccessfulReceiptSentAutomatically( + override val amountWithCurrencyLabel: String, + val recipientEmail: String, + val onPrintReceiptClicked: () -> Unit, + val onSaveUserClicked: () -> Unit + ) : PaymentSuccessful(amountWithCurrencyLabel) + } + + sealed class PaymentFailed( + open val errorType: PaymentFlowError, + open val amountWithCurrencyLabel: String?, + open val onCancel: (() -> Unit)? = null, + open val onRetry: (() -> Unit)?, + open val cta: CallToAction? = null, + ) : CardReaderPaymentState() { + data class BuiltInReaderFailedPayment( + override val errorType: PaymentFlowError, + override val amountWithCurrencyLabel: String?, + override val onCancel: (() -> Unit)? = null, + override val onRetry: (() -> Unit)? = null, + override val cta: CallToAction? = null, + ) : PaymentFailed( + errorType, + amountWithCurrencyLabel, + onCancel, + onRetry, + cta, + ) + + data class ExternalReaderFailedPayment( + override val errorType: PaymentFlowError, + override val amountWithCurrencyLabel: String?, + override val onCancel: (() -> Unit)? = null, + override val onRetry: (() -> Unit)? = null, + override val cta: CallToAction? = null, + ) : PaymentFailed( + errorType, + amountWithCurrencyLabel, + onCancel, + onRetry, + cta, + ) + } + + data object SharingReceipt : CardReaderPaymentState() + } + + sealed class CardReaderInteracRefundState : CardReaderPaymentOrRefundState() { + data class LoadingData(val onCancel: () -> Unit) : CardReaderInteracRefundState() + + data class CollectingInteracRefund( + val amountWithCurrencyLabel: String, + val onCancel: () -> Unit, + @StringRes val cardReaderHint: Int? = null, + ) : CardReaderInteracRefundState() + + data class ProcessingInteracRefund( + val amountWithCurrencyLabel: String, + ) : CardReaderInteracRefundState() + + data class InteracRefundFailure( + val amountWithCurrencyLabel: String?, + val errorType: InteracRefundFlowError, + val onCancel: (() -> Unit)? = null, + val onRetry: (() -> Unit)? = null, + val cta: CallToAction? = null, + ) : CardReaderInteracRefundState() + + data class InteracRefundSuccessful( + val amountWithCurrencyLabel: String, + ) : CardReaderInteracRefundState() + } + + data class CallToAction( + @StringRes val label: Int, + val onCallToActionTapped: () -> Unit, + ) +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentStateProvider.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentStateProvider.kt new file mode 100644 index 000000000000..589c1a47c5d6 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentStateProvider.kt @@ -0,0 +1,118 @@ +package com.woocommerce.android.ui.payments.cardreader.payment.controller + +import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderType +import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderType.BUILT_IN +import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderType.EXTERNAL +import com.woocommerce.android.ui.payments.cardreader.payment.PaymentFlowError +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.CollectingPayment +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.PaymentCapturing +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.PaymentFailed.BuiltInReaderFailedPayment +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.PaymentSuccessful +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.ProcessingPayment +import javax.inject.Inject + +class CardReaderPaymentStateProvider @Inject constructor() { + fun provideFailedPaymentState( + cardReaderType: CardReaderType, + errorType: PaymentFlowError, + amountWithCurrencyLabel: String?, + onCancel: (() -> Unit)? = null, + onRetry: (() -> Unit)? = null, + cta: CardReaderPaymentOrRefundState.CallToAction? = null, + ) = when (cardReaderType) { + BUILT_IN -> BuiltInReaderFailedPayment( + errorType = errorType, + amountWithCurrencyLabel = amountWithCurrencyLabel, + onCancel = onCancel, + onRetry = onRetry, + cta = cta + ) + EXTERNAL -> ExternalReaderFailedPayment( + errorType = errorType, + amountWithCurrencyLabel = amountWithCurrencyLabel, + onCancel = onCancel, + onRetry = onRetry, + cta = cta + ) + } + + fun provideCollectingPaymentState( + cardReaderType: CardReaderType, + amountWithCurrencyLabel: String, + onCancel: () -> Unit + ) = when (cardReaderType) { + BUILT_IN -> CollectingPayment.BuiltInReaderCollectPaymentState( + amountWithCurrencyLabel = amountWithCurrencyLabel + ) + + EXTERNAL -> CollectingPayment.ExternalReaderCollectPaymentState( + amountWithCurrencyLabel = amountWithCurrencyLabel, + onCancel = onCancel, + ) + } + + fun provideProcessingPaymentState( + cardReaderType: CardReaderType, + amountLabel: String, + onCancel: () -> Unit + ): ProcessingPayment = when (cardReaderType) { + BUILT_IN -> ProcessingPayment.BuiltInReaderProcessingPayment( + amountWithCurrencyLabel = amountLabel, + ) + EXTERNAL -> ProcessingPayment.ExternalReaderProcessingPayment( + amountWithCurrencyLabel = amountLabel, + onCancel = onCancel, + ) + } + + fun provideCapturingPaymentState( + cardReaderType: CardReaderType, + amountLabel: String + ): PaymentCapturing = when (cardReaderType) { + BUILT_IN -> PaymentCapturing.BuiltInReaderPaymentCapturing(amountLabel) + EXTERNAL -> PaymentCapturing.ExternalReaderPaymentCapturing(amountLabel) + } + + fun providePaymentSuccessfulReceiptSentAutomaticallyState( + cardReaderType: CardReaderType, + amountLabel: String, + recipientEmail: String, + onPrintReceiptClicked: () -> Unit, + onSaveUserClicked: () -> Unit + ): CardReaderPaymentOrRefundState = when (cardReaderType) { + BUILT_IN -> PaymentSuccessful.BuiltInReaderPaymentSuccessfulReceiptSentAutomatically( + amountWithCurrencyLabel = amountLabel, + recipientEmail = recipientEmail, + onPrintReceiptClicked = onPrintReceiptClicked, + onSaveUserClicked = onSaveUserClicked + ) + EXTERNAL -> PaymentSuccessful.ExternalReaderPaymentSuccessfulReceiptSentAutomatically( + amountWithCurrencyLabel = amountLabel, + recipientEmail = recipientEmail, + onPrintReceiptClicked = onPrintReceiptClicked, + onSaveUserClicked = onSaveUserClicked + ) + } + + fun providePaymentSuccessState( + cardReaderType: CardReaderType, + amountLabel: String, + onPrintReceiptClicked: () -> Unit, + onSendReceiptClicked: () -> Unit, + onSaveUserClicked: () -> Unit + ): PaymentSuccessful = when (cardReaderType) { + BUILT_IN -> PaymentSuccessful.BuiltInReaderPaymentSuccessful( + amountWithCurrencyLabel = amountLabel, + onSendReceiptClicked = onSendReceiptClicked, + onPrintReceiptClicked = onPrintReceiptClicked, + onSaveUserClicked = onSaveUserClicked + ) + EXTERNAL -> PaymentSuccessful.ExternalReaderPaymentSuccessful( + amountWithCurrencyLabel = amountLabel, + onSendReceiptClicked = onSendReceiptClicked, + onPrintReceiptClicked = onPrintReceiptClicked, + onSaveUserClicked = onSaveUserClicked + ) + } +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderTrackCanceledFlowAction.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderTrackCanceledFlowAction.kt new file mode 100644 index 000000000000..5f76bb1620da --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderTrackCanceledFlowAction.kt @@ -0,0 +1,41 @@ +package com.woocommerce.android.ui.payments.cardreader.payment.controller + +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderInteracRefundState +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState +import com.woocommerce.android.ui.payments.tracking.PaymentsFlowTracker +import com.woocommerce.android.util.WooLog +import javax.inject.Inject + +class CardReaderTrackCanceledFlowAction @Inject constructor( + private val tracker: PaymentsFlowTracker, +) { + operator fun invoke(state: CardReaderPaymentOrRefundState) = when (state) { + is CardReaderPaymentState -> { + val nameForTracking = when (state) { + is CardReaderPaymentState.CollectingPayment -> "Collecting" + is CardReaderPaymentState.PaymentCapturing -> "Capturing" + is CardReaderPaymentState.ProcessingPayment -> "Processing" + is CardReaderPaymentState.LoadingData -> "Loading" + else -> null + } + if (nameForTracking == null) { + WooLog.e(WooLog.T.CARD_READER, "Invalid state received") + } else { + tracker.trackPaymentCancelled(nameForTracking) + } + } + is CardReaderInteracRefundState -> { + val nameForTracking = when (state) { + is CardReaderInteracRefundState.CollectingInteracRefund -> "Collecting" + is CardReaderInteracRefundState.LoadingData -> "Loading" + is CardReaderInteracRefundState.ProcessingInteracRefund -> "Processing" + else -> null + } + if (nameForTracking == null) { + WooLog.e(WooLog.T.CARD_READER, "Invalid state received") + } else { + tracker.trackInteracRefundCancelled(nameForTracking) + } + } + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt index 646789b424f9..ebe35a09d576 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt @@ -88,6 +88,8 @@ import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.Processi import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ReFetchingOrderState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundLoadingDataState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.RefundSuccessfulState +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentStateProvider +import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderTrackCanceledFlowAction import com.woocommerce.android.ui.payments.receipt.PaymentReceiptHelper import com.woocommerce.android.ui.payments.receipt.PaymentReceiptShare import com.woocommerce.android.ui.payments.tracking.CardReaderTrackingInfoKeeper @@ -148,6 +150,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { private val selectedSite: SelectedSite = mock() private val paymentCollectibilityChecker: CardReaderPaymentCollectibilityChecker = mock() private val tracker: PaymentsFlowTracker = mock() + private val trackCanceledFlow = CardReaderTrackCanceledFlowAction(tracker) private val appPrefs: AppPrefs = mock() private val currencyFormatter: CurrencyFormatter = mock() private val wooStore: WooCommerceStore = mock() @@ -179,6 +182,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { private val interacRefundErrorMapper: CardReaderInteracRefundErrorMapper = mock() private val interacRefundableChecker: CardReaderInteracRefundableChecker = mock() private val cardReaderPaymentReaderTypeStateProvider = CardReaderPaymentReaderTypeStateProvider() + private val paymentStateProvider = CardReaderPaymentStateProvider() private val cardReaderPaymentOrderHelper: CardReaderPaymentOrderHelper = mock() private val paymentReceiptHelper: PaymentReceiptHelper = mock() private val cardReaderOnboardingChecker: CardReaderOnboardingChecker = mock() @@ -200,6 +204,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { paymentCollectibilityChecker = paymentCollectibilityChecker, interacRefundableChecker = interacRefundableChecker, tracker = tracker, + trackCancelledFlow = trackCanceledFlow, appPrefs = appPrefs, currencyFormatter = currencyFormatter, errorMapper = errorMapper, @@ -213,6 +218,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { cardReaderOnboardingChecker = cardReaderOnboardingChecker, cardReaderConfigProvider = cardReaderConfigProvider, paymentReceiptShare = paymentReceiptShare, + paymentStateProvider = paymentStateProvider, ) whenever(orderRepository.getOrderById(any())).thenReturn(mockedOrder) @@ -4488,6 +4494,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { paymentCollectibilityChecker = paymentCollectibilityChecker, interacRefundableChecker = interacRefundableChecker, tracker = tracker, + trackCancelledFlow = trackCanceledFlow, appPrefs = appPrefs, currencyFormatter = currencyFormatter, errorMapper = errorMapper, @@ -4501,6 +4508,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { cardReaderOnboardingChecker = cardReaderOnboardingChecker, cardReaderConfigProvider = cardReaderConfigProvider, paymentReceiptShare = paymentReceiptShare, + paymentStateProvider = CardReaderPaymentStateProvider(), ) viewModel.event.observeForever {} } @@ -4524,6 +4532,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { paymentCollectibilityChecker = paymentCollectibilityChecker, interacRefundableChecker = interacRefundableChecker, tracker = tracker, + trackCancelledFlow = trackCanceledFlow, appPrefs = appPrefs, currencyFormatter = currencyFormatter, errorMapper = errorMapper, @@ -4537,6 +4546,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { cardReaderOnboardingChecker = cardReaderOnboardingChecker, cardReaderConfigProvider = cardReaderConfigProvider, paymentReceiptShare = paymentReceiptShare, + paymentStateProvider = paymentStateProvider, ) viewModel.event.observeForever {} }