diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt index e7197367ff4..39bd1d75405 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt @@ -26,7 +26,7 @@ sealed class ChildToParentEvent { data object PaymentProcessing : ChildToParentEvent() data object PaymentFailed : ChildToParentEvent() data object RetryFailedPaymentClicked : ChildToParentEvent() - data object ExitOrderAfterFailedTransactionClicked : ChildToParentEvent() + data object GoBackToCheckoutAfterFailedPayment : ChildToParentEvent() data object OrderSuccessfullyPaid : ChildToParentEvent() data object ExitPosClicked : ChildToParentEvent() data object ProductsDialogInfoIconClicked : ChildToParentEvent() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt index 98af275c508..ff7093dbf58 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt @@ -25,7 +25,6 @@ sealed class ParentToChildrenEvent { ) : ParentToChildrenEvent() data class CheckoutClicked(val productIds: List) : ParentToChildrenEvent() data object OrderSuccessfullyPaid : ParentToChildrenEvent() - data object OrderCardPaymentAborted : ParentToChildrenEvent() } interface WooPosParentToChildrenEventReceiver { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt index d178a545bf0..8ef31e407d0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt @@ -106,18 +106,13 @@ class WooPosHomeViewModel @Inject constructor( ) } - is ChildToParentEvent.ExitOrderAfterFailedTransactionClicked -> { - _state.value = _state.value.copy( - screenPositionState = WooPosHomeState.ScreenPositionState.Cart.Visible - ) - sendEventToChildren(ParentToChildrenEvent.OrderCardPaymentAborted) - } is ChildToParentEvent.NewTransactionClicked -> { _state.value = _state.value.copy( screenPositionState = WooPosHomeState.ScreenPositionState.Cart.Visible ) sendEventToChildren(ParentToChildrenEvent.OrderSuccessfullyPaid) } + is ChildToParentEvent.PaymentProcessing, is ChildToParentEvent.OrderSuccessfullyPaid, is ChildToParentEvent.PaymentFailed -> { @@ -125,11 +120,14 @@ class WooPosHomeViewModel @Inject constructor( screenPositionState = WooPosHomeState.ScreenPositionState.Checkout.FullScreenTotals ) } + + is ChildToParentEvent.GoBackToCheckoutAfterFailedPayment, is ChildToParentEvent.RetryFailedPaymentClicked -> { _state.value = _state.value.copy( screenPositionState = WooPosHomeState.ScreenPositionState.Checkout.CartWithTotals ) } + ChildToParentEvent.ExitPosClicked -> { _state.value = _state.value.copy( exitConfirmationDialog = WooPosHomeState.ExitConfirmationDialog(isVisible = true) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt index 6beac3b6b17..d62010e71af 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt @@ -120,8 +120,7 @@ class WooPosCartViewModel @Inject constructor( is ParentToChildrenEvent.ItemClickedInProductSelector -> handleItemClickedInProductSelector(event) - is ParentToChildrenEvent.OrderSuccessfullyPaid, - is ParentToChildrenEvent.OrderCardPaymentAborted -> clearCart() + is ParentToChildrenEvent.OrderSuccessfullyPaid -> clearCart() is ParentToChildrenEvent.CheckoutClicked -> Unit } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsUIEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsUIEvent.kt index 5540b3b69ac..7d4d7df6ad1 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsUIEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsUIEvent.kt @@ -3,6 +3,6 @@ package com.woocommerce.android.ui.woopos.home.totals sealed class WooPosTotalsUIEvent { data object OnNewTransactionClicked : WooPosTotalsUIEvent() data object RetryFailedTransactionClicked : WooPosTotalsUIEvent() - data object ExitOrderAfterFailedTransactionClicked : WooPosTotalsUIEvent() + data object GoBackToCheckoutAfterFailedPayment : WooPosTotalsUIEvent() data object RetryOrderCreationClicked : WooPosTotalsUIEvent() } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt index 14767e9141a..2a5fe6fe4bb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt @@ -151,24 +151,43 @@ class WooPosTotalsViewModel @Inject constructor( is WooPosTotalsUIEvent.RetryOrderCreationClicked -> { createOrderDraft(dataState.value.productIds) } - WooPosTotalsUIEvent.ExitOrderAfterFailedTransactionClicked -> viewModelScope.launch { - childrenToParentEventSender.sendToParent(ChildToParentEvent.ExitOrderAfterFailedTransactionClicked) + WooPosTotalsUIEvent.GoBackToCheckoutAfterFailedPayment -> viewModelScope.launch { + childrenToParentEventSender.sendToParent(ChildToParentEvent.GoBackToCheckoutAfterFailedPayment) + retryPaymentCollectionFromScratch() } WooPosTotalsUIEvent.RetryFailedTransactionClicked -> viewModelScope.launch { - cancelPaymentAction() - childrenToParentEventSender.sendToParent(ChildToParentEvent.RetryFailedPaymentClicked) - val order = totalsRepository.getOrderById(dataState.value.orderId) - if (order == null) { - uiState.value = InitialState - childrenToParentEventSender.sendToParent(ChildToParentEvent.BackFromCheckoutToCartClicked) - } else { - uiState.value = buildWooPosTotalsViewState(order) - collectPayment() + val paymentState = cardReaderPaymentController?.paymentState?.value + check(paymentState != null) { + "Retry failed transaction clicked but payment controller is null" + } + check(paymentState is CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment) { + "Retry failed transaction clicked but payment state is not PaymentFailed" + } + when { + paymentState.onRetry != null -> { + paymentState.onRetry!!() + } + else -> { + childrenToParentEventSender.sendToParent(ChildToParentEvent.RetryFailedPaymentClicked) + retryPaymentCollectionFromScratch() + } } } } } + private suspend fun retryPaymentCollectionFromScratch() { + cancelPaymentAction() + val order = totalsRepository.getOrderById(dataState.value.orderId) + if (order == null) { + uiState.value = InitialState + childrenToParentEventSender.sendToParent(ChildToParentEvent.BackFromCheckoutToCartClicked) + } else { + uiState.value = buildWooPosTotalsViewState(order) + collectPayment() + } + } + private fun collectPayment() { if (!networkStatus.isConnected()) { viewModelScope.launch { @@ -203,7 +222,6 @@ class WooPosTotalsViewModel @Inject constructor( } is ParentToChildrenEvent.ItemClickedInProductSelector, - ParentToChildrenEvent.OrderCardPaymentAborted, ParentToChildrenEvent.OrderSuccessfullyPaid -> Unit } } @@ -222,14 +240,15 @@ class WooPosTotalsViewModel @Inject constructor( when (paymentState) { is CardReaderPaymentState.CollectingPayment, - is CardReaderPaymentState.LoadingData -> { - } + is CardReaderPaymentState.LoadingData -> {} + is CardReaderPaymentState.ProcessingPayment, is CardReaderPaymentState.PaymentCapturing, CardReaderPaymentState.ReFetchingOrder -> { uiState.value = buildPaymentProcessingState() childrenToParentEventSender.sendToParent(ChildToParentEvent.PaymentProcessing) } + is CardReaderPaymentState.PaymentSuccessful -> { uiState.value = PaymentSuccess( @@ -237,10 +256,12 @@ class WooPosTotalsViewModel @Inject constructor( ) childrenToParentEventSender.sendToParent(ChildToParentEvent.OrderSuccessfullyPaid) } + is CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment -> { uiState.value = buildPaymentFailedState(paymentState) childrenToParentEventSender.sendToParent(ChildToParentEvent.PaymentFailed) } + is CardReaderPaymentOrRefundState.CardReaderInteracRefundState, is CardReaderPaymentState.PaymentFailed.BuiltInReaderFailedPayment, is CardReaderPaymentState.PrintingReceipt, @@ -254,12 +275,22 @@ class WooPosTotalsViewModel @Inject constructor( private fun buildPaymentFailedState( state: CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment - ): PaymentFailed = PaymentFailed( - title = resourceProvider.getString( - R.string.woopos_success_totals_payment_failed_title - ), - subtitle = uiStringParser.asString(state.errorType.message) - ) + ): PaymentFailed { + val isRetryAvailable = state.onRetry != null + val retryButtonLabel = if (isRetryAvailable) { + resourceProvider.getString(R.string.woo_pos_payment_failed_try_again) + } else { + resourceProvider.getString(R.string.woo_pos_payment_failed_try_another_payment_method) + } + return PaymentFailed( + title = resourceProvider.getString( + R.string.woopos_success_totals_payment_failed_title + ), + subtitle = uiStringParser.asString(state.errorType.message), + retryPaymentButtonLabel = retryButtonLabel, + isReturnToCheckoutButtonVisible = isRetryAvailable + ) + } private fun buildPaymentProcessingState(): PaymentProcessing = PaymentProcessing( title = resourceProvider.getString( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewState.kt index 9ee0747f8fa..17feee449ed 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewState.kt @@ -31,6 +31,8 @@ sealed class WooPosTotalsViewState : Parcelable { data class PaymentFailed( val title: String, val subtitle: String, + val retryPaymentButtonLabel: String, + val isReturnToCheckoutButtonVisible: Boolean = false, ) : WooPosTotalsViewState() data class PaymentSuccess(var orderTotalText: String) : WooPosTotalsViewState() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/failed/WooPosTotalsPaymentFailedScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/failed/WooPosTotalsPaymentFailedScreen.kt index 80f32ae1de1..49cc6dfea5d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/failed/WooPosTotalsPaymentFailedScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/payment/failed/WooPosTotalsPaymentFailedScreen.kt @@ -60,25 +60,27 @@ fun WooPosPaymentFailedScreen( ) Spacer(modifier = Modifier.height(40.dp.toAdaptivePadding())) WooPosButton( - text = stringResource(R.string.woo_pos_payment_failed_try_another_payment_method), + text = state.retryPaymentButtonLabel, modifier = Modifier .height(80.dp) .width(604.dp) ) { onUIEvent(WooPosTotalsUIEvent.RetryFailedTransactionClicked) } - Spacer(modifier = Modifier.height(24.dp.toAdaptivePadding())) - WooPosOutlinedButton( - modifier = Modifier - .height(80.dp) - .width(604.dp), - content = { - Text( - color = MaterialTheme.colors.primary, - style = MaterialTheme.typography.h5, - fontWeight = FontWeight.Bold, - text = stringResource(R.string.woo_pos_payment_failed_exit_order) - ) - } - ) { onUIEvent(WooPosTotalsUIEvent.ExitOrderAfterFailedTransactionClicked) } + if (state.isReturnToCheckoutButtonVisible) { + Spacer(modifier = Modifier.height(24.dp.toAdaptivePadding())) + WooPosOutlinedButton( + modifier = Modifier + .height(80.dp) + .width(604.dp), + content = { + Text( + color = MaterialTheme.colors.primary, + style = MaterialTheme.typography.h5, + fontWeight = FontWeight.Bold, + text = stringResource(R.string.woo_pos_payment_failed_go_back_to_checkout) + ) + } + ) { onUIEvent(WooPosTotalsUIEvent.GoBackToCheckoutAfterFailedPayment) } + } Spacer(modifier = Modifier.height(80.dp.toAdaptivePadding())) } } @@ -91,6 +93,8 @@ fun WooPosPaymentFailedScreenPreview() { state = WooPosTotalsViewState.PaymentFailed( title = "Payment failed", subtitle = "Unfortunately, this payment has been declined.", + retryPaymentButtonLabel = "Try again", + isReturnToCheckoutButtonVisible = true, ), onUIEvent = {} ) diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index b48e3a59659..0a84a88541d 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -4304,7 +4304,9 @@ Processing payment Please wait… Payment failed - Unfortunately, this payment has been declined. + Try another payment method + Try payment again + Exit order Dimmed background. Tap to close the menu. Card reader connected @@ -4390,6 +4392,4 @@ Box Envelope Hmm, we can\'t find a WordPress.com account connected to this email address. - Try another payment method - Exit order diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModelTest.kt index a1d8d7d316b..e19c12dee6b 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModelTest.kt @@ -274,7 +274,7 @@ class WooPosHomeViewModelTest { } @Test - fun `given home screen is at checkout, when exit order clicked after failed payment, then should show cart`() = runTest { + fun `given home screen is at checkout, when go back to checkout clicked after failed payment, then should show cart with totals`() = runTest { // GIVEN val events = MutableSharedFlow() whenever(childrenToParentEventReceiver.events).thenReturn(events) @@ -286,12 +286,12 @@ class WooPosHomeViewModelTest { ).isEqualTo(WooPosHomeState.ScreenPositionState.Checkout.CartWithTotals) // WHEN - events.emit(ChildToParentEvent.ExitOrderAfterFailedTransactionClicked) + events.emit(ChildToParentEvent.GoBackToCheckoutAfterFailedPayment) // THEN assertThat( viewModel.state.value.screenPositionState - ).isEqualTo(WooPosHomeState.ScreenPositionState.Cart.Visible) + ).isEqualTo(WooPosHomeState.ScreenPositionState.Checkout.CartWithTotals) } @Test diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt index d9eb2c2e4b1..54784e1f829 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt @@ -513,37 +513,6 @@ class WooPosCartViewModelTest { assertThat(finalItem.isAppearanceAnimationPlayed).isTrue } - @Test - fun `given non-empty cart, when card payment is aborted, then should clear the cart`() = runTest { - // GIVEN - val product = ProductTestUtils.generateProduct( - productId = 23L, - productName = "title", - amount = "10.0" - ).copy(firstImageUrl = "url") - - val parentToChildrenEventsMutableFlow = MutableSharedFlow() - whenever(parentToChildrenEventReceiver.events).thenReturn(parentToChildrenEventsMutableFlow) - whenever(getProductById(eq(product.remoteId))).thenReturn(product) - val sut = createSut() - val states = sut.state.captureValues() - - parentToChildrenEventsMutableFlow.emit( - ParentToChildrenEvent.ItemClickedInProductSelector( - WooPosItemsViewModel.ItemClickedData.SimpleProduct(id = product.remoteId) - ) - ) - - // WHEN - parentToChildrenEventsMutableFlow.emit(ParentToChildrenEvent.OrderCardPaymentAborted) - - // THEN - val toolbar = states.last().toolbar - assertThat(toolbar.backIconVisible).isFalse() - assertThat(toolbar.itemsCount).isNull() - assertThat(toolbar.isClearAllButtonVisible).isFalse() - } - private fun createSut(): WooPosCartViewModel { return WooPosCartViewModel( childrenToParentEventSender, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt index ec8dda7c0e9..f020cdcd169 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt @@ -43,6 +43,7 @@ import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import com.woocommerce.android.util.CurrencyFormatter import com.woocommerce.android.util.UiStringParser import com.woocommerce.android.viewmodel.ResourceProvider +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -55,6 +56,7 @@ import org.junit.Before import org.junit.Rule import org.mockito.kotlin.any import org.mockito.kotlin.atLeastOnce +import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -288,10 +290,7 @@ class WooPosTotalsViewModelTest { val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock { on { events }.thenReturn(mock()) } - whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_failed_title)) - .thenReturn("Payment failed") - whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_failed_subtitle)) - .thenReturn("Unfortunately, this payment has been declined.") + val savedState = createMockSavedStateHandle() val viewModel = createViewModel( @@ -834,7 +833,7 @@ class WooPosTotalsViewModelTest { } @Test - fun `given payment failed, when retry clicked, then should retry`() = runTest { + fun `given payment failed with retry action, when retry clicked, then should retry previous payment action`() = runTest { // GIVEN whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_processing_title)) .thenReturn("Processing payment") @@ -843,6 +842,8 @@ class WooPosTotalsViewModelTest { whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_failed_title)) .thenReturn("Payment failed") whenever(uiStringParser.asString(any())).thenReturn("Unfortunately, this payment has been declined.") + whenever(resourceProvider.getString(R.string.woo_pos_payment_failed_try_again)) + .thenReturn("Try payment again") whenever(networkStatus.isConnected()).thenReturn(true) val readerStatus = MutableStateFlow(CardReaderStatus.Connected(mock())) @@ -857,17 +858,98 @@ class WooPosTotalsViewModelTest { whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + val failedPaymentRetryAction: ()->Unit = mock() paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.NonCancelable( - errorType = PaymentFlowError.NoNetwork, {} + errorType = PaymentFlowError.NoNetwork, failedPaymentRetryAction + ) + assertThat(vm.state.value).isInstanceOf(WooPosTotalsViewState.PaymentFailed::class.java) + assertTrue((paymentState.value as CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment).onRetry != null) + + // WHEN + vm.onUIEvent(WooPosTotalsUIEvent.RetryFailedTransactionClicked) + + // THEN + verify(failedPaymentRetryAction).invoke() + } + + @Test + fun `given payment failed without retry action, when retry clicked, then should cancel previous payment action and start again`() = + runTest { + // GIVEN + whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_processing_title)) + .thenReturn("Processing payment") + whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_processing_subtitle)) + .thenReturn("Please wait…") + whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_failed_title)) + .thenReturn("Payment failed") + whenever(uiStringParser.asString(any())).thenReturn("Unfortunately, this payment has been declined.") + whenever(resourceProvider.getString(R.string.woo_pos_payment_failed_try_another_payment_method)) + .thenReturn("Try another payment method") + whenever(networkStatus.isConnected()).thenReturn(true) + val readerStatus = MutableStateFlow(CardReaderStatus.Connected(mock())) + whenever(cardReaderFacade.readerStatus).thenReturn(readerStatus) + val mockCardReaderPaymentController: CardReaderPaymentController = mock() + val factory: CardReaderPaymentControllerFactory = mock() + whenever(factory.create(any(), any(), any(), any())).thenReturn(mockCardReaderPaymentController) + val paymentState = + MutableStateFlow( + CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + ) + whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) + val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.Cancelable( + errorType = PaymentFlowError.NoNetwork, onRetry = null, onCancel = {}, amountWithCurrencyLabel = "" + ) + assertThat(vm.state.value).isInstanceOf(WooPosTotalsViewState.PaymentFailed::class.java) + assertTrue((paymentState.value as CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment).onRetry == null) + + // WHEN + clearInvocations(mockCardReaderPaymentController) + vm.onUIEvent(WooPosTotalsUIEvent.RetryFailedTransactionClicked) + + // THEN + verify(mockCardReaderPaymentController).onCleared() + verify(mockCardReaderPaymentController).start() + } + + @Test + fun `given payment failed without retry action, when retry clicked, then should go back to checkout`() = runTest { + // GIVEN + whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_processing_title)) + .thenReturn("Processing payment") + whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_processing_subtitle)) + .thenReturn("Please wait…") + whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_failed_title)) + .thenReturn("Payment failed") + whenever(resourceProvider.getString(R.string.woo_pos_payment_failed_try_another_payment_method)) + .thenReturn("Try another payment method") + whenever(uiStringParser.asString(any())).thenReturn("Unfortunately, this payment has been declined.") + + whenever(networkStatus.isConnected()).thenReturn(true) + val readerStatus = MutableStateFlow(CardReaderStatus.Connected(mock())) + whenever(cardReaderFacade.readerStatus).thenReturn(readerStatus) + val mockCardReaderPaymentController: CardReaderPaymentController = mock() + val factory: CardReaderPaymentControllerFactory = mock() + whenever(factory.create(any(), any(), any(), any())).thenReturn(mockCardReaderPaymentController) + val paymentState = + MutableStateFlow( + CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + ) + whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) + val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.Cancelable( + errorType = PaymentFlowError.NoNetwork, onRetry = null, onCancel = {}, amountWithCurrencyLabel = "" ) assertThat(vm.state.value).isInstanceOf(WooPosTotalsViewState.PaymentFailed::class.java) + assertTrue((paymentState.value as CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment).onRetry == null) // WHEN - paymentState.value = CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} vm.onUIEvent(WooPosTotalsUIEvent.RetryFailedTransactionClicked) // THEN - assertThat(vm.state.value).isInstanceOf(WooPosTotalsViewState.Totals::class.java) + verify(childrenToParentEventSender).sendToParent(ChildToParentEvent.RetryFailedPaymentClicked) } @Test @@ -879,6 +961,8 @@ class WooPosTotalsViewModelTest { .thenReturn("Please wait…") whenever(resourceProvider.getString(R.string.woopos_success_totals_payment_failed_title)) .thenReturn("Payment failed") + whenever(resourceProvider.getString(R.string.woo_pos_payment_failed_try_again)) + .thenReturn("Try payment again") whenever(uiStringParser.asString(any())).thenReturn("Unfortunately, this payment has been declined.") whenever(networkStatus.isConnected()).thenReturn(true) @@ -900,10 +984,10 @@ class WooPosTotalsViewModelTest { assertThat(vm.state.value).isInstanceOf(WooPosTotalsViewState.PaymentFailed::class.java) // WHEN - vm.onUIEvent(WooPosTotalsUIEvent.ExitOrderAfterFailedTransactionClicked) + vm.onUIEvent(WooPosTotalsUIEvent.GoBackToCheckoutAfterFailedPayment) // THEN - verify(childrenToParentEventSender).sendToParent(ChildToParentEvent.ExitOrderAfterFailedTransactionClicked) + verify(childrenToParentEventSender).sendToParent(ChildToParentEvent.GoBackToCheckoutAfterFailedPayment) } private suspend fun createViewModelAndSetupForSuccessfulOrderCreation(