diff --git a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt index 388bb9eabea2..755029b0f4d5 100644 --- a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt +++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt @@ -80,12 +80,12 @@ class MockCardReaderManagerModule { override fun setupTapToPayUx(config: TapToPayUxConfig) {} - override fun startConnectionToReader(cardReader: CardReader, locationId: String) {} + override suspend fun startConnectionToReader(cardReader: CardReader, locationId: String) {} override suspend fun disconnectReader(): Boolean = true override suspend fun collectPayment(paymentInfo: PaymentInfo): Flow = - flowOf(CardPaymentStatus.CollectingPayment) + flowOf(CardPaymentStatus.ProcessingPayment) override suspend fun refundInteracPayment( refundParams: RefundParams, @@ -100,6 +100,8 @@ class MockCardReaderManagerModule { override fun cancelPayment(paymentData: PaymentData) {} + override fun cancelReconnection() {} + override suspend fun startAsyncSoftwareUpdate() {} override suspend fun clearCachedCredentials() {} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/connect/CardReaderConnectViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/connect/CardReaderConnectViewModel.kt index 181ec8e92119..87f2d92a967c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/connect/CardReaderConnectViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/connect/CardReaderConnectViewModel.kt @@ -303,6 +303,10 @@ class CardReaderConnectViewModel @Inject constructor( connectionStarted = true viewState.value = provideConnectingState() } + + CardReaderStatus.Reconnecting -> { + // Reconnecting is handled by the SDK, no action needed during connection flow + } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailFragment.kt index 11eb5ac98526..1420cd5f26d8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailFragment.kt @@ -27,6 +27,7 @@ import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailVie import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.ConnectedState import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.Loading import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.NotConnectedState +import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.ReconnectingState import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderType.EXTERNAL import com.woocommerce.android.ui.payments.cardreader.update.CardReaderUpdateDialogFragment import com.woocommerce.android.ui.payments.cardreader.update.CardReaderUpdateViewModel.UpdateResult @@ -180,6 +181,25 @@ class CardReaderDetailFragment : BaseFragment(R.layout.fragment_card_reader_deta Loading -> { } + + is ReconnectingState -> { + with(binding.readerDisconnectedState) { + UiHelpers.setTextOrHide(cardReaderDetailConnectHeaderLabel, state.headerLabel) + UiHelpers.setImageOrHideInLandscapeOnCompactScreenHeightSizeClass( + cardReaderDetailIllustration, + state.illustration + ) + cardReaderDetailFirstHintLabel.visibility = View.GONE + cardReaderDetailFirstHintNumberLabel.visibility = View.GONE + cardReaderDetailSecondHintLabel.visibility = View.GONE + cardReaderDetailSecondHintNumberLabel.visibility = View.GONE + cardReaderDetailThirdHintLabel.visibility = View.GONE + cardReaderDetailThirdHintNumberLabel.visibility = View.GONE + UiHelpers.setTextOrHide(cardReaderDetailConnectBtn, state.cancelBtnLabel) + cardReaderDetailConnectBtn.setOnClickListener { state.onCancelClicked.invoke() } + cardReaderDetailLearnMoreTv.root.visibility = View.GONE + } + } } } } @@ -192,7 +212,10 @@ class CardReaderDetailFragment : BaseFragment(R.layout.fragment_card_reader_deta private fun makeStateVisible(binding: FragmentCardReaderDetailBinding, state: ViewState) { UiHelpers.updateVisibility(binding.readerConnectedState.root, state is ConnectedState) - UiHelpers.updateVisibility(binding.readerDisconnectedState.root, state is NotConnectedState) + UiHelpers.updateVisibility( + binding.readerDisconnectedState.root, + state is NotConnectedState || state is ReconnectingState + ) UiHelpers.updateVisibility(binding.readerConnectedLoading, state is Loading) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModel.kt index a77fa5141254..7aa961de8320 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModel.kt @@ -13,6 +13,7 @@ import com.woocommerce.android.cardreader.connection.CardReader import com.woocommerce.android.cardreader.connection.CardReaderStatus.Connected import com.woocommerce.android.cardreader.connection.CardReaderStatus.Connecting import com.woocommerce.android.cardreader.connection.CardReaderStatus.NotConnected +import com.woocommerce.android.cardreader.connection.CardReaderStatus.Reconnecting import com.woocommerce.android.cardreader.connection.ReaderType import com.woocommerce.android.cardreader.connection.event.CardReaderBatteryStatus import com.woocommerce.android.cardreader.connection.event.CardReaderBatteryStatus.StatusChanged @@ -86,6 +87,9 @@ class CardReaderDetailViewModel @Inject constructor( ) handleNotConnectedState() } + Reconnecting -> { + handleReconnectingState() + } } } } @@ -134,6 +138,16 @@ class CardReaderDetailViewModel @Inject constructor( NotConnectedState(onPrimaryActionClicked = ::onConnectBtnClicked, onLearnMoreClicked = ::onLearnMoreClicked) } + private fun handleReconnectingState() { + viewState.value = ViewState.ReconnectingState( + onCancelClicked = ::onCancelReconnectionClicked + ) + } + + private fun onCancelReconnectionClicked() { + cardReaderManager.cancelReconnection() + } + private fun cancelConnectedScopeJobs() { if (::softwareUpdateAvailabilityJob.isInitialized) softwareUpdateAvailabilityJob.cancel() if (::batteryStatusUpdateJob.isInitialized) batteryStatusUpdateJob.cancel() @@ -307,6 +321,16 @@ class CardReaderDetailViewModel @Inject constructor( } object Loading : ViewState() + + data class ReconnectingState( + val onCancelClicked: (() -> Unit), + ) : ViewState() { + val headerLabel = UiStringRes(R.string.card_reader_detail_reconnecting_header) + + @DrawableRes + val illustration = R.drawable.img_card_reader_not_connected + val cancelBtnLabel = UiStringRes(R.string.card_reader_detail_reconnecting_cancel) + } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentStateToViewStateMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentStateToViewStateMapper.kt index 92d31dc166f4..761af5ef8931 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentStateToViewStateMapper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentStateToViewStateMapper.kt @@ -5,7 +5,6 @@ import com.woocommerce.android.model.UiString import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderType import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderInteracRefundState -import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.CollectingPayment import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.LoadingData import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.PaymentCapturing import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentOrRefundState.CardReaderPaymentState.PaymentFailed @@ -42,21 +41,6 @@ class CardReaderPaymentStateToViewStateMapper @Inject constructor( is CardReaderInteracRefundState.ProcessingInteracRefund -> { ViewState.ProcessingRefundState(paymentState.amountWithCurrencyLabel) } - is CollectingPayment.BuiltInReaderCollectPaymentState -> { - ViewState.BuiltInReaderCollectPaymentState( - amountWithCurrencyLabel = paymentState.amountWithCurrencyLabel, - hintLabel = paymentState.cardReaderHint - ?: R.string.card_reader_payment_collect_payment_built_in_hint - ) - } - is CollectingPayment.ExternalReaderCollectPaymentState -> { - ViewState.ExternalReaderCollectPaymentState( - amountWithCurrencyLabel = paymentState.amountWithCurrencyLabel, - hintLabel = paymentState.cardReaderHint - ?: R.string.card_reader_payment_collect_payment_hint, - onSecondaryActionClicked = paymentState.onCancel - ) - } is LoadingData -> ViewState.LoadingDataState(paymentState.onCancel) is PaymentCapturing.BuiltInReaderPaymentCapturing -> { ViewState.BuiltInReaderCapturingPaymentState( @@ -103,7 +87,9 @@ class CardReaderPaymentStateToViewStateMapper @Inject constructor( is ProcessingPayment.ExternalReaderProcessingPayment -> { ViewState.ExternalReaderProcessingPaymentState( amountWithCurrencyLabel = paymentState.amountWithCurrencyLabel, - onSecondaryActionClicked = paymentState.onCancel + onSecondaryActionClicked = paymentState.onCancel, + hintLabel = paymentState.cardReaderHint + ?: R.string.card_reader_payment_collect_payment_hint, ) } ReFetchingOrder -> ViewState.ReFetchingOrderState diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt index d0621bd6b925..15c3ab5a50ae 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/CardReaderPaymentViewState.kt @@ -105,10 +105,10 @@ sealed class ViewState( data class ExternalReaderProcessingPaymentState( override val amountWithCurrencyLabel: String, override val onSecondaryActionClicked: (() -> Unit), + override val hintLabel: Int = R.string.card_reader_payment_collect_payment_hint, ) : ViewState( - hintLabel = R.string.card_reader_payment_processing_payment_hint, - headerLabel = R.string.card_reader_payment_processing_payment_header, - paymentStateLabel = UiStringRes(R.string.card_reader_payment_processing_payment_state), + headerLabel = R.string.card_reader_payment_collect_payment_header, + paymentStateLabel = UiStringRes(R.string.card_reader_payment_collect_payment_state), illustration = R.drawable.img_card_reader_available, secondaryActionLabel = R.string.cancel, ), 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 363ca5f8c50a..1d7dc82482ef 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 @@ -29,7 +29,6 @@ import com.woocommerce.android.cardreader.payments.CardPaymentStatus.AdditionalI import com.woocommerce.android.cardreader.payments.CardPaymentStatus.AdditionalInfoType.TRY_ANOTHER_CARD import com.woocommerce.android.cardreader.payments.CardPaymentStatus.AdditionalInfoType.TRY_ANOTHER_READ_METHOD import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CapturingPayment -import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CollectingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.InitializingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentCompleted import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentFailed @@ -318,14 +317,6 @@ class CardReaderPaymentController( CardReaderPaymentState.LoadingData(::onCancelPaymentFlow) } - CollectingPayment -> { - _paymentState.value = paymentStateProvider.provideCollectingPaymentState( - cardReaderType, - amountLabel, - ::onCancelPaymentFlow - ) - } - ProcessingPayment -> { _paymentState.value = paymentStateProvider.provideProcessingPaymentState( cardReaderType, @@ -643,12 +634,12 @@ class CardReaderPaymentController( ) } - is CardReaderPaymentState.CollectingPayment.BuiltInReaderCollectPaymentState -> + is CardReaderPaymentState.ProcessingPayment.BuiltInReaderProcessingPayment -> _paymentState.value = state.copy( cardReaderHint = cardReaderHint.toHintLabel(false) ) - is CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState -> + is CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment -> _paymentState.value = state.copy( cardReaderHint = cardReaderHint.toHintLabel(false) ) 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 index f88e355a0184..6abfcc4252e3 100644 --- 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 @@ -10,32 +10,20 @@ sealed class CardReaderPaymentOrRefundState { data object ReFetchingOrder : CardReaderPaymentState() - sealed class CollectingPayment( + sealed class ProcessingPayment( 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( + data class BuiltInReaderProcessingPayment( 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) + @StringRes override val cardReaderHint: Int? = null, + ) : ProcessingPayment(amountWithCurrencyLabel, cardReaderHint) data class ExternalReaderProcessingPayment( override val amountWithCurrencyLabel: String, - val onCancel: () -> Unit - ) : ProcessingPayment(amountWithCurrencyLabel) + val onCancel: () -> Unit, + @StringRes override val cardReaderHint: Int? = null, + ) : ProcessingPayment(amountWithCurrencyLabel, cardReaderHint) } data class PrintingReceipt(val amountWithCurrencyLabel: String) : CardReaderPaymentState() 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 index a69ae3e45e14..caeac2cecdc3 100644 --- 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 @@ -4,7 +4,6 @@ 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 @@ -55,21 +54,6 @@ class CardReaderPaymentStateProvider @Inject constructor() { ) } - 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, 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 index 5f76bb1620da..25a6e4f0957d 100644 --- 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 @@ -12,7 +12,6 @@ class CardReaderTrackCanceledFlowAction @Inject constructor( 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" diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cardreader/WooPosCardReaderFacade.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cardreader/WooPosCardReaderFacade.kt index 6e0f1dfaa94e..4860757cda97 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cardreader/WooPosCardReaderFacade.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/cardreader/WooPosCardReaderFacade.kt @@ -49,6 +49,10 @@ class WooPosCardReaderFacade @Inject constructor( cardReaderManager.disconnectReader() } + fun cancelReconnection() { + cardReaderManager.cancelReconnection() + } + @Suppress("DEPRECATION") private fun startActivity(intent: Intent) { val options = ActivityOptionsCompat.makeCustomAnimation( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbar.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbar.kt index 8317d2da3e2a..57d7532baa9a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbar.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbar.kt @@ -309,15 +309,11 @@ private fun CardReaderStatusButton( when (status) { WooPosCardReaderStatus.Connected -> WooPosTheme.colors.success WooPosCardReaderStatus.NotConnected -> WooPosTheme.colors.alert + WooPosCardReaderStatus.Reconnecting -> WooPosTheme.colors.alert } } - val title = stringResource( - id = when (state) { - WooPosCardReaderStatus.Connected -> WooPosCardReaderStatus.Connected.title - WooPosCardReaderStatus.NotConnected -> WooPosCardReaderStatus.NotConnected.title - } - ) + val title = stringResource(id = state.title) val borderColor by transition.animateColor( transitionSpec = { tween(durationMillis = animationDuration) }, @@ -326,6 +322,7 @@ private fun CardReaderStatusButton( when (status) { WooPosCardReaderStatus.Connected -> Color.Transparent WooPosCardReaderStatus.NotConnected -> MaterialTheme.colorScheme.primary + WooPosCardReaderStatus.Reconnecting -> WooPosTheme.colors.alert } } @@ -408,6 +405,9 @@ private fun getToolbarAccessibilityLabels( WooPosCardReaderStatus.NotConnected -> stringResource( id = R.string.woopos_floating_toolbar_card_reader_not_connected_status_content_description ) + WooPosCardReaderStatus.Reconnecting -> stringResource( + id = R.string.woopos_reader_reconnecting + ) } val floatingToolbarMenuOverlayContentDescription = when (menuCardDisabled) { true -> { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarState.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarState.kt index 87fe185b1fde..eddef459c231 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarState.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarState.kt @@ -11,6 +11,7 @@ data class WooPosHomeFloatingToolbarState( sealed class WooPosCardReaderStatus(@StringRes val title: Int) { data object NotConnected : WooPosCardReaderStatus(title = R.string.woopos_reader_disconnected) data object Connected : WooPosCardReaderStatus(title = R.string.woopos_reader_connected) + data object Reconnecting : WooPosCardReaderStatus(title = R.string.woopos_reader_reconnecting) } sealed class Menu { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModel.kt index a0ea933f711e..38a51be68a0d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModel.kt @@ -11,6 +11,7 @@ import com.woocommerce.android.cardreader.connection.CardReaderStatus import com.woocommerce.android.cardreader.connection.CardReaderStatus.Connected import com.woocommerce.android.cardreader.connection.CardReaderStatus.Connecting import com.woocommerce.android.cardreader.connection.CardReaderStatus.NotConnected +import com.woocommerce.android.cardreader.connection.CardReaderStatus.Reconnecting import com.woocommerce.android.ui.woopos.cardreader.WooPosCardReaderFacade import com.woocommerce.android.ui.woopos.home.ChildToParentEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender @@ -128,12 +129,17 @@ class WooPosHomeFloatingToolbarViewModel @Inject constructor( cardReaderFacade.connectToReader() } } + + WooPosHomeFloatingToolbarState.WooPosCardReaderStatus.Reconnecting -> { + cardReaderFacade.cancelReconnection() + } } } private fun mapCardReaderStatusToUiState(status: CardReaderStatus) = when (status) { is Connected -> WooPosHomeFloatingToolbarState.WooPosCardReaderStatus.Connected is NotConnected, Connecting -> WooPosHomeFloatingToolbarState.WooPosCardReaderStatus.NotConnected + Reconnecting -> WooPosHomeFloatingToolbarState.WooPosCardReaderStatus.Reconnecting } private val toolbarMenuItems by lazy { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsAnalyticsTracker.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsAnalyticsTracker.kt index ad9981391c1c..8e9af9344cdd 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsAnalyticsTracker.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsAnalyticsTracker.kt @@ -22,15 +22,11 @@ class WooPosTotalsAnalyticsTracker @Inject constructor( suspend fun trackPaymentStates(paymentState: StateFlow?) { paymentState?.distinctUntilChanged { old, new -> old::class == new::class }?.collect { when (it) { - is CardReaderPaymentState.CollectingPayment -> { + is CardReaderPaymentState.ProcessingPayment -> { analyticsData.readerReadyForPaymentTimestamp = System.currentTimeMillis() trackReaderReadyForPayment() } - is CardReaderPaymentState.ProcessingPayment -> { - analyticsData.cardTappedTimestamp = System.currentTimeMillis() - } - is CardReaderPaymentOrRefundState.CardReaderInteracRefundState.CollectingInteracRefund, is CardReaderPaymentOrRefundState.CardReaderInteracRefundState.InteracRefundFailure.Cancelable, is CardReaderPaymentOrRefundState.CardReaderInteracRefundState.InteracRefundFailure.NonCancelable, 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 58ca9e213fe2..1731a9063e42 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 @@ -9,6 +9,7 @@ import com.woocommerce.android.WooException import com.woocommerce.android.cardreader.connection.CardReaderStatus.Connected import com.woocommerce.android.cardreader.connection.CardReaderStatus.Connecting import com.woocommerce.android.cardreader.connection.CardReaderStatus.NotConnected +import com.woocommerce.android.cardreader.connection.CardReaderStatus.Reconnecting import com.woocommerce.android.model.Order import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderFlowParam.PaymentOrRefund import com.woocommerce.android.ui.payments.cardreader.payment.controller.CardReaderPaymentController @@ -122,6 +123,10 @@ class WooPosTotalsViewModel @Inject constructor( cancelPaymentAction() } + Reconnecting -> { + // We start payment right away so this state not worth handling + } + is Connected -> { val state = uiState.value if (state !is WooPosTotalsViewState.Checkout) return@collect @@ -375,13 +380,12 @@ class WooPosTotalsViewModel @Inject constructor( viewModelScope.launch { cardReaderPaymentController?.paymentState?.collect { paymentState -> when (paymentState) { - is CardReaderPaymentState.CollectingPayment -> handleCollectingPaymentState(paymentState) + is CardReaderPaymentState.ProcessingPayment -> handleProcessingPaymentState(paymentState) is CardReaderPaymentState.LoadingData -> handleReaderLoadingPaymentState() - is CardReaderPaymentState.PaymentCapturing, - is CardReaderPaymentState.ProcessingPayment -> { - handleProcessingOrCapturingPaymentState() + is CardReaderPaymentState.PaymentCapturing -> { + handleCapturingPaymentState() } is CardReaderPaymentState.PaymentSuccessful -> { @@ -407,12 +411,10 @@ class WooPosTotalsViewModel @Inject constructor( viewModelScope.launch { totalsAnalyticsTracker.trackPaymentStates(cardReaderPaymentController?.paymentState) } } - private suspend fun handleProcessingOrCapturingPaymentState() { + private suspend fun handleCapturingPaymentState() { val state = uiState.value if (state is WooPosTotalsViewState.Checkout) { uiState.value = state.copy(totals = Totals.Hidden) - // allow the UI to show "shrinking" exit animation of totals grid before showing - // the "payment in progress" state. @Suppress("MagicNumber") delay(384) } @@ -423,7 +425,7 @@ class WooPosTotalsViewModel @Inject constructor( ) } - private suspend fun handleCollectingPaymentState(paymentState: CardReaderPaymentState.CollectingPayment) { + private suspend fun handleProcessingPaymentState(paymentState: CardReaderPaymentState.ProcessingPayment) { val totalsState = uiState.value if (totalsState is WooPosTotalsViewState.Checkout) { uiState.value = totalsState.copy( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/hardware/cardreader/WooPosSettingsHardwareCardReaderViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/hardware/cardreader/WooPosSettingsHardwareCardReaderViewModel.kt index 014390df8ab7..b1c29fc71cdd 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/hardware/cardreader/WooPosSettingsHardwareCardReaderViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/settings/details/hardware/cardreader/WooPosSettingsHardwareCardReaderViewModel.kt @@ -115,6 +115,11 @@ class WooPosSettingsHardwareCardReaderViewModel @Inject constructor( currentSoftwareUpdateAvailable = false WooPosSettingsHardwareCardReaderUiState.Disconnected } + + CardReaderStatus.Reconnecting -> { + // Keep current state while SDK attempts to reconnect + _uiState.value + } } } } diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index ac0e6093b3d0..7df88370f621 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -1634,6 +1634,8 @@ Card Reader Detail --> Connect your card reader + Reconnecting to card reader… + Cancel reconnection <a href=\'\'>Learn more</a> about accepting mobile payments and ordering card readers Make sure card reader is charged Turn card reader on and place it next to mobile device @@ -3590,6 +3592,7 @@ Reader connected Connect your reader + Reconnecting… Check out Remove %s from cart Product %s, Price %s 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 d3c2a7bbdecf..e67aa8861001 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 @@ -28,7 +28,6 @@ import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType.NoNetwork import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType.ReaderNotConnected import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType.Server -import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CollectingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.InitializingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentCompleted import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentFailed @@ -65,14 +64,12 @@ import com.woocommerce.android.ui.payments.cardreader.payment.PaymentFlowError.U import com.woocommerce.android.ui.payments.cardreader.payment.PlayChaChing import com.woocommerce.android.ui.payments.cardreader.payment.PrintReceipt import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderCapturingPaymentState -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.BuiltInReaderPaymentSuccessfulReceiptSentAutomaticallyState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderPaymentSuccessfulState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.BuiltInReaderProcessingPaymentState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.CollectRefundState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderCapturingPaymentState -import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderCollectPaymentState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderFailedPaymentState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderPaymentSuccessfulReceiptSentAutomaticallyState import com.woocommerce.android.ui.payments.cardreader.payment.ViewState.ExternalReaderPaymentSuccessfulState @@ -262,13 +259,13 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() advanceUntilIdle() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_retry_card_prompt) } @@ -282,12 +279,12 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_collect_payment_hint) } @@ -301,12 +298,12 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_collect_payment_hint) } @@ -320,12 +317,12 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_collect_payment_hint) } @@ -340,13 +337,13 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() advanceUntilIdle() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_remove_card_prompt) } @@ -361,13 +358,13 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() advanceUntilIdle() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_try_another_card_prompt) } @@ -382,13 +379,13 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() advanceUntilIdle() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_card_removed_too_early) } @@ -403,13 +400,13 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() advanceUntilIdle() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_try_another_read_method_prompt) } @@ -424,13 +421,13 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() advanceUntilIdle() - assertThat((viewModel.viewStateData.value as ExternalReaderCollectPaymentState).hintLabel) + assertThat((viewModel.viewStateData.value as ExternalReaderProcessingPaymentState).hintLabel) .isEqualTo(R.string.card_reader_payment_multiple_contactless_cards_detected_prompt) } @@ -532,25 +529,25 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { fun `when collecting payment, then ui updated to collecting payment state`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() - assertThat(viewModel.viewStateData.value).isInstanceOf(ExternalReaderCollectPaymentState::class.java) + assertThat(viewModel.viewStateData.value).isInstanceOf(ExternalReaderProcessingPaymentState::class.java) } @Test fun `given built in reader,when collecting payment, then ui updated to collecting payment state`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } initViewModel(BUILT_IN) viewModel.start() - assertThat(viewModel.viewStateData.value).isInstanceOf(BuiltInReaderCollectPaymentState::class.java) + assertThat(viewModel.viewStateData.value).isInstanceOf(BuiltInReaderProcessingPaymentState::class.java) } @Test @@ -1511,7 +1508,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { fun `when collecting payment, then progress and cancel button is visible`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -1527,7 +1524,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { fun `when collecting payment, then correct labels and illustration is shown`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -1572,15 +1569,15 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { val viewState = viewModel.viewStateData.value!! assertThat(viewState.headerLabel).describedAs("headerLabel") - .isEqualTo(R.string.card_reader_payment_processing_payment_header) + .isEqualTo(R.string.card_reader_payment_collect_payment_header) assertThat(viewState.amountWithCurrencyLabel).describedAs("amountWithCurrencyLabel") .isEqualTo("$DUMMY_CURRENCY_SYMBOL$DUMMY_TOTAL") assertThat(viewState.illustration).describedAs("illustration") .isEqualTo(R.drawable.img_card_reader_available) assertThat(viewState.paymentStateLabel).describedAs("paymentStateLabel") - .isEqualTo(UiStringRes(R.string.card_reader_payment_processing_payment_state)) + .isEqualTo(UiStringRes(R.string.card_reader_payment_collect_payment_state)) assertThat(viewState.hintLabel).describedAs("hintLabel") - .isEqualTo(R.string.card_reader_payment_processing_payment_hint) + .isEqualTo(R.string.card_reader_payment_collect_payment_hint) } @Test diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModelTest.kt index ae04d3e00d00..c8787d222305 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModelTest.kt @@ -22,6 +22,7 @@ import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailVie import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.ConnectedState import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.Loading import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.NotConnectedState +import com.woocommerce.android.ui.payments.cardreader.detail.CardReaderDetailViewModel.ViewState.ReconnectingState import com.woocommerce.android.ui.payments.cardreader.onboarding.CardReaderFlowParam import com.woocommerce.android.ui.payments.cardreader.onboarding.PluginType.STRIPE_EXTENSION_GATEWAY import com.woocommerce.android.ui.payments.cardreader.onboarding.PluginType.WOOCOMMERCE_PAYMENTS @@ -620,6 +621,33 @@ class CardReaderDetailViewModelTest : BaseUnitTest() { .isEqualTo(AppUrls.STRIPE_LEARN_MORE_ABOUT_PAYMENTS) } + @Test + fun `given reconnecting state, when view model init, then should emit reconnecting view state`() { + // GIVEN + val status = MutableStateFlow(CardReaderStatus.Reconnecting) + whenever(cardReaderManager.readerStatus).thenReturn(status) + + // WHEN + val viewModel = createViewModel() + + // THEN + assertThat(viewModel.viewStateData.value).isInstanceOf(ReconnectingState::class.java) + } + + @Test + fun `given reconnecting state, when cancel clicked, then should call cancelReconnection`() { + // GIVEN + val status = MutableStateFlow(CardReaderStatus.Reconnecting) + whenever(cardReaderManager.readerStatus).thenReturn(status) + val viewModel = createViewModel() + + // WHEN + (viewModel.viewStateData.value as ReconnectingState).onCancelClicked.invoke() + + // THEN + verify(cardReaderManager).cancelReconnection() + } + private fun verifyNotConnectedState(viewModel: CardReaderDetailViewModel) { val state = viewModel.viewStateData.value as NotConnectedState assertThat(state.headerLabel) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentControllerTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentControllerTest.kt index cabdb93c95c5..6717e19f71ad 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentControllerTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/payment/controller/CardReaderPaymentControllerTest.kt @@ -26,7 +26,6 @@ import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType.Generic import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType.NoNetwork import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType.Server -import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CollectingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.InitializingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentCompleted import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentFailed @@ -229,13 +228,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(RETRY_CARD) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_retry_card_prompt) } @@ -246,13 +245,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(INSERT_CARD) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_collect_payment_hint) } @@ -263,13 +262,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(INSERT_OR_SWIPE_CARD) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_collect_payment_hint) } @@ -280,13 +279,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(SWIPE_CARD) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_collect_payment_hint) } @@ -297,13 +296,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(REMOVE_CARD) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_remove_card_prompt) } @@ -314,14 +313,14 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(TRY_ANOTHER_CARD) advanceUntilIdle() - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_try_another_card_prompt) } @@ -332,13 +331,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(CARD_REMOVED_TOO_EARLY) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_card_removed_too_early) } @@ -349,13 +348,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(TRY_ANOTHER_READ_METHOD) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_try_another_read_method_prompt) } @@ -366,14 +365,14 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { val readerMessages = MutableStateFlow(CardReaderNoMessage) whenever(cardReaderManager.collectPayment(any())).thenReturn(paymentStatus) whenever(cardReaderManager.displayBluetoothCardReaderMessages).thenReturn(readerMessages) - paymentStatus.value = CollectingPayment + paymentStatus.value = ProcessingPayment controller.start() readerMessages.value = BluetoothCardReaderMessages.CardReaderDisplayMessage(MULTIPLE_CONTACTLESS_CARDS_DETECTED) - assertThat((controller.paymentState.value as CardReaderPaymentState.CollectingPayment).cardReaderHint) + assertThat((controller.paymentState.value as CardReaderPaymentState.ProcessingPayment).cardReaderHint) .isEqualTo(R.string.card_reader_payment_multiple_contactless_cards_detected_prompt) } @@ -577,30 +576,30 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { } @Test - fun `when collecting payment, then CollectingPayment state emitted`() = + fun `when collecting payment, then ProcessingPayment state emitted`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } controller.start() assertThat(controller.paymentState.value) - .isInstanceOf(CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState::class.java) + .isInstanceOf(CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment::class.java) } @Test - fun `given built in reader,when collecting payment, then CollectingPayment state emitted`() = + fun `given built in reader, when collecting payment, then ProcessingPayment state emitted`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } createController(BUILT_IN) controller.start() assertThat(controller.paymentState.value) - .isInstanceOf(CardReaderPaymentState.CollectingPayment.BuiltInReaderCollectPaymentState::class.java) + .isInstanceOf(CardReaderPaymentState.ProcessingPayment.BuiltInReaderProcessingPayment::class.java) } @Test @@ -1336,13 +1335,13 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { fun `when collecting payment, then cancellation is possible`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } controller.start() val paymentState = controller.paymentState.value as - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment assertNotNull(paymentState.onCancel) } @@ -2089,19 +2088,6 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { verify(tracker).trackPaymentCancelled("Loading") } - @Test - fun `given payment flow is collecting state, when user presses back button, then cancel event is tracked`() = - testBlocking { - whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } - } - controller.start() - - controller.onBackPressed() - - verify(tracker).trackPaymentCancelled("Collecting") - } - @Test fun `given payment flow is processing state, when user presses back button, then cancel event is tracked`() = testBlocking { @@ -2188,27 +2174,27 @@ class CardReaderPaymentControllerTest : BaseUnitTest() { fun `given payment flow is collection payment state, when user presses cancel, then cancel event is tracked`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } controller.start() val state = controller.paymentState.value as - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment state.onCancel() - verify(tracker).trackPaymentCancelled("Collecting") + verify(tracker).trackPaymentCancelled("Processing") } @Test fun `given payment flow is collection payment state, when user presses cancel, then exit event emitted`() = testBlocking { whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } val events = controller.event.runAndCaptureValues { controller.start() val state = controller.paymentState.value as - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment state.onCancel() } assertThat(events.last()).isInstanceOf(CardReaderPaymentEvent.Exit::class.java) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModelTest.kt index 597b5bc47dae..2ddf5fdaf9fe 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModelTest.kt @@ -257,6 +257,31 @@ class WooPosHomeFloatingToolbarViewModelTest { assertThat(viewModel.state.value.menu).isEqualTo(WooPosHomeFloatingToolbarState.Menu.Hidden) } + @Test + fun `given card reader status is Reconnecting, when initialized, then state should be Reconnecting`() = runTest { + // GIVEN + whenever(cardReaderFacade.readerStatus).thenReturn(MutableStateFlow(CardReaderStatus.Reconnecting)) + val viewModel = createViewModel() + + // THEN + assertThat(viewModel.state.value.cardReaderStatus) + .isEqualTo(WooPosHomeFloatingToolbarState.WooPosCardReaderStatus.Reconnecting) + } + + @Test + fun `given card reader status is Reconnecting, when OnCardReaderStatusClicked, then cancelReconnection should be called`() = + runTest { + // GIVEN + whenever(cardReaderFacade.readerStatus).thenReturn(MutableStateFlow(CardReaderStatus.Reconnecting)) + val viewModel = createViewModel() + + // WHEN + viewModel.onUiEvent(WooPosHomeFloatingToolbarUIEvent.OnCardReaderStatusClicked) + + // THEN + verify(cardReaderFacade).cancelReconnection() + } + private fun createViewModel() = WooPosHomeFloatingToolbarViewModel( cardReaderFacade, 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 85923257f711..bb2a6e94e082 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 @@ -776,13 +776,13 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) // WHEN - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.PaymentCapturing.ExternalReaderPaymentCapturing("") advanceUntilIdle() // THEN @@ -809,7 +809,7 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) @@ -843,7 +843,7 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) @@ -862,7 +862,7 @@ class WooPosTotalsViewModelTest { } @Test - fun `given order draft created and reader connected, when payment is processed, should show processing state`() = + fun `given order draft created and reader connected, when payment is processed, then should show checkout with ready for payment`() = runTest { // GIVEN givenCardReaderConnectedAndNetworkAvailable() @@ -871,22 +871,20 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) // WHEN val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) advanceUntilIdle() // THEN - val processingState = vm.state.value as WooPosTotalsViewState.PaymentInProgress - assertThat(processingState).isInstanceOf(WooPosTotalsViewState.PaymentInProgress::class.java) - with(processingState) { - assertThat(title).isEqualTo("Processing payment") - assertThat(subtitle).isEqualTo("Please wait…") - } + val checkoutState = vm.state.value as WooPosTotalsViewState.Checkout + assertThat(checkoutState.readerStatus).isInstanceOf( + WooPosTotalsViewState.ReaderStatus.ReadyForPayment::class.java + ) } @Test @@ -916,11 +914,11 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) val failedPaymentRetryAction: () -> Unit = mock() paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.NonCancelable( errorType = PaymentFlowError.NoNetwork, failedPaymentRetryAction @@ -957,11 +955,11 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.Cancelable( errorType = PaymentFlowError.NoNetwork, onRetry = null, onCancel = {}, amountWithCurrencyLabel = "" ) @@ -999,11 +997,11 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.Cancelable( errorType = PaymentFlowError.NoNetwork, onRetry = null, onCancel = {}, amountWithCurrencyLabel = "" ) @@ -1038,11 +1036,11 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.NonCancelable( errorType = PaymentFlowError.NoNetwork, {} ) @@ -1261,7 +1259,7 @@ class WooPosTotalsViewModelTest { } @Test - fun `given payment processing state, when OnBackClicked, then should ignore OnBackClicked`() = runTest { + fun `given payment in progress state with capturing payment, when OnBackClicked, then should ignore OnBackClicked`() = runTest { // GIVEN givenCardReaderConnectedAndNetworkAvailable() val mockCardReaderPaymentController: CardReaderPaymentController = mock() @@ -1269,13 +1267,13 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) // WHEN val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.PaymentCapturing.ExternalReaderPaymentCapturing("") advanceUntilIdle() vm.onUIEvent(OnBackClicked) @@ -1295,7 +1293,7 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) @@ -1313,7 +1311,7 @@ class WooPosTotalsViewModelTest { } @Test - fun `given payment collecting state, when OnBackClicked, then should not ignore OnBackClicked`() = runTest { + fun `given payment processing state, when OnBackClicked, then should not ignore OnBackClicked`() = runTest { // GIVEN givenCardReaderConnectedAndNetworkAvailable() val mockCardReaderPaymentController: CardReaderPaymentController = mock() @@ -1321,7 +1319,7 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) @@ -1346,13 +1344,13 @@ class WooPosTotalsViewModelTest { whenever(factory.create(any(), any(), any())).thenReturn(mockCardReaderPaymentController) val paymentState = MutableStateFlow( - CardReaderPaymentState.CollectingPayment.ExternalReaderCollectPaymentState("") {} + CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) ) whenever(mockCardReaderPaymentController.paymentState).thenReturn(paymentState) // WHEN val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("") {} + paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) paymentState.value = CardReaderPaymentState.PaymentFailed.ExternalReaderFailedPayment.NonCancelable( errorType = PaymentFlowError.NoNetwork, {} ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 198fd9890639..58236a40dc3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -96,7 +96,7 @@ sentry = '5.12.2' squareup-javapoet = "1.13.0" squareup-leakcanary = '2.14' squareup-okhttp3 = "5.2.3" -stripe-terminal = '4.7.5' +stripe-terminal = '5.1.0' swiperefreshlayout = "1.1.0" tinder-statemachine = '0.2.0' volley = "1.2.1" @@ -261,6 +261,7 @@ squareup-okhttp3-tls = { module = "com.squareup.okhttp3:okhttp-tls", version.ref squareup-okhttp3-urlconnection = { module = "com.squareup.okhttp3:okhttp-urlconnection", version.ref = "squareup-okhttp3" } stripe-terminal-taptopay = { group = "com.stripe", name = "stripeterminal-taptopay", version.ref = "stripe-terminal" } stripe-terminal-core = { group = "com.stripe", name = "stripeterminal-core", version.ref = "stripe-terminal" } +stripe-terminal-ktx = { group = "com.stripe", name = "stripeterminal-ktx", version.ref = "stripe-terminal" } tinder-statemachine = { group = "com.tinder.statemachine", name = "statemachine", version.ref = "tinder-statemachine" } volley = { module = "com.android.volley:volley", version.ref = "volley" } wellsql = { module = "org.wordpress:wellsql", version.ref = "wellsql" } diff --git a/libs/cardreader/build.gradle b/libs/cardreader/build.gradle index f491cc951f80..8866488df4b6 100644 --- a/libs/cardreader/build.gradle +++ b/libs/cardreader/build.gradle @@ -30,6 +30,7 @@ android { dependencies { runtimeOnly(libs.stripe.terminal.taptopay) implementation(libs.stripe.terminal.core) + implementation(libs.stripe.terminal.ktx) // Coroutines implementation(libs.kotlinx.coroutines.core) diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManager.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManager.kt index b78dac62d069..9e2bbe9a0f45 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManager.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManager.kt @@ -45,8 +45,9 @@ interface CardReaderManager { fun setupTapToPayUx(config: TapToPayUxConfig) - fun startConnectionToReader(cardReader: CardReader, locationId: String) + suspend fun startConnectionToReader(cardReader: CardReader, locationId: String) suspend fun disconnectReader(): Boolean + fun cancelReconnection() suspend fun collectPayment(paymentInfo: PaymentInfo): Flow suspend fun refundInteracPayment( diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManagerFactory.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManagerFactory.kt index 3ebd9a418695..79012f818ad7 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManagerFactory.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/CardReaderManagerFactory.kt @@ -18,11 +18,9 @@ import com.woocommerce.android.cardreader.internal.payments.PaymentManager import com.woocommerce.android.cardreader.internal.payments.PaymentUtils import com.woocommerce.android.cardreader.internal.payments.RefundErrorMapper import com.woocommerce.android.cardreader.internal.payments.actions.CancelPaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.CollectInteracRefundAction -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessInteracRefundAction -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessRefundAction import com.woocommerce.android.cardreader.internal.wrappers.PaymentIntentParametersFactory import com.woocommerce.android.cardreader.internal.wrappers.PaymentMethodTypeMapper import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper @@ -42,7 +40,7 @@ object CardReaderManagerFactory { UpdateErrorMapper(batteryLevelProvider), terminalListener ) - val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper) + val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper, terminalListener) val cardReaderConfigFactory = CardReaderConfigFactory() val paymentUtils = PaymentUtils(logWrapper) @@ -61,16 +59,14 @@ object CardReaderManagerFactory { cardReaderConfigFactory, paymentUtils, ), - CollectPaymentAction(terminal, logWrapper), - ProcessPaymentAction(terminal, logWrapper), + ProcessPaymentIntentAction(terminal, logWrapper), CancelPaymentAction(terminal), paymentUtils, PaymentErrorMapper(), cardReaderConfigFactory ), InteracRefundManager( - CollectInteracRefundAction(terminal), - ProcessInteracRefundAction(terminal), + ProcessRefundAction(terminal), RefundErrorMapper(), paymentUtils, ), @@ -81,6 +77,7 @@ object CardReaderManagerFactory { DiscoverReadersAction(terminal, logWrapper), terminalListener, application, + logWrapper, ), SoftwareUpdateManager( terminal, diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/connection/CardReaderStatus.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/connection/CardReaderStatus.kt index e1edf87595e8..8edaee24b334 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/connection/CardReaderStatus.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/connection/CardReaderStatus.kt @@ -13,4 +13,5 @@ sealed class CardReaderStatus { } data class Connected(val cardReader: CardReader) : CardReaderStatus() data object Connecting : CardReaderStatus() + data object Reconnecting : CardReaderStatus() } diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImpl.kt index 6b7bfe9efb5d..291c7ea6323b 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImpl.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImpl.kt @@ -92,7 +92,7 @@ internal class CardReaderManagerImpl( connectionManager.setupTapToPayUx(config) } - override fun startConnectionToReader(cardReader: CardReader, locationId: String) { + override suspend fun startConnectionToReader(cardReader: CardReader, locationId: String) { if (!terminal.isInitialized()) error("Terminal not initialized") connectionManager.startConnectionToReader(cardReader, locationId) } @@ -103,6 +103,11 @@ internal class CardReaderManagerImpl( return connectionManager.disconnectReader() } + override fun cancelReconnection() { + if (!terminal.isInitialized()) error("Terminal not initialized") + connectionManager.cancelReconnection() + } + override suspend fun collectPayment(paymentInfo: PaymentInfo): Flow { resetBluetoothDisplayMessage() return paymentManager.acceptPayment(paymentInfo) diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/FlowExtensions.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/FlowExtensions.kt deleted file mode 100644 index 5bf32c2bd7ea..000000000000 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/FlowExtensions.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.woocommerce.android.cardreader.internal - -import com.woocommerce.android.cardreader.LogWrapper -import kotlinx.coroutines.channels.ProducerScope -import kotlinx.coroutines.channels.onClosed -import kotlinx.coroutines.channels.onFailure -import kotlinx.coroutines.channels.trySendBlocking - -internal fun ProducerScope.sendAndLog(status: T, logWrapper: LogWrapper) { - trySendBlocking(status) - .onClosed { logWrapper.e(LOG_TAG, it?.message.orEmpty()) } - .onFailure { logWrapper.e(LOG_TAG, it?.message.orEmpty()) } -} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt index 87f5c67edafb..a0ce1ba2d68f 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt @@ -1,15 +1,18 @@ package com.woocommerce.android.cardreader.internal.connection +import com.stripe.stripeterminal.external.callable.Callback import com.stripe.stripeterminal.external.callable.Cancelable import com.stripe.stripeterminal.external.callable.MobileReaderListener import com.stripe.stripeterminal.external.models.BatteryStatus import com.stripe.stripeterminal.external.models.DisconnectReason +import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.ReaderDisplayMessage import com.stripe.stripeterminal.external.models.ReaderEvent import com.stripe.stripeterminal.external.models.ReaderInputOptions import com.stripe.stripeterminal.external.models.ReaderSoftwareUpdate import com.stripe.stripeterminal.external.models.TerminalException import com.woocommerce.android.cardreader.LogWrapper +import com.woocommerce.android.cardreader.connection.CardReaderImpl import com.woocommerce.android.cardreader.connection.CardReaderStatus import com.woocommerce.android.cardreader.connection.event.BluetoothCardReaderMessages import com.woocommerce.android.cardreader.connection.event.BluetoothCardReaderMessages.CardReaderNoMessage @@ -45,6 +48,7 @@ internal class BluetoothReaderListenerImpl( val batteryStatusEvents = _batteryStatusEvents.asStateFlow() var cancelUpdateAction: Cancelable? = null + var cancelReconnectAction: Cancelable? = null override fun onFinishInstallingUpdate(update: ReaderSoftwareUpdate?, e: TerminalException?) { logWrapper.d(LOG_TAG, "onFinishInstallingUpdate: $update $e") @@ -117,6 +121,28 @@ internal class BluetoothReaderListenerImpl( terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected(errorCode = errorCode)) } + override fun onReaderReconnectFailed(reader: Reader) { + logWrapper.d(LOG_TAG, "onReaderReconnectFailed") + cancelReconnectAction = null + terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) + } + + override fun onReaderReconnectStarted( + reader: Reader, + cancelReconnect: Cancelable, + reason: DisconnectReason + ) { + logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") + cancelReconnectAction = cancelReconnect + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) + } + + override fun onReaderReconnectSucceeded(reader: Reader) { + logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") + cancelReconnectAction = null + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) + } + fun resetConnectionState() { _updateStatusEvents.value = SoftwareUpdateStatus.Unknown _updateAvailabilityEvents.value = SoftwareUpdateAvailability.NotAvailable @@ -125,4 +151,9 @@ internal class BluetoothReaderListenerImpl( fun resetDisplayMessage() { _displayMessagesEvents.value = CardReaderNoMessage } + + fun cancelReconnection(callback: Callback) { + cancelReconnectAction?.cancel(callback) + cancelReconnectAction = null + } } diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManager.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManager.kt index b3a82d2cc3f8..cba18b202d09 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManager.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManager.kt @@ -5,7 +5,6 @@ import android.app.Application import android.content.pm.PackageManager import android.os.Build import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.ReaderCallback import com.stripe.stripeterminal.external.models.ConnectionConfiguration.BluetoothConnectionConfiguration import com.stripe.stripeterminal.external.models.ConnectionConfiguration.TapToPayConnectionConfiguration import com.stripe.stripeterminal.external.models.DeviceType @@ -13,6 +12,7 @@ import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.TerminalErrorCode import com.stripe.stripeterminal.external.models.TerminalException import com.woocommerce.android.cardreader.CardReaderManager +import com.woocommerce.android.cardreader.LogWrapper import com.woocommerce.android.cardreader.connection.CardReader import com.woocommerce.android.cardreader.connection.CardReaderDiscoveryEvents import com.woocommerce.android.cardreader.connection.CardReaderImpl @@ -32,11 +32,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine private const val ARTIFICIAL_STATUS_UPDATE_DELAY_IN_MILLIS = 500L +private const val LOG_TAG = "ConnectionManager" +@Suppress("LongParameterList") internal class ConnectionManager( private val terminal: TerminalWrapper, private val bluetoothReaderListener: BluetoothReaderListenerImpl, @@ -44,6 +44,7 @@ internal class ConnectionManager( private val discoverReadersAction: DiscoverReadersAction, private val terminalListenerImpl: TerminalListenerImpl, private val application: Application, + private val logWrapper: LogWrapper, ) { val softwareUpdateStatus = bluetoothReaderListener.updateStatusEvents val softwareUpdateAvailability = bluetoothReaderListener.updateAvailabilityEvents @@ -124,43 +125,50 @@ internal class ConnectionManager( } } - fun startConnectionToReader(cardReader: CardReader, locationId: String) { + suspend fun startConnectionToReader(cardReader: CardReader, locationId: String) { (cardReader as CardReaderImpl).let { updateReaderStatus(CardReaderStatus.Connecting) - val readerCallback = object : ReaderCallback { - override fun onSuccess(reader: Reader) { - updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) + try { + val reader = when (it.cardReader.deviceType) { + DeviceType.TAP_TO_PAY_DEVICE -> connectToBuiltInReader(cardReader, locationId) + else -> connectToExternalReader(cardReader, locationId) } - - override fun onFailure(e: TerminalException) { - updateReaderStatus( - CardReaderStatus.NotConnected( - errorCode = e.errorCode.toErrorCode(), - errorMessage = e.errorMessage, - ) + updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) + } catch (e: TerminalException) { + updateReaderStatus( + CardReaderStatus.NotConnected( + errorCode = e.errorCode.toErrorCode(), + errorMessage = e.errorMessage, ) - } + ) } + } + } - when (it.cardReader.deviceType) { - DeviceType.TAP_TO_PAY_DEVICE -> connectToBuiltInReader(cardReader, locationId, readerCallback) - else -> connectToExternalReader(cardReader, locationId, readerCallback) - } + suspend fun disconnectReader(): Boolean { + return try { + terminal.disconnectReader() + updateReaderStatus(CardReaderStatus.NotConnected()) + true + } catch (e: TerminalException) { + logWrapper.e(LOG_TAG, "Failed to disconnect reader: ${e.errorMessage}") + updateReaderStatus(CardReaderStatus.NotConnected()) + false } } - suspend fun disconnectReader() = suspendCoroutine { continuation -> - terminal.disconnectReader(object : Callback { + fun cancelReconnection() { + val callback = object : Callback { override fun onFailure(e: TerminalException) { updateReaderStatus(CardReaderStatus.NotConnected()) - continuation.resume(false) } override fun onSuccess() { updateReaderStatus(CardReaderStatus.NotConnected()) - continuation.resume(true) } - }) + } + bluetoothReaderListener.cancelReconnection(callback) + tapToPayReaderListener.cancelReconnection(callback) } private fun startStateResettingJobIfNeeded(currentStatus: CardReaderStatus) { @@ -199,31 +207,27 @@ internal class ConnectionManager( terminal.setupTapToPayUx(config) } - private fun connectToExternalReader( + private suspend fun connectToExternalReader( cardReader: CardReaderImpl, - locationId: String, - readerCallback: ReaderCallback - ) { - terminal.connectToReader( + locationId: String + ): Reader { + return terminal.connectToReader( cardReader.cardReader, - BluetoothConnectionConfiguration(locationId, true, bluetoothReaderListener), - readerCallback + BluetoothConnectionConfiguration(locationId, true, bluetoothReaderListener) ) } - private fun connectToBuiltInReader( + private suspend fun connectToBuiltInReader( cardReader: CardReaderImpl, - locationId: String, - readerCallback: ReaderCallback - ) { - terminal.connectToMobile( + locationId: String + ): Reader { + return terminal.connectToMobile( cardReader.cardReader, TapToPayConnectionConfiguration( locationId, autoReconnectOnUnexpectedDisconnect = true, tapToPayReaderListener - ), - readerCallback + ) ) } diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/TapToPayReaderListenerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/TapToPayReaderListenerImpl.kt index b5d08f4d6137..2c3c059088fa 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/TapToPayReaderListenerImpl.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/TapToPayReaderListenerImpl.kt @@ -1,21 +1,30 @@ package com.woocommerce.android.cardreader.internal.connection +import com.stripe.stripeterminal.external.callable.Callback import com.stripe.stripeterminal.external.callable.Cancelable import com.stripe.stripeterminal.external.callable.TapToPayReaderListener import com.stripe.stripeterminal.external.models.DisconnectReason import com.stripe.stripeterminal.external.models.Reader import com.woocommerce.android.cardreader.LogWrapper +import com.woocommerce.android.cardreader.connection.CardReaderImpl +import com.woocommerce.android.cardreader.connection.CardReaderStatus import com.woocommerce.android.cardreader.internal.LOG_TAG -class TapToPayReaderListenerImpl( - private val logWrapper: LogWrapper +internal class TapToPayReaderListenerImpl( + private val logWrapper: LogWrapper, + private val terminalListenerImpl: TerminalListenerImpl ) : TapToPayReaderListener { + var cancelReconnectAction: Cancelable? = null + override fun onDisconnect(reason: DisconnectReason) { - logWrapper.d(LOG_TAG, "onDisconnect") + logWrapper.d(LOG_TAG, "onDisconnect: reason=$reason") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } override fun onReaderReconnectFailed(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectFailed") + cancelReconnectAction = null + terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } override fun onReaderReconnectStarted( @@ -23,10 +32,19 @@ class TapToPayReaderListenerImpl( cancelReconnect: Cancelable, reason: DisconnectReason ) { - logWrapper.d(LOG_TAG, "onReaderReconnectStarted") + logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") + cancelReconnectAction = cancelReconnect + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) } override fun onReaderReconnectSucceeded(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") + cancelReconnectAction = null + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) + } + + fun cancelReconnection(callback: Callback) { + cancelReconnectAction?.cancel(callback) + cancelReconnectAction = null } } diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersAction.kt index 6ba25cada2d3..5519a258c815 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersAction.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersAction.kt @@ -1,21 +1,24 @@ package com.woocommerce.android.cardreader.internal.connection.actions import androidx.annotation.RequiresPermission -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.DiscoveryListener import com.stripe.stripeterminal.external.models.DiscoveryConfiguration import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.TerminalException import com.woocommerce.android.cardreader.LogWrapper +import com.woocommerce.android.cardreader.internal.LOG_TAG import com.woocommerce.android.cardreader.internal.connection.actions.DiscoverReadersAction.DiscoverReadersStatus.Failure import com.woocommerce.android.cardreader.internal.connection.actions.DiscoverReadersAction.DiscoverReadersStatus.FoundReaders import com.woocommerce.android.cardreader.internal.connection.actions.DiscoverReadersAction.DiscoverReadersStatus.Started import com.woocommerce.android.cardreader.internal.connection.actions.DiscoverReadersAction.DiscoverReadersStatus.Success -import com.woocommerce.android.cardreader.internal.sendAndLog import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart private const val DISCOVERY_TIMEOUT_IN_SECONDS = 60 @@ -55,40 +58,22 @@ internal class DiscoverReadersAction( ], ) private fun discoverReaders(config: DiscoveryConfiguration): Flow { - return callbackFlow { - sendAndLog(Started, logWrapper) - var foundReaders: List? = null - val cancelable = terminal.discoverReaders( - config, - object : DiscoveryListener { - override fun onUpdateDiscoveredReaders(readers: List) { - if (readers != foundReaders) { - foundReaders = readers - this@callbackFlow.sendAndLog(FoundReaders(readers), logWrapper) - } - } - }, - object : Callback { - override fun onFailure(e: TerminalException) { - this@callbackFlow.sendAndLog(Failure(e), logWrapper) - this@callbackFlow.close() - } - - override fun onSuccess() { - this@callbackFlow.sendAndLog(Success, logWrapper) - this@callbackFlow.close() - } + return terminal.discoverReaders(config) + .distinctUntilChanged() + .map, DiscoverReadersStatus> { readers -> FoundReaders(readers) } + .onStart { emit(Started) } + .onCompletion { cause -> + if (cause == null || cause is CancellationException) { + emit(Success) + } + } + .catch { e -> + if (e is TerminalException) { + emit(Failure(e)) + } else { + throw e } - ) - awaitClose { - cancelable.takeIf { !it.isCompleted }?.cancel(noopCallback) } - } + .onEach { logWrapper.d(LOG_TAG, it.toString()) } } } - -private val noopCallback = object : Callback { - override fun onFailure(e: TerminalException) {} - - override fun onSuccess() {} -} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManager.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManager.kt index 516657246476..8a9141713122 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManager.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManager.kt @@ -1,19 +1,16 @@ package com.woocommerce.android.cardreader.internal.payments -import com.woocommerce.android.cardreader.internal.payments.actions.CollectInteracRefundAction -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessInteracRefundAction +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessRefundAction import com.woocommerce.android.cardreader.payments.CardInteracRefundStatus import com.woocommerce.android.cardreader.payments.RefundConfig import com.woocommerce.android.cardreader.payments.RefundParams import com.woocommerce.android.cardreader.payments.toStripeRefundConfiguration import com.woocommerce.android.cardreader.payments.toStripeRefundParameters import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow internal class InteracRefundManager( - private val collectInteracRefundAction: CollectInteracRefundAction, - private val processInteracRefundAction: ProcessInteracRefundAction, + private val processRefundAction: ProcessRefundAction, private val refundErrorMapper: RefundErrorMapper, private val paymentsUtils: PaymentUtils, ) { @@ -21,39 +18,17 @@ internal class InteracRefundManager( refundParameters: RefundParams, refundConfig: RefundConfig, ): Flow = flow { - collectInteracRefund(refundParameters, refundConfig) - } - - private suspend fun FlowCollector.collectInteracRefund( - refundParameters: RefundParams, - refundConfig: RefundConfig, - ) { emit(CardInteracRefundStatus.CollectingInteracRefund) - collectInteracRefundAction.collectRefund( + val status = processRefundAction.processRefund( refundParameters.toStripeRefundParameters(paymentsUtils), refundConfig.toStripeRefundConfiguration() - ).collect { refundStatus -> - when (refundStatus) { - CollectInteracRefundAction.CollectInteracRefundStatus.Success -> { - processInteracRefund(refundParameters) - } - is CollectInteracRefundAction.CollectInteracRefundStatus.Failure -> { - emit(refundErrorMapper.mapTerminalError(refundParameters, refundStatus.exception)) - } + ) + when (status) { + is ProcessRefundAction.ProcessRefundStatus.Success -> { + emit(CardInteracRefundStatus.InteracRefundSuccess) } - } - } - - private suspend fun FlowCollector.processInteracRefund(refundParameters: RefundParams) { - emit(CardInteracRefundStatus.ProcessingInteracRefund) - processInteracRefundAction.processRefund().collect { status -> - when (status) { - is ProcessInteracRefundAction.ProcessRefundStatus.Success -> { - emit(CardInteracRefundStatus.InteracRefundSuccess) - } - is ProcessInteracRefundAction.ProcessRefundStatus.Failure -> { - emit(refundErrorMapper.mapTerminalError(refundParameters, status.exception)) - } + is ProcessRefundAction.ProcessRefundStatus.Failure -> { + emit(refundErrorMapper.mapTerminalError(refundParameters, status.exception)) } } } diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/PaymentManager.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/PaymentManager.kt index f0666611b7fc..8501482aa7bb 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/PaymentManager.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/PaymentManager.kt @@ -8,17 +8,21 @@ import com.woocommerce.android.cardreader.CardReaderStore.CapturePaymentResponse import com.woocommerce.android.cardreader.config.CardReaderConfigFactory import com.woocommerce.android.cardreader.config.CardReaderConfigForSupportedCountry import com.woocommerce.android.cardreader.internal.payments.actions.CancelPaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction.CollectPaymentStatus import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction.CreatePaymentStatus.Failure import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction.CreatePaymentStatus.Success -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction.ProcessPaymentStatus +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction.ProcessPaymentIntentStatus import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper import com.woocommerce.android.cardreader.payments.CardPaymentStatus -import com.woocommerce.android.cardreader.payments.CardPaymentStatus.* +import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CapturingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType.Generic +import com.woocommerce.android.cardreader.payments.CardPaymentStatus.InitializingPayment +import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentCompleted +import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentFailed +import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentMethodType +import com.woocommerce.android.cardreader.payments.CardPaymentStatus.ProcessingPayment +import com.woocommerce.android.cardreader.payments.CardPaymentStatus.ProcessingPaymentCompleted import com.woocommerce.android.cardreader.payments.PaymentData import com.woocommerce.android.cardreader.payments.PaymentInfo import kotlinx.coroutines.flow.Flow @@ -30,8 +34,7 @@ internal class PaymentManager( private val terminalWrapper: TerminalWrapper, private val cardReaderStore: CardReaderStore, private val createPaymentAction: CreatePaymentAction, - private val collectPaymentAction: CollectPaymentAction, - private val processPaymentAction: ProcessPaymentAction, + private val processPaymentIntentAction: ProcessPaymentIntentAction, private val cancelPaymentAction: CancelPaymentAction, private val paymentUtils: PaymentUtils, private val errorMapper: PaymentErrorMapper, @@ -68,10 +71,9 @@ internal class PaymentManager( return@flow } - if (paymentIntent.status == PaymentIntentStatus.REQUIRES_PAYMENT_METHOD) { - paymentIntent = collectPayment(paymentIntent) - } - if (paymentIntent.status == PaymentIntentStatus.REQUIRES_CONFIRMATION) { + if (paymentIntent.status == PaymentIntentStatus.REQUIRES_PAYMENT_METHOD || + paymentIntent.status == PaymentIntentStatus.REQUIRES_CONFIRMATION + ) { paymentIntent = processPayment(paymentIntent) } @@ -114,47 +116,31 @@ internal class PaymentManager( } private suspend fun FlowCollector.createPaymentIntent(paymentInfo: PaymentInfo): PaymentIntent? { - var paymentIntent: PaymentIntent? = null emit(InitializingPayment) - createPaymentAction.createPaymentIntent(paymentInfo).collect { - when (it) { - is Failure -> emit(errorMapper.mapTerminalError(paymentIntent, it.exception)) - is Success -> paymentIntent = it.paymentIntent + return when (val result = createPaymentAction.createPaymentIntent(paymentInfo)) { + is Failure -> { + emit(errorMapper.mapTerminalError(null, result.exception)) + null } + is Success -> result.paymentIntent } - return paymentIntent - } - - private suspend fun FlowCollector.collectPayment( - paymentIntent: PaymentIntent - ): PaymentIntent { - var result = paymentIntent - emit(CollectingPayment) - collectPaymentAction.collectPayment(paymentIntent).collect { - when (it) { - is CollectPaymentStatus.Failure -> emit(errorMapper.mapTerminalError(paymentIntent, it.exception)) - is CollectPaymentStatus.Success -> result = it.paymentIntent - } - } - return result } private suspend fun FlowCollector.processPayment( paymentIntent: PaymentIntent ): PaymentIntent { - var result = paymentIntent emit(ProcessingPayment) - processPaymentAction.processPayment(paymentIntent).collect { - when (it) { - is ProcessPaymentStatus.Failure -> emit(errorMapper.mapTerminalError(paymentIntent, it.exception)) - is ProcessPaymentStatus.Success -> { - val paymentMethodType = determinePaymentMethodType(it) - emit(ProcessingPaymentCompleted(paymentMethodType)) - result = it.paymentIntent - } + return when (val result = processPaymentIntentAction.processPaymentIntent(paymentIntent)) { + is ProcessPaymentIntentStatus.Failure -> { + emit(errorMapper.mapTerminalError(paymentIntent, result.exception)) + paymentIntent + } + is ProcessPaymentIntentStatus.Success -> { + val paymentMethodType = determinePaymentMethodType(result) + emit(ProcessingPaymentCompleted(paymentMethodType)) + result.paymentIntent } } - return result } // Stripe new SDk now has paymentIntent.id as nullable to support offline payment. But we don't support offline yet @@ -188,7 +174,7 @@ internal class PaymentManager( } } - private fun determinePaymentMethodType(status: ProcessPaymentStatus.Success): PaymentMethodType { + private fun determinePaymentMethodType(status: ProcessPaymentIntentStatus.Success): PaymentMethodType { val charge = status.paymentIntent.getCharges().firstOrNull() return when { charge?.paymentMethodDetails?.interacPresentDetails != null -> PaymentMethodType.INTERAC_PRESENT diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CancelPaymentAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CancelPaymentAction.kt index 19b894c41f2a..f43999527785 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CancelPaymentAction.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CancelPaymentAction.kt @@ -1,8 +1,6 @@ package com.woocommerce.android.cardreader.internal.payments.actions -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback import com.stripe.stripeterminal.external.models.PaymentIntent -import com.stripe.stripeterminal.external.models.TerminalException import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -13,17 +11,7 @@ internal class CancelPaymentAction(private val terminal: TerminalWrapper) { fun cancelPayment(paymentIntent: PaymentIntent) { // Usage of GlobalScope is intentional since the app should always try to cancel the payment intent GlobalScope.launch { - terminal.cancelPayment(paymentIntent, noopCallback) + runCatching { terminal.cancelPayment(paymentIntent) } } } } - -private val noopCallback = object : PaymentIntentCallback { - override fun onFailure(e: TerminalException) { - // noop - } - - override fun onSuccess(paymentIntent: PaymentIntent) { - // noop - } -} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundAction.kt deleted file mode 100644 index d79d2c4149e2..000000000000 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundAction.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.models.RefundConfiguration -import com.stripe.stripeterminal.external.models.RefundParameters -import com.stripe.stripeterminal.external.models.TerminalException -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -internal class CollectInteracRefundAction(private val terminal: TerminalWrapper) { - sealed class CollectInteracRefundStatus { - object Success : CollectInteracRefundStatus() - data class Failure(val exception: TerminalException) : CollectInteracRefundStatus() - } - - fun collectRefund( - refundParameters: RefundParameters, - refundConfiguration: RefundConfiguration - ): Flow { - return callbackFlow { - val cancelable = terminal.refundPayment( - refundParameters, - refundConfiguration, - object : Callback { - override fun onSuccess() { - trySend(CollectInteracRefundStatus.Success) - close() - } - - override fun onFailure(e: TerminalException) { - trySend(CollectInteracRefundStatus.Failure(e)) - close() - } - } - ) - awaitClose { - if (!cancelable.isCompleted) cancelable.cancel(noop) - } - } - } -} - -private val noop = object : Callback { - override fun onFailure(e: TerminalException) { - // noop - } - - override fun onSuccess() { - // noop - } -} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentAction.kt deleted file mode 100644 index 40470068ccbb..000000000000 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentAction.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback -import com.stripe.stripeterminal.external.models.PaymentIntent -import com.stripe.stripeterminal.external.models.TerminalException -import com.woocommerce.android.cardreader.LogWrapper -import com.woocommerce.android.cardreader.internal.LOG_TAG -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction.CollectPaymentStatus.Failure -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction.CollectPaymentStatus.Success -import com.woocommerce.android.cardreader.internal.sendAndLog -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -internal class CollectPaymentAction(private val terminal: TerminalWrapper, private val logWrapper: LogWrapper) { - sealed class CollectPaymentStatus { - data class Success(val paymentIntent: PaymentIntent) : CollectPaymentStatus() - data class Failure(val exception: TerminalException) : CollectPaymentStatus() - } - - fun collectPayment(paymentIntent: PaymentIntent): Flow { - return callbackFlow { - val cancelable = terminal.collectPaymentMethod( - paymentIntent, - object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - logWrapper.d(LOG_TAG, "Payment collected") - this@callbackFlow.sendAndLog(Success(paymentIntent), logWrapper) - this@callbackFlow.close() - } - - override fun onFailure(e: TerminalException) { - logWrapper.d(LOG_TAG, "Payment collection failed") - this@callbackFlow.sendAndLog(Failure(e), logWrapper) - this@callbackFlow.close() - } - } - ) - awaitClose { - if (!cancelable.isCompleted) cancelable.cancel(noop) - } - } - } -} - -private val noop = object : Callback { - override fun onFailure(e: TerminalException) { - // noop - } - - override fun onSuccess() { - // noop - } -} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentAction.kt index 1f144cb3bc95..f75f5b25af31 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentAction.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentAction.kt @@ -1,6 +1,5 @@ package com.woocommerce.android.cardreader.internal.payments.actions -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback import com.stripe.stripeterminal.external.models.PaymentIntent import com.stripe.stripeterminal.external.models.PaymentIntentParameters import com.stripe.stripeterminal.external.models.TerminalException @@ -12,13 +11,9 @@ import com.woocommerce.android.cardreader.internal.payments.MetaDataKeys import com.woocommerce.android.cardreader.internal.payments.PaymentUtils import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction.CreatePaymentStatus.Failure import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction.CreatePaymentStatus.Success -import com.woocommerce.android.cardreader.internal.sendAndLog import com.woocommerce.android.cardreader.internal.wrappers.PaymentIntentParametersFactory import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper import com.woocommerce.android.cardreader.payments.PaymentInfo -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow internal class CreatePaymentAction( private val paymentIntentParametersFactory: PaymentIntentParametersFactory, @@ -32,25 +27,14 @@ internal class CreatePaymentAction( data class Failure(val exception: TerminalException) : CreatePaymentStatus() } - fun createPaymentIntent(paymentInfo: PaymentInfo): Flow { - return callbackFlow { - terminal.createPaymentIntent( - createParams(paymentInfo), - object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - logWrapper.d(LOG_TAG, "Creating payment intent succeeded") - this@callbackFlow.sendAndLog(Success(paymentIntent), logWrapper) - this@callbackFlow.close() - } - - override fun onFailure(e: TerminalException) { - logWrapper.d(LOG_TAG, "Creating payment intent failed") - this@callbackFlow.sendAndLog(Failure(e), logWrapper) - this@callbackFlow.close() - } - } - ) - awaitClose() + suspend fun createPaymentIntent(paymentInfo: PaymentInfo): CreatePaymentStatus { + return try { + val paymentIntent = terminal.createPaymentIntent(createParams(paymentInfo)) + logWrapper.d(LOG_TAG, "Creating payment intent succeeded") + Success(paymentIntent) + } catch (e: TerminalException) { + logWrapper.d(LOG_TAG, "Creating payment intent failed") + Failure(e) } } diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundAction.kt deleted file mode 100644 index e05f99b4ca0d..000000000000 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundAction.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.RefundCallback -import com.stripe.stripeterminal.external.models.Refund -import com.stripe.stripeterminal.external.models.TerminalException -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -internal class ProcessInteracRefundAction(private val terminal: TerminalWrapper) { - sealed class ProcessRefundStatus { - data class Success(val refund: Refund) : ProcessRefundStatus() - data class Failure(val exception: TerminalException) : ProcessRefundStatus() - } - - fun processRefund(): Flow { - return callbackFlow { - terminal.processRefund(object : RefundCallback { - override fun onSuccess(refund: Refund) { - trySend(ProcessRefundStatus.Success(refund)) - close() - } - - override fun onFailure(e: TerminalException) { - trySend(ProcessRefundStatus.Failure(e)) - close() - } - }) - awaitClose() - } - } -} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentAction.kt deleted file mode 100644 index 4bbcb5e34052..000000000000 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentAction.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback -import com.stripe.stripeterminal.external.models.PaymentIntent -import com.stripe.stripeterminal.external.models.TerminalException -import com.woocommerce.android.cardreader.LogWrapper -import com.woocommerce.android.cardreader.internal.LOG_TAG -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction.ProcessPaymentStatus.Failure -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction.ProcessPaymentStatus.Success -import com.woocommerce.android.cardreader.internal.sendAndLog -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow - -internal class ProcessPaymentAction(private val terminal: TerminalWrapper, private val logWrapper: LogWrapper) { - sealed class ProcessPaymentStatus { - data class Success(val paymentIntent: PaymentIntent) : ProcessPaymentStatus() - data class Failure(val exception: TerminalException) : ProcessPaymentStatus() - } - - fun processPayment(paymentIntent: PaymentIntent): Flow { - return callbackFlow { - val cancelable = terminal.processPayment( - paymentIntent, - object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - logWrapper.d(LOG_TAG, "Processing payment succeeded") - this@callbackFlow.sendAndLog(Success(paymentIntent), logWrapper) - this@callbackFlow.close() - } - - override fun onFailure(e: TerminalException) { - logWrapper.e( - LOG_TAG, - "Processing payment failed. " + - "Message: ${e.errorMessage}, DeclineCode: ${e.apiError?.declineCode}" - ) - this@callbackFlow.sendAndLog(Failure(e), logWrapper) - this@callbackFlow.close() - } - } - ) - awaitClose { - if (!cancelable.isCompleted) cancelable.cancel(noop) - } - } - } -} - -private val noop = object : Callback { - override fun onFailure(e: TerminalException) { - // noop - } - - override fun onSuccess() { - // noop - } -} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentIntentAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentIntentAction.kt new file mode 100644 index 000000000000..3defdad5ee5b --- /dev/null +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentIntentAction.kt @@ -0,0 +1,35 @@ +package com.woocommerce.android.cardreader.internal.payments.actions + +import com.stripe.stripeterminal.external.models.PaymentIntent +import com.stripe.stripeterminal.external.models.TerminalException +import com.woocommerce.android.cardreader.LogWrapper +import com.woocommerce.android.cardreader.internal.LOG_TAG +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction.ProcessPaymentIntentStatus.Failure +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction.ProcessPaymentIntentStatus.Success +import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper + +internal class ProcessPaymentIntentAction( + private val terminal: TerminalWrapper, + private val logWrapper: LogWrapper +) { + sealed class ProcessPaymentIntentStatus { + data class Success(val paymentIntent: PaymentIntent) : ProcessPaymentIntentStatus() + data class Failure(val exception: TerminalException) : ProcessPaymentIntentStatus() + } + + suspend fun processPaymentIntent(paymentIntent: PaymentIntent): ProcessPaymentIntentStatus { + logWrapper.d(LOG_TAG, "Processing payment intent") + return try { + val processedPaymentIntent = terminal.processPaymentIntent(paymentIntent) + logWrapper.d(LOG_TAG, "Processing payment intent succeeded") + Success(processedPaymentIntent) + } catch (e: TerminalException) { + logWrapper.e( + LOG_TAG, + "Processing payment intent failed. " + + "Message: ${e.errorMessage}, DeclineCode: ${e.apiError?.declineCode}" + ) + Failure(e) + } + } +} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundAction.kt new file mode 100644 index 000000000000..2ef4de3e55bb --- /dev/null +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundAction.kt @@ -0,0 +1,26 @@ +package com.woocommerce.android.cardreader.internal.payments.actions + +import com.stripe.stripeterminal.external.models.CollectRefundConfiguration +import com.stripe.stripeterminal.external.models.Refund +import com.stripe.stripeterminal.external.models.RefundParameters +import com.stripe.stripeterminal.external.models.TerminalException +import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper + +internal class ProcessRefundAction(private val terminal: TerminalWrapper) { + sealed class ProcessRefundStatus { + data class Success(val refund: Refund) : ProcessRefundStatus() + data class Failure(val exception: TerminalException) : ProcessRefundStatus() + } + + suspend fun processRefund( + refundParameters: RefundParameters, + refundConfiguration: CollectRefundConfiguration + ): ProcessRefundStatus { + return try { + val refund = terminal.processRefund(refundParameters, refundConfiguration) + ProcessRefundStatus.Success(refund) + } catch (e: TerminalException) { + ProcessRefundStatus.Failure(e) + } + } +} diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/wrappers/TerminalWrapper.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/wrappers/TerminalWrapper.kt index a256cbbdadab..85837aa61c8c 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/wrappers/TerminalWrapper.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/wrappers/TerminalWrapper.kt @@ -3,20 +3,17 @@ package com.woocommerce.android.cardreader.internal.wrappers import android.app.Application import androidx.annotation.RequiresPermission import com.stripe.stripeterminal.Terminal -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.Cancelable import com.stripe.stripeterminal.external.callable.ConnectionTokenProvider -import com.stripe.stripeterminal.external.callable.DiscoveryListener -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback -import com.stripe.stripeterminal.external.callable.ReaderCallback -import com.stripe.stripeterminal.external.callable.RefundCallback import com.stripe.stripeterminal.external.callable.TerminalListener +import com.stripe.stripeterminal.external.models.CollectPaymentIntentConfiguration +import com.stripe.stripeterminal.external.models.CollectRefundConfiguration +import com.stripe.stripeterminal.external.models.ConfirmPaymentIntentConfiguration import com.stripe.stripeterminal.external.models.ConnectionConfiguration import com.stripe.stripeterminal.external.models.DiscoveryConfiguration import com.stripe.stripeterminal.external.models.PaymentIntent import com.stripe.stripeterminal.external.models.PaymentIntentParameters import com.stripe.stripeterminal.external.models.Reader -import com.stripe.stripeterminal.external.models.RefundConfiguration +import com.stripe.stripeterminal.external.models.Refund import com.stripe.stripeterminal.external.models.RefundParameters import com.stripe.stripeterminal.external.models.SimulateReaderUpdate import com.stripe.stripeterminal.external.models.SimulatedCard @@ -24,10 +21,18 @@ import com.stripe.stripeterminal.external.models.SimulatedCardType import com.stripe.stripeterminal.external.models.SimulatorConfiguration import com.stripe.stripeterminal.external.models.TapToPayUxConfiguration import com.stripe.stripeterminal.external.models.TapToPayUxConfiguration.Color +import com.stripe.stripeterminal.ktx.cancelPaymentIntent +import com.stripe.stripeterminal.ktx.connectReader +import com.stripe.stripeterminal.ktx.createPaymentIntent +import com.stripe.stripeterminal.ktx.disconnectReader +import com.stripe.stripeterminal.ktx.discoverReaders +import com.stripe.stripeterminal.ktx.processPaymentIntent +import com.stripe.stripeterminal.ktx.processRefund import com.stripe.stripeterminal.log.LogLevel import com.woocommerce.android.cardreader.CardReaderManager import com.woocommerce.android.cardreader.connection.CardReader import com.woocommerce.android.cardreader.connection.CardReaderImpl +import kotlinx.coroutines.flow.Flow /** * Injectable wrapper for Stripe's Terminal object. @@ -40,7 +45,7 @@ internal class TerminalWrapper { logLevel: LogLevel, tokenProvider: ConnectionTokenProvider, listener: TerminalListener - ) = Terminal.initTerminal(application, logLevel, tokenProvider, listener) + ) = Terminal.init(application, logLevel, tokenProvider, listener, null) @RequiresPermission( anyOf = [ @@ -48,51 +53,42 @@ internal class TerminalWrapper { "android.permission.ACCESS_COARSE_LOCATION" ], ) - fun discoverReaders( - config: DiscoveryConfiguration, - discoveryListener: DiscoveryListener, - callback: Callback - ): Cancelable = Terminal.getInstance().discoverReaders(config, discoveryListener, callback) + fun discoverReaders(config: DiscoveryConfiguration): Flow> = + Terminal.getInstance().discoverReaders(config) - fun connectToReader( + suspend fun connectToReader( reader: Reader, - configuration: ConnectionConfiguration.BluetoothConnectionConfiguration, - callback: ReaderCallback - ) = Terminal.getInstance().connectReader(reader, configuration, callback) + configuration: ConnectionConfiguration.BluetoothConnectionConfiguration + ): Reader = Terminal.getInstance().connectReader(reader, configuration) - fun connectToMobile( + suspend fun connectToMobile( reader: Reader, - configuration: ConnectionConfiguration.TapToPayConnectionConfiguration, - callback: ReaderCallback - ) = Terminal.getInstance().connectReader(reader, configuration, callback) + configuration: ConnectionConfiguration.TapToPayConnectionConfiguration + ): Reader = Terminal.getInstance().connectReader(reader, configuration) - fun disconnectReader(callback: Callback) = - Terminal.getInstance().disconnectReader(callback) + suspend fun disconnectReader() = Terminal.getInstance().disconnectReader() - fun clearCachedCredentials() = Terminal.getInstance().clearCachedCredentials() - - fun createPaymentIntent(params: PaymentIntentParameters, callback: PaymentIntentCallback) = - Terminal.getInstance().createPaymentIntent(params, callback) + fun clearCachedCredentials() { + Terminal.getInstance().clearCachedCredentials() + } - fun collectPaymentMethod( - paymentIntent: PaymentIntent, - callback: PaymentIntentCallback - ): Cancelable = Terminal.getInstance().collectPaymentMethod(paymentIntent, callback) + suspend fun createPaymentIntent(params: PaymentIntentParameters): PaymentIntent = + Terminal.getInstance().createPaymentIntent(params, null) - fun processPayment(paymentIntent: PaymentIntent, callback: PaymentIntentCallback): Cancelable = - Terminal.getInstance().confirmPaymentIntent(paymentIntent, callback) + suspend fun processPaymentIntent(paymentIntent: PaymentIntent): PaymentIntent = + Terminal.getInstance().processPaymentIntent( + paymentIntent, + CollectPaymentIntentConfiguration.Builder().build(), + ConfirmPaymentIntentConfiguration.Builder().build() + ) - fun cancelPayment(paymentIntent: PaymentIntent, callback: PaymentIntentCallback) = - Terminal.getInstance().cancelPaymentIntent(paymentIntent, callback) + suspend fun cancelPayment(paymentIntent: PaymentIntent): PaymentIntent = + Terminal.getInstance().cancelPaymentIntent(paymentIntent) - fun refundPayment( + suspend fun processRefund( refundParameters: RefundParameters, - refundConfiguration: RefundConfiguration, - callback: Callback - ) = Terminal.getInstance().collectRefundPaymentMethod(refundParameters, refundConfiguration, callback) - - fun processRefund(callback: RefundCallback) = - Terminal.getInstance().confirmRefund(callback) + refundConfiguration: CollectRefundConfiguration + ): Refund = Terminal.getInstance().processRefund(refundParameters, refundConfiguration) fun installSoftwareUpdate() = Terminal.getInstance().installAvailableUpdate() @@ -121,12 +117,7 @@ internal class TerminalWrapper { fun setupTapToPayUx(config: CardReaderManager.TapToPayUxConfig) { val uxConfig = TapToPayUxConfiguration.Builder() - .tapZone( - TapToPayUxConfiguration.TapZone.Manual.Builder() - .indicator(TapToPayUxConfiguration.TapZoneIndicator.DEFAULT) - .position(TapToPayUxConfiguration.TapZonePosition.Default) - .build() - ) + .tapZone(TapToPayUxConfiguration.TapZone.Default) .colors( TapToPayUxConfiguration.ColorScheme.Builder() .primary(Color.Resource(config.primaryColor)) diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/CardPaymentStatus.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/CardPaymentStatus.kt index a90c2732b73b..d5516ee62b64 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/CardPaymentStatus.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/CardPaymentStatus.kt @@ -2,7 +2,6 @@ package com.woocommerce.android.cardreader.payments sealed class CardPaymentStatus { object InitializingPayment : CardPaymentStatus() - object CollectingPayment : CardPaymentStatus() object WaitingForInput : CardPaymentStatus() object ProcessingPayment : CardPaymentStatus() data class ProcessingPaymentCompleted(val paymentMethodType: PaymentMethodType) : CardPaymentStatus() diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundConfig.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundConfig.kt index 88aac6741390..d5d3af62e175 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundConfig.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundConfig.kt @@ -1,13 +1,20 @@ package com.woocommerce.android.cardreader.payments -import com.stripe.stripeterminal.external.models.RefundConfiguration +import com.stripe.stripeterminal.external.models.CollectRefundConfiguration +import com.stripe.stripeterminal.external.models.CustomerCancellation data class RefundConfig( val enableCustomerCancellation: Boolean ) -internal fun RefundConfig.toStripeRefundConfiguration(): RefundConfiguration { - return RefundConfiguration.Builder() - .setEnableCustomerCancellation(this.enableCustomerCancellation) +internal fun RefundConfig.toStripeRefundConfiguration(): CollectRefundConfiguration { + return CollectRefundConfiguration.Builder() + .setCustomerCancellation( + if (enableCustomerCancellation) { + CustomerCancellation.ENABLE_IF_AVAILABLE + } else { + CustomerCancellation.DISABLE_IF_AVAILABLE + } + ) .build() } diff --git a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundParams.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundParams.kt index 6c9c6864dd71..2d64e5a3d241 100644 --- a/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundParams.kt +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/payments/RefundParams.kt @@ -1,7 +1,6 @@ package com.woocommerce.android.cardreader.payments import com.stripe.stripeterminal.external.models.RefundParameters -import com.stripe.stripeterminal.external.models.RefundParameters.Id import com.woocommerce.android.cardreader.internal.payments.PaymentUtils import java.math.BigDecimal @@ -12,8 +11,8 @@ data class RefundParams( ) internal fun RefundParams.toStripeRefundParameters(paymentUtils: PaymentUtils): RefundParameters { - return RefundParameters.Builder( - Id.Charge(id = this.chargeId), + return RefundParameters.ByChargeId( + id = this.chargeId, amount = paymentUtils.convertToSmallestCurrencyUnit(this.amount, this.currency), currency = this.currency ).build() diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImplTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImplTest.kt index 3bb2f517099a..946f19ed9f03 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImplTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImplTest.kt @@ -27,6 +27,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.math.BigDecimal +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi class CardReaderManagerImplTest : CardReaderBaseUnitTest() { private lateinit var cardReaderManager: CardReaderManagerImpl diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManagerTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManagerTest.kt index b42df8cd02b0..046744ff3af3 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManagerTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManagerTest.kt @@ -1,12 +1,11 @@ package com.woocommerce.android.cardreader.internal.connection import android.app.Application -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.ReaderCallback import com.stripe.stripeterminal.external.models.DeviceType import com.stripe.stripeterminal.external.models.Reader import com.stripe.stripeterminal.external.models.TerminalErrorCode import com.stripe.stripeterminal.external.models.TerminalException +import com.woocommerce.android.cardreader.LogWrapper import com.woocommerce.android.cardreader.connection.CardReaderDiscoveryEvents import com.woocommerce.android.cardreader.connection.CardReaderDiscoveryEvents.ReadersFound import com.woocommerce.android.cardreader.connection.CardReaderImpl @@ -47,6 +46,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { on { readerStatus }.thenReturn(MutableStateFlow(CardReaderStatus.NotConnected())) } private val application: Application = mock() + private val logWrapper: LogWrapper = mock() private val supportedReaders = CardReaderTypesToDiscover.SpecificReaders.ExternalReaders( @@ -60,7 +60,6 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { val defaultReaderStatus: StateFlow = MutableStateFlow(CardReaderStatus.NotConnected()) whenever(terminalListenerImpl.readerStatus).thenReturn(defaultReaderStatus) - // uses the previously created mock objects connectionManager = ConnectionManager( terminalWrapper, bluetoothReaderListener, @@ -68,6 +67,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { discoverReadersAction, terminalListenerImpl, application, + logWrapper, ) } @@ -246,9 +246,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { val cardReader: CardReaderImpl = mock { on { cardReader }.thenReturn(reader) } - whenever(terminalWrapper.connectToReader(any(), any(), any())).thenAnswer { - (it.arguments[2] as ReaderCallback).onSuccess(mock()) - } + whenever(terminalWrapper.connectToReader(any(), any())).thenReturn(mock()) connectionManager.startConnectionToReader(cardReader, "location_id") @@ -270,9 +268,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { on { errorMessage }.thenReturn(message) on { this.errorCode }.thenReturn(errorCode) } - whenever(terminalWrapper.connectToReader(any(), any(), any())).thenAnswer { - (it.arguments[2] as ReaderCallback).onFailure(exception) - } + whenever(terminalWrapper.connectToReader(any(), any())).thenAnswer { throw exception } connectionManager.startConnectionToReader(cardReader, "location_id") @@ -299,9 +295,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { on { errorMessage }.thenReturn(message) on { this.errorCode }.thenReturn(errorCode) } - whenever(terminalWrapper.connectToReader(any(), any(), any())).thenAnswer { - (it.arguments[2] as ReaderCallback).onFailure(exception) - } + whenever(terminalWrapper.connectToReader(any(), any())).thenAnswer { throw exception } connectionManager.startConnectionToReader(cardReader, "location_id") @@ -322,9 +316,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { val cardReader: CardReaderImpl = mock { on { cardReader }.thenReturn(reader) } - whenever(terminalWrapper.connectToReader(any(), any(), any())).thenAnswer { - (it.arguments[2] as ReaderCallback).onSuccess(cardReader.cardReader) - } + whenever(terminalWrapper.connectToReader(any(), any())).thenReturn(reader) connectionManager.startConnectionToReader(cardReader, "location_id") @@ -336,9 +328,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { @Test fun `when disconnect succeeds, then status updated with not connected`() = testBlocking { - whenever(terminalWrapper.disconnectReader(any())).thenAnswer { - (it.arguments[0] as Callback).onSuccess() - } + whenever(terminalWrapper.disconnectReader()).thenReturn(Unit) connectionManager.disconnectReader() @@ -347,9 +337,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { @Test fun `when disconnect succeeds, then true is returned`() = testBlocking { - whenever(terminalWrapper.disconnectReader(any())).thenAnswer { - (it.arguments[0] as Callback).onSuccess() - } + whenever(terminalWrapper.disconnectReader()).thenReturn(Unit) val result = connectionManager.disconnectReader() @@ -358,9 +346,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { @Test fun `when disconnect fails, then false is returned`() = testBlocking { - whenever(terminalWrapper.disconnectReader(any())).thenAnswer { - (it.arguments[0] as Callback).onFailure(mock()) - } + whenever(terminalWrapper.disconnectReader()).thenAnswer { throw mock() } val result = connectionManager.disconnectReader() @@ -369,9 +355,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { @Test fun `when disconnect fails, then false with not connected`() = testBlocking { - whenever(terminalWrapper.disconnectReader(any())).thenAnswer { - (it.arguments[0] as Callback).onFailure(mock()) - } + whenever(terminalWrapper.disconnectReader()).thenAnswer { throw mock() } connectionManager.disconnectReader() diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersActionTest.kt index 49ad16a592c5..95b935c389ac 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersActionTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/connection/actions/DiscoverReadersActionTest.kt @@ -1,10 +1,8 @@ package com.woocommerce.android.cardreader.internal.connection.actions -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.Cancelable -import com.stripe.stripeterminal.external.callable.DiscoveryListener import com.stripe.stripeterminal.external.models.DiscoveryConfiguration import com.stripe.stripeterminal.external.models.Reader +import com.stripe.stripeterminal.external.models.TerminalException import com.woocommerce.android.cardreader.LogWrapper import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest import com.woocommerce.android.cardreader.internal.connection.actions.DiscoverReadersAction.DiscoverReadersStatus.Failure @@ -16,16 +14,14 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -42,10 +38,6 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when discovery started, then Started is emitted`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - mock() - } - val result = action.discoverExternalReaders(false).first() assertThat(result).isInstanceOf(Started::class.java) @@ -53,10 +45,9 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when nearby readers found, then FoundReaders is emitted`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(mock())) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn( + flow { emit(listOf(mock())) } + ) val event = action.discoverExternalReaders(false) .ignoreStartedEvent().first() @@ -66,12 +57,12 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when new readers found, then FoundReaders is emitted`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(mock())) - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(mock(), mock())) - onSuccess(args = it.arguments) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn( + flow { + emit(listOf(mock())) + emit(listOf(mock(), mock())) + } + ) val events = action.discoverExternalReaders(false) .ignoreStartedEvent().toList() @@ -82,13 +73,13 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when already found readers found, then FoundReaders is NOT emitted`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - val reader = mock() - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(reader)) - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(reader)) - onSuccess(args = it.arguments) - mock() - } + val reader = mock() + whenever(terminal.discoverReaders(any())).thenReturn( + flow { + emit(listOf(reader)) + emit(listOf(reader)) + } + ) val events = action.discoverExternalReaders(false) .ignoreStartedEvent().toList() @@ -99,23 +90,19 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when reader discover succeeds, then Success is emitted`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onSuccess(args = it.arguments) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn(flow { }) - val event = action.discoverExternalReaders(false) - .ignoreStartedEvent().first() + val events = action.discoverExternalReaders(false) + .ignoreStartedEvent().toList() - assertThat(event).isInstanceOf(Success::class.java) + assertThat(events.last()).isInstanceOf(Success::class.java) } @Test fun `when reader discover fails, then Failure is emitted`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onFailure(it.arguments) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn( + flow { throw mock() } + ) val event = action.discoverExternalReaders(false) .ignoreStartedEvent().first() @@ -125,71 +112,35 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when reader discover succeeds, then flow is terminated`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onSuccess(args = it.arguments) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn(flow { }) - val event = action.discoverExternalReaders(false) + val events = action.discoverExternalReaders(false) .ignoreStartedEvent().toList() - assertThat(event.size).isEqualTo(1) + assertThat(events.size).isEqualTo(1) } @Test fun `when reader discover fails, then flow is terminated`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onFailure(it.arguments) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn( + flow { throw mock() } + ) - val event = action.discoverExternalReaders(false) + val events = action.discoverExternalReaders(false) .ignoreStartedEvent().toList() - assertThat(event.size).isEqualTo(1) - } - - @Test - fun `given flow not terminated, when job canceled, then reader discovery gets canceled`() = testBlocking { - val cancelable = mock() - whenever(cancelable.isCompleted).thenReturn(false) - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { cancelable } - val job = launch { - action.discoverExternalReaders(false).collect { } - } - - job.cancel() - joinAll(job) - - verify(cancelable).cancel(any()) - } - - @Test - fun `given flow already terminated, when job canceled, then reader discovery not canceled`() = testBlocking { - val cancelable = mock() - whenever(cancelable.isCompleted).thenReturn(true) - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onSuccess(it.arguments) - cancelable - } - val job = launch { - action.discoverExternalReaders(false).collect { } - } - - job.cancel() - joinAll(job) - - verify(cancelable, never()).cancel(any()) + assertThat(events.size).isEqualTo(1) } @Test fun `given last event is terminal, when discovery external readers, then flow terminates`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(mock())) - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(mock())) - onFailure(it.arguments) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn( + flow { + emit(listOf(mock())) + emit(listOf(mock(), mock())) + throw mock() + } + ) val result = action.discoverExternalReaders(false) .ignoreStartedEvent().toList() @@ -199,16 +150,12 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when discovery external readers, then config keeps bluetooth scan`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { mock() } + whenever(terminal.discoverReaders(any())).thenReturn(flow { }) - action.discoverExternalReaders(false).first() + action.discoverExternalReaders(false).toList() val configCaptor = argumentCaptor() - verify(terminal).discoverReaders( - configCaptor.capture(), - any(), - any() - ) + verify(terminal).discoverReaders(configCaptor.capture()) assertThat(configCaptor.firstValue).isEqualTo( DiscoveryConfiguration.BluetoothDiscoveryConfiguration( 60, @@ -219,16 +166,12 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `when discovery built in readers, then config keeps local mobile`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { mock() } + whenever(terminal.discoverReaders(any())).thenReturn(flow { }) - action.discoverBuildInReaders(true).first() + action.discoverBuildInReaders(true).toList() val configCaptor = argumentCaptor() - verify(terminal).discoverReaders( - configCaptor.capture(), - any(), - any() - ) + verify(terminal).discoverReaders(configCaptor.capture()) assertThat(configCaptor.firstValue).isEqualTo( DiscoveryConfiguration.TapToPayDiscoveryConfiguration( true @@ -238,12 +181,13 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { @Test fun `given last event is terminal, when discovery built in readers, then flow terminates`() = testBlocking { - whenever(terminal.discoverReaders(any(), any(), any())).thenAnswer { - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(mock())) - onUpdateDiscoveredReaders(args = it.arguments, readers = listOf(mock())) - onFailure(it.arguments) - mock() - } + whenever(terminal.discoverReaders(any())).thenReturn( + flow { + emit(listOf(mock())) + emit(listOf(mock(), mock())) + throw mock() + } + ) val result = action.discoverBuildInReaders(false) .ignoreStartedEvent().toList() @@ -251,17 +195,5 @@ class DiscoverReadersActionTest : CardReaderBaseUnitTest() { assertThat(result.size).isEqualTo(3) } - private fun onUpdateDiscoveredReaders(args: Array, readers: List) { - args.filterIsInstance().first().onUpdateDiscoveredReaders(readers) - } - - private fun onSuccess(args: Array) { - args.filterIsInstance().first().onSuccess() - } - - private fun onFailure(args: Array) { - args.filterIsInstance().first().onFailure(mock()) - } - private fun Flow.ignoreStartedEvent(): Flow = filterNot { it is Started } } diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManagerTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManagerTest.kt index 88b4764390bf..ae1be2d9c3fd 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManagerTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManagerTest.kt @@ -1,8 +1,7 @@ package com.woocommerce.android.cardreader.internal.payments import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest -import com.woocommerce.android.cardreader.internal.payments.actions.CollectInteracRefundAction -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessInteracRefundAction +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessRefundAction import com.woocommerce.android.cardreader.payments.CardInteracRefundStatus import com.woocommerce.android.cardreader.payments.CardInteracRefundStatus.RefundStatusErrorType.DeclinedByBackendError import com.woocommerce.android.cardreader.payments.CardInteracRefundStatus.RefundStatusErrorType.Generic @@ -11,7 +10,6 @@ import com.woocommerce.android.cardreader.payments.RefundConfig import com.woocommerce.android.cardreader.payments.RefundParams import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take @@ -24,10 +22,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any -import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock -import org.mockito.kotlin.never -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.math.BigDecimal import kotlin.reflect.KClass @@ -37,27 +32,25 @@ private const val USD_CURRENCY = "USD" private const val DUMMY_CHARGE_ID = "ch_abcdefgh" private const val TIMEOUT = 1000L +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class InteracRefundManagerTest : CardReaderBaseUnitTest() { private lateinit var manager: InteracRefundManager - private val collectInteracRefundAction: CollectInteracRefundAction = mock() - private val processInteracRefundAction: ProcessInteracRefundAction = mock() + private val processRefundAction: ProcessRefundAction = mock() private val refundErrorMapper: RefundErrorMapper = mock() private val paymentsUtils: PaymentUtils = mock() private val expectedInteracRefundSequence = listOf( CardInteracRefundStatus.CollectingInteracRefund::class, - CardInteracRefundStatus.ProcessingInteracRefund::class, CardInteracRefundStatus.InteracRefundSuccess::class ) @Before fun setUp() = testBlocking { manager = InteracRefundManager( - collectInteracRefundAction, - processInteracRefundAction, + processRefundAction, refundErrorMapper, paymentsUtils, ) @@ -72,34 +65,21 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { } @Test - fun `given collect interac refund success, when refund starts, then ProcessingInteracRefund is emitted`() = + fun `given process refund success, when refund starts, then InteracRefundSuccess is emitted`() = testBlocking { - whenever(collectInteracRefundAction.collectRefund(anyOrNull(), any())) - .thenReturn(flow { emit(CollectInteracRefundAction.CollectInteracRefundStatus.Success) }) + whenever(processRefundAction.processRefund(any(), any())) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Success(mock())) val result = manager.refundInteracPayment(createRefundParams(), refundConfig) - .takeUntil(CardInteracRefundStatus.ProcessingInteracRefund::class).toList() + .takeUntil(CardInteracRefundStatus.InteracRefundSuccess::class).toList() - assertThat(result.last()).isInstanceOf(CardInteracRefundStatus.ProcessingInteracRefund::class.java) + assertThat(result.last()).isInstanceOf(CardInteracRefundStatus.InteracRefundSuccess::class.java) } @Test - fun `given collect interac refund failure, when refund starts, then ProcessingInteracRefund is NOT emitted`() = + fun `given process refund failure, when refund starts, then failure is emitted`() = testBlocking { - whenever(collectInteracRefundAction.collectRefund(anyOrNull(), any())) - .thenReturn(flow { emit(CollectInteracRefundAction.CollectInteracRefundStatus.Failure(mock())) }) - whenever(refundErrorMapper.mapTerminalError(any(), any())) - .thenReturn(CardInteracRefundStatus.InteracRefundFailure(Generic, "", mock())) - val result = manager.refundInteracPayment(createRefundParams(), refundConfig).toList() - - assertThat(result.last()).isNotInstanceOf(CardInteracRefundStatus.ProcessingInteracRefund::class.java) - verify(processInteracRefundAction, never()).processRefund() - } - - @Test - fun `given collect interac refund failure, when refund starts, then failure is emitted`() = - testBlocking { - whenever(collectInteracRefundAction.collectRefund(anyOrNull(), any())) - .thenReturn(flow { emit(CollectInteracRefundAction.CollectInteracRefundStatus.Failure(mock())) }) + whenever(processRefundAction.processRefund(any(), any())) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(Generic, "", mock()) @@ -110,11 +90,11 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { } @Test - fun `given collect interac refund failure, when refund starts, then failure message is captured`() = + fun `given process refund failure, when refund starts, then failure message is captured`() = testBlocking { val expectedErrorMessage = "Generic Error" - whenever(collectInteracRefundAction.collectRefund(anyOrNull(), any())) - .thenReturn(flow { emit(CollectInteracRefundAction.CollectInteracRefundStatus.Failure(mock())) }) + whenever(processRefundAction.processRefund(any(), any())) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(Generic, expectedErrorMessage, mock()) @@ -128,11 +108,11 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { } @Test - fun `given collect interac refund failure, when refund starts, then failure type is captured`() = + fun `given process refund failure, when refund starts, then failure type is captured`() = testBlocking { val expectedErrorType = DeclinedByBackendError.Unknown - whenever(collectInteracRefundAction.collectRefund(anyOrNull(), any())) - .thenReturn(flow { emit(CollectInteracRefundAction.CollectInteracRefundStatus.Failure(mock())) }) + whenever(processRefundAction.processRefund(any(), any())) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(expectedErrorType, "Declined", mock()) @@ -146,11 +126,11 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { } @Test - fun `given collect interac refund failure, when refund starts, then refund params is captured`() = + fun `given process refund failure, when refund starts, then refund params is captured`() = testBlocking { val expectedRefundParams = createRefundParams() - whenever(collectInteracRefundAction.collectRefund(anyOrNull(), any())) - .thenReturn(flow { emit(CollectInteracRefundAction.CollectInteracRefundStatus.Failure(mock())) }) + whenever(processRefundAction.processRefund(any(), any())) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure( @@ -168,21 +148,19 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { } @Test - fun `given collect interac refund failure, when refund starts, then flow terminates`() = + fun `given process refund failure, when refund starts, then flow terminates`() = testBlocking { - whenever(collectInteracRefundAction.collectRefund(anyOrNull(), any())) - .thenReturn(flow { emit(CollectInteracRefundAction.CollectInteracRefundStatus.Failure(mock())) }) + whenever(processRefundAction.processRefund(any(), any())) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) val result = withTimeoutOrNull(TIMEOUT) { manager.refundInteracPayment(createRefundParams(), refundConfig).toList() } - assertThat(result).isNotNull // verify the flow did not timeout + assertThat(result).isNotNull } private fun Flow.takeUntil(untilStatus: KClass<*>): Flow = this.take(expectedInteracRefundSequence.indexOf(untilStatus) + 1) - // the below lines are here just as a safeguard to verify that the - // expectedInteracRefundSequence is defined correctly .withIndex() .onEach { if (expectedInteracRefundSequence[it.index] != it.value!!::class) { diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentManagerTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentManagerTest.kt index 134ef3f64e73..2fdba9c429f3 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentManagerTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentManagerTest.kt @@ -16,16 +16,13 @@ import com.woocommerce.android.cardreader.config.CardReaderConfigFactory import com.woocommerce.android.cardreader.config.CardReaderConfigForUSA import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest import com.woocommerce.android.cardreader.internal.payments.actions.CancelPaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction.CollectPaymentStatus import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction.CreatePaymentStatus -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction.ProcessPaymentStatus +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction.ProcessPaymentIntentStatus import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CapturingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CardPaymentStatusErrorType -import com.woocommerce.android.cardreader.payments.CardPaymentStatus.CollectingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.InitializingPayment import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentCompleted import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentFailed @@ -37,7 +34,6 @@ import com.woocommerce.android.cardreader.payments.StatementDescriptor import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.single @@ -69,14 +65,14 @@ private const val DUMMY_CUSTOMER_NAME = "Tester" private const val DUMMY_SITE_URL = "www.test.test/test" private const val DUMMY_STORE_NAME = "Test store" +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi class PaymentManagerTest : CardReaderBaseUnitTest() { private lateinit var manager: PaymentManager private val terminalWrapper: TerminalWrapper = mock() private val cardReaderStore: CardReaderStore = mock() private val createPaymentAction: CreatePaymentAction = mock() - private val collectPaymentAction: CollectPaymentAction = mock() - private val processPaymentAction: ProcessPaymentAction = mock() + private val processPaymentIntentAction: ProcessPaymentIntentAction = mock() private val cancelPaymentAction: CancelPaymentAction = mock() private val paymentErrorMapper: PaymentErrorMapper = mock() private val paymentUtils: PaymentUtils = mock() @@ -84,7 +80,6 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { private val expectedSequence = listOf( InitializingPayment::class, - CollectingPayment::class, ProcessingPayment::class, ProcessingPaymentCompleted::class, CapturingPayment::class, @@ -97,26 +92,22 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { terminalWrapper, cardReaderStore, createPaymentAction, - collectPaymentAction, - processPaymentAction, + processPaymentIntentAction, cancelPaymentAction, paymentUtils, paymentErrorMapper, cardReaderConfigFactory, ) whenever(terminalWrapper.isInitialized()).thenReturn(true) - whenever(createPaymentAction.createPaymentIntent(any())) - .thenReturn( - flow { - emit(CreatePaymentStatus.Success(createPaymentIntent(REQUIRES_PAYMENT_METHOD))) - } - ) - whenever(collectPaymentAction.collectPayment(anyOrNull())) - .thenReturn(flow { emit(CollectPaymentStatus.Success(createPaymentIntent(REQUIRES_CONFIRMATION))) }) + val createPaymentIntentResponse = createPaymentIntent(REQUIRES_PAYMENT_METHOD) + val processPaymentIntentResponse = createPaymentIntent(REQUIRES_CAPTURE) - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Success(createPaymentIntent(REQUIRES_CAPTURE))) }) + whenever(createPaymentAction.createPaymentIntent(any())) + .thenReturn(CreatePaymentStatus.Success(createPaymentIntentResponse)) + + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(processPaymentIntentResponse)) whenever(cardReaderStore.capturePaymentIntent(any(), anyString())) .thenReturn(CapturePaymentResponse.Successful.Success) @@ -132,7 +123,6 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { whenever(paymentUtils.isSupportedCurrency(any(), any())).thenReturn(true) } - // BEGIN - Arguments validation and conversion @Test fun `when currency not supported, then error emitted`() = testBlocking { whenever(paymentUtils.isSupportedCurrency(any(), any())).thenReturn(false) @@ -159,8 +149,6 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { assertThat(result).isInstanceOf(PaymentFailed::class.java) } - // END - Arguments validation and conversion - // BEGIN - Creating Payment intent @Test fun `when creating payment intent starts, then InitializingPayment is emitted`() = testBlocking { val result = manager.acceptPayment(createPaymentInfo()) @@ -172,7 +160,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent fails, then error emitted`() = testBlocking { whenever(createPaymentAction.createPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(CreatePaymentStatus.Failure(mock())) }) + .thenReturn(CreatePaymentStatus.Failure(mock())) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -183,7 +171,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent fails, then mapTerminalError invoked`() = testBlocking { whenever(createPaymentAction.createPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(CreatePaymentStatus.Failure(mock())) }) + .thenReturn(CreatePaymentStatus.Failure(mock())) manager .acceptPayment(createPaymentInfo()).toList() @@ -194,8 +182,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given status not REQUIRES_PAYMENT_METHOD, when creating payment finishes, then flow terminates`() = testBlocking { + val canceledIntent = createPaymentIntent(CANCELED) whenever(createPaymentAction.createPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(CreatePaymentStatus.Success(createPaymentIntent(CANCELED))) }) + .thenReturn(CreatePaymentStatus.Success(canceledIntent)) val result = withTimeoutOrNull(TIMEOUT) { manager @@ -203,60 +192,10 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { .toList() } - assertThat(result).isNotNull // verify the flow did not timeout - verify(collectPaymentAction, never()).collectPayment(anyOrNull()) + assertThat(result).isNotNull + verify(processPaymentIntentAction, never()).processPaymentIntent(anyOrNull()) } - // END - Creating Payment intent - // BEGIN - Collecting Payment - @Test - fun `when collecting payment starts, then CollectingPayment is emitted`() = testBlocking { - val result = manager.acceptPayment(createPaymentInfo()) - .takeUntil(CollectingPayment::class).toList() - - assertThat(result.last()).isInstanceOf(CollectingPayment::class.java) - } - - @Test - fun `when collecting payment fails, then error is emitted`() = testBlocking { - whenever(collectPaymentAction.collectPayment(anyOrNull())) - .thenReturn(flow { emit(CollectPaymentStatus.Failure(mock())) }) - - val result = manager - .acceptPayment(createPaymentInfo()).toList() - - assertThat(result.last()).isInstanceOf(PaymentFailed::class.java) - } - - @Test - fun `when collecting payment intent fails, then mapTerminalError invoked`() = testBlocking { - whenever(collectPaymentAction.collectPayment(anyOrNull())) - .thenReturn(flow { emit(CollectPaymentStatus.Failure(mock())) }) - - manager - .acceptPayment(createPaymentInfo()).toList() - - verify(paymentErrorMapper).mapTerminalError(anyOrNull(), anyOrNull()) - } - - @Test - fun `given status not REQUIRES_CONFIRMATION, when collecting payment finishes, then flow terminates`() = - testBlocking { - whenever(collectPaymentAction.collectPayment(anyOrNull())) - .thenReturn(flow { emit(CollectPaymentStatus.Success(createPaymentIntent(CANCELED))) }) - - val result = withTimeoutOrNull(TIMEOUT) { - manager - .acceptPayment(createPaymentInfo()) - .toList() - } - - assertThat(result).isNotNull // verify the flow did not timeout - verify(processPaymentAction, never()).processPayment(anyOrNull()) - } - - // END - Collecting Payment - // BEGIN - Processing Payment @Test fun `when processing payment starts, then ProcessingPayment is emitted`() = testBlocking { val result = manager.acceptPayment(createPaymentInfo()) @@ -267,8 +206,8 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when processing payment fails, then error emitted`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Failure(mock())) }) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Failure(mock())) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -278,8 +217,8 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when processing payment fails, then mapTerminalError invoked`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Failure(mock())) }) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Failure(mock())) manager .acceptPayment(createPaymentInfo()).toList() @@ -290,8 +229,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given status not REQUIRES_CAPTURE, when processing payment finishes, then flow terminates`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Success(createPaymentIntent(CANCELED))) }) + val canceledIntent = createPaymentIntent(CANCELED) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(canceledIntent)) val result = withTimeoutOrNull(TIMEOUT) { manager @@ -299,23 +239,16 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { .toList() } - assertThat(result).isNotNull // verify the flow did not timeout + assertThat(result).isNotNull verify(cardReaderStore, never()).capturePaymentIntent(any(), anyString()) } @Test fun `given interac payment, when processing payment finishes successfully, then capture payment is emitted`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentStatus.Success( - createPaymentIntent(SUCCEEDED, interacPresentDetails = mock()) - ) - ) - } - ) + val succeededInteracIntent = createPaymentIntent(SUCCEEDED, interacPresentDetails = mock()) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(succeededInteracIntent)) val result = manager .acceptPayment(createPaymentInfo()).takeUntil(CapturingPayment::class).toList() @@ -326,16 +259,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given interac payment, when processing payment finishes with canceled status, then flow terminates`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentStatus.Success( - createPaymentIntent(CANCELED, interacPresentDetails = mock()) - ) - ) - } - ) + val canceledInteracIntent = createPaymentIntent(CANCELED, interacPresentDetails = mock()) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(canceledInteracIntent)) val result = withTimeoutOrNull(TIMEOUT) { manager @@ -343,7 +269,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { .toList() } - assertThat(result).isNotNull // verify the flow did not timeout + assertThat(result).isNotNull verify(cardReaderStore, never()).capturePaymentIntent(any(), anyString()) } @@ -360,8 +286,8 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { } val charges = listOf(charge) whenever(intent.getCharges()).thenReturn(charges) - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Success(intent)) }) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(intent)) val result = manager.acceptPayment(createPaymentInfo()).toList() @@ -381,8 +307,8 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { } val charges = listOf(charge) whenever(intent.getCharges()).thenReturn(charges) - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Success(intent)) }) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(intent)) val result = manager.acceptPayment(createPaymentInfo()).toList() @@ -392,27 +318,21 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given processing payment suc with unknown, when processing, then ProcessingPaymentCompleted emitted`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Success(createPaymentIntent(REQUIRES_CAPTURE))) }) + val captureIntent = createPaymentIntent(REQUIRES_CAPTURE) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(captureIntent)) val result = manager.acceptPayment(createPaymentInfo()).toList() assertThat(result).contains(ProcessingPaymentCompleted(PaymentMethodType.UNKNOWN)) } - // END - Processing Payment - // BEGIN - Capturing Payment @Test fun `when receiptUrl is empty, then PaymentFailed emitted`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) + val noReceiptIntent = createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) .thenReturn( - flow { - emit( - ProcessPaymentStatus.Success( - createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) - ) - ) - } + ProcessPaymentIntentStatus.Success(noReceiptIntent) ) val result = manager @@ -423,16 +343,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when receiptUrl is empty, then PaymentData for retry are empty`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentStatus.Success( - createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) - ) - ) - } - ) + val noReceiptIntent = createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(noReceiptIntent)) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -459,16 +372,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when capturing payment succeeds, then PaymentCompleted event contains receipt url`() = testBlocking { val expectedReceiptUrl = "abcd" - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentStatus.Success( - createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = expectedReceiptUrl) - ) - ) - } - ) + val intentWithReceipt = createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = expectedReceiptUrl) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(ProcessPaymentIntentStatus.Success(intentWithReceipt)) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -508,11 +414,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { verify(paymentErrorMapper).mapCapturePaymentError(anyOrNull(), anyOrNull()) } - // END - Capturing Payment - // BEGIN - Retry @Test - fun `given PaymentStatus REQUIRES_PAYMENT_METHOD, when retrying payment, then flow resumes on collectPayment`() = + fun `given PaymentStatus REQUIRES_PAYMENT_METHOD, when retrying payment, then flow resumes on processPayment`() = testBlocking { val paymentIntent = mock().also { whenever(it.status).thenReturn(REQUIRES_PAYMENT_METHOD) @@ -521,7 +425,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { val result = manager.retryPayment(DUMMY_ORDER_ID, paymentData).first() - assertThat(result).isInstanceOf(CollectingPayment::class.java) + assertThat(result).isInstanceOf(ProcessingPayment::class.java) } @Test @@ -573,9 +477,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { assertThat(result.last()).isInstanceOf(PaymentFailed::class.java) } - // END - Retry - // BEGIN - Cancel @Test fun `given PaymentStatus REQUIRES_PAYMENT_METHOD, when canceling payment, then payment intent canceled`() = testBlocking { @@ -608,7 +510,6 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { verify(cancelPaymentAction, never()).cancelPayment(paymentIntent) } - // END - Cancel private fun createPaymentIntent( status: PaymentIntentStatus, @@ -618,16 +519,16 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { mock().also { whenever(it.status).thenReturn(status) whenever(it.id).thenReturn("dummyId") + val paymentMethodDetails = mock() + whenever(paymentMethodDetails.interacPresentDetails).thenReturn(interacPresentDetails) val charge = mock() whenever(charge.receiptUrl).thenReturn(receiptUrl) - whenever(charge.paymentMethodDetails).thenReturn(mock()) - whenever(charge.paymentMethodDetails?.interacPresentDetails).thenReturn(interacPresentDetails) + whenever(charge.paymentMethodDetails).thenReturn(paymentMethodDetails) whenever(it.getCharges()).thenReturn(listOf(charge)) } private fun Flow.takeUntil(untilStatus: KClass<*>): Flow = this.take(expectedSequence.indexOf(untilStatus) + 1) - // the below lines are here just as a safeguard to verify that the expectedSequence is defined correctly .withIndex() .onEach { if (expectedSequence[it.index] != it.value!!::class) { diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentUtilsTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentUtilsTest.kt index fc06b49d605e..c27231e73940 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentUtilsTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/PaymentUtilsTest.kt @@ -16,6 +16,7 @@ import java.math.BigDecimal private const val NONE_USD_CURRENCY = "CZK" private const val USD_CURRENCY = "USD" +@Suppress("DoNotMockDataClass", "DoNotMockSealedClass") @ExperimentalCoroutinesApi class PaymentUtilsTest : CardReaderBaseUnitTest() { private lateinit var paymentUtils: PaymentUtils diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/RefundErrorMapperTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/RefundErrorMapperTest.kt index 9866aebb6e6b..c7094a98ab8d 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/RefundErrorMapperTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/RefundErrorMapperTest.kt @@ -16,6 +16,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.math.BigDecimal +@Suppress("DoNotMockDataClass") @RunWith(MockitoJUnitRunner::class) class RefundErrorMapperTest { private lateinit var mapper: RefundErrorMapper diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundActionTest.kt deleted file mode 100644 index d64a9ce5706e..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundActionTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.Cancelable -import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest -import com.woocommerce.android.cardreader.internal.payments.actions.CollectInteracRefundAction.CollectInteracRefundStatus.Failure -import com.woocommerce.android.cardreader.internal.payments.actions.CollectInteracRefundAction.CollectInteracRefundStatus.Success -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -class CollectInteracRefundActionTest : CardReaderBaseUnitTest() { - private lateinit var action: CollectInteracRefundAction - private val terminal: TerminalWrapper = mock() - - @Before - fun setUp() { - action = CollectInteracRefundAction(terminal) - } - - @Test - fun `when collecting interac refund succeeds, then Success is emitted`() = testBlocking { - whenever(terminal.refundPayment(any(), any(), any())).thenAnswer { - (it.arguments[2] as Callback).onSuccess() - mock() - } - - val result = action.collectRefund(mock(), mock()).first() - - assertThat(result).isExactlyInstanceOf(Success::class.java) - } - - @Test - fun `when collecting interac refund fails, then Failure is emitted`() = testBlocking { - whenever(terminal.refundPayment(any(), any(), any())).thenAnswer { - (it.arguments[2] as Callback).onFailure(mock()) - mock() - } - - val result = action.collectRefund(mock(), mock()).first() - - assertThat(result).isExactlyInstanceOf(Failure::class.java) - } - - @Test - fun `when collecting interac refund succeeds, then flow is terminated`() = testBlocking { - whenever(terminal.refundPayment(any(), any(), any())).thenAnswer { - (it.arguments[2] as Callback).onSuccess() - mock() - } - - val result = action.collectRefund(mock(), mock()).toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when collecting interac refund fails, then flow is terminated`() = testBlocking { - whenever(terminal.refundPayment(any(), any(), any())).thenAnswer { - (it.arguments[2] as Callback).onFailure(mock()) - mock() - } - - val result = action.collectRefund(mock(), mock()).toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `given flow not terminated, when job canceled, then interac refund gets canceled`() = testBlocking { - val cancelable = mock() - whenever(cancelable.isCompleted).thenReturn(false) - whenever(terminal.refundPayment(any(), any(), any())).thenAnswer { cancelable } - val job = launch { - action.collectRefund(mock(), mock()).collect { } - } - - job.cancel() - joinAll(job) - - verify(cancelable).cancel(any()) - } -} diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentActionTest.kt deleted file mode 100644 index 6039d81a9135..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentActionTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.Cancelable -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback -import com.stripe.stripeterminal.external.models.PaymentIntent -import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction.CollectPaymentStatus.Failure -import com.woocommerce.android.cardreader.internal.payments.actions.CollectPaymentAction.CollectPaymentStatus.Success -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever - -@ExperimentalCoroutinesApi -internal class CollectPaymentActionTest : CardReaderBaseUnitTest() { - private lateinit var action: CollectPaymentAction - private val terminal: TerminalWrapper = mock() - - @Before - fun setUp() { - action = CollectPaymentAction(terminal, mock()) - } - - @Test - fun `when collecting payment succeeds, then Success is emitted`() = testBlocking { - whenever(terminal.collectPaymentMethod(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - mock() - } - - val result = action.collectPayment(mock()).first() - - assertThat(result).isExactlyInstanceOf(Success::class.java) - } - - @Test - fun `when collecting payment fails, then Failure is emitted`() = testBlocking { - whenever(terminal.collectPaymentMethod(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onFailure(mock()) - mock() - } - - val result = action.collectPayment(mock()).first() - - assertThat(result).isExactlyInstanceOf(Failure::class.java) - } - - @Test - fun `when collecting payment succeeds, then updated paymentIntent is returned`() = testBlocking { - val updatedPaymentIntent = mock() - whenever(terminal.collectPaymentMethod(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(updatedPaymentIntent) - mock() - } - - val result = action.collectPayment(mock()).first() - - assertThat((result as Success).paymentIntent).isEqualTo(updatedPaymentIntent) - } - - @Test - fun `when collecting payment succeeds, then flow is terminated`() = testBlocking { - whenever(terminal.collectPaymentMethod(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - mock() - } - - val result = action.collectPayment(mock()).toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when collecting payment fails, then flow is terminated`() = testBlocking { - whenever(terminal.collectPaymentMethod(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onFailure(mock()) - mock() - } - - val result = action.collectPayment(mock()).toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when job canceled, then collect payment gets canceled`() = - testBlocking { - val cancelable = mock() - whenever(cancelable.isCompleted).thenReturn(false) - whenever(terminal.collectPaymentMethod(any(), any())).thenAnswer { cancelable } - val job = launch { - action.collectPayment(mock()).collect { } - } - - job.cancel() - joinAll(job) - - verify(cancelable).cancel(any()) - } -} diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentActionTest.kt index 275120212a9a..887e8d24c65d 100644 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentActionTest.kt +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CreatePaymentActionTest.kt @@ -1,8 +1,8 @@ package com.woocommerce.android.cardreader.internal.payments.actions -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback import com.stripe.stripeterminal.external.models.PaymentIntent import com.stripe.stripeterminal.external.models.PaymentIntentParameters +import com.stripe.stripeterminal.external.models.TerminalException import com.woocommerce.android.cardreader.config.CardReaderConfigFactory import com.woocommerce.android.cardreader.config.CardReaderConfigForCanada import com.woocommerce.android.cardreader.config.CardReaderConfigForUSA @@ -17,8 +17,6 @@ import com.woocommerce.android.cardreader.payments.CardPaymentStatus.PaymentMeth import com.woocommerce.android.cardreader.payments.PaymentInfo import com.woocommerce.android.cardreader.payments.StatementDescriptor import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.toList import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test @@ -31,6 +29,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.math.BigDecimal +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { private lateinit var action: CreatePaymentAction @@ -41,7 +40,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { private val paymentUtils: PaymentUtils = mock() @Before - fun setUp() { + fun setUp() = testBlocking { action = CreatePaymentAction( paymentIntentParametersFactory, terminal, @@ -54,34 +53,30 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { whenever(intentParametersBuilder.setCurrency(any())).thenReturn(intentParametersBuilder) whenever(intentParametersBuilder.setDescription(any())).thenReturn(intentParametersBuilder) whenever(intentParametersBuilder.setMetadata(any())).thenReturn(intentParametersBuilder) + whenever(intentParametersBuilder.setReceiptEmail(any())).thenReturn(intentParametersBuilder) + whenever(intentParametersBuilder.setApplicationFeeAmount(any())).thenReturn(intentParametersBuilder) + whenever(intentParametersBuilder.setStatementDescriptor(any())).thenReturn(intentParametersBuilder) whenever(intentParametersBuilder.build()).thenReturn(mock()) whenever(cardReaderConfigFactory.getCardReaderConfigFor(any())).thenReturn( CardReaderConfigForUSA ) - - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } + whenever(terminal.createPaymentIntent(any())).thenReturn(mock()) } @Test fun `when creating paymentIntent succeeds, then Success is emitted`() = testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } + whenever(terminal.createPaymentIntent(any())).thenReturn(mock()) - val result = action.createPaymentIntent(createPaymentInfo()).first() + val result = action.createPaymentIntent(createPaymentInfo()) assertThat(result).isExactlyInstanceOf(CreatePaymentStatus.Success::class.java) } @Test fun `when creating paymentIntent fails, then Failure is emitted`() = testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onFailure(mock()) - } + whenever(terminal.createPaymentIntent(any())).thenAnswer { throw mock() } - val result = action.createPaymentIntent(createPaymentInfo()).first() + val result = action.createPaymentIntent(createPaymentInfo()) assertThat(result).isExactlyInstanceOf(CreatePaymentStatus.Failure::class.java) } @@ -89,37 +84,13 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating paymentIntent succeeds, then updated paymentIntent is returned`() = testBlocking { val updatedPaymentIntent = mock() - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(updatedPaymentIntent) - } + whenever(terminal.createPaymentIntent(any())).thenReturn(updatedPaymentIntent) - val result = action.createPaymentIntent(createPaymentInfo()).first() + val result = action.createPaymentIntent(createPaymentInfo()) assertThat((result as CreatePaymentStatus.Success).paymentIntent).isEqualTo(updatedPaymentIntent) } - @Test - fun `when creating paymentIntent succeeds, then flow is terminated`() = testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } - - val result = action.createPaymentIntent(createPaymentInfo()).toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when creating paymentIntent fails, then flow is terminated`() = testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onFailure(mock()) - } - - val result = action.createPaymentIntent(createPaymentInfo()).toList() - - assertThat(result.size).isEqualTo(1) - } - @Test fun `given wcpay can send emails, when customer email not empty, then PaymentIntent setReceiptEmail not invoked`() = testBlocking { @@ -130,7 +101,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { customerEmail = expectedEmail, wcpayCanSendReceipt = true ) - ).toList() + ) verify(intentParametersBuilder, never()).setReceiptEmail(any()) } @@ -145,21 +116,21 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { customerEmail = expectedEmail, wcpayCanSendReceipt = false ) - ).toList() + ) verify(intentParametersBuilder).setReceiptEmail(expectedEmail) } @Test fun `when customer email is null, then PaymentIntent setReceiptEmail not invoked`() = testBlocking { - action.createPaymentIntent(createPaymentInfo(customerEmail = null)).toList() + action.createPaymentIntent(createPaymentInfo(customerEmail = null)) verify(intentParametersBuilder, never()).setReceiptEmail(any()) } @Test fun `when customer email is empty, then PaymentIntent setReceiptEmail not invoked`() = testBlocking { - action.createPaymentIntent(createPaymentInfo()).toList() + action.createPaymentIntent(createPaymentInfo()) verify(intentParametersBuilder, never()).setReceiptEmail(any()) } @@ -168,7 +139,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { fun `when statement descriptor not empty, then PaymentIntent setStatementDescriptor invoked`() = testBlocking { val expected = "Site abcd" - action.createPaymentIntent(createPaymentInfo(statementDescriptor = expected)).toList() + action.createPaymentIntent(createPaymentInfo(statementDescriptor = expected)) verify(intentParametersBuilder).setStatementDescriptor(expected) } @@ -177,7 +148,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { fun `when statement descriptor empty, then PaymentIntent setStatementDescriptor NOT invoked`() = testBlocking { val expected = "" - action.createPaymentIntent(createPaymentInfo(statementDescriptor = expected)).toList() + action.createPaymentIntent(createPaymentInfo(statementDescriptor = expected)) verify(intentParametersBuilder, never()).setStatementDescriptor(any()) } @@ -186,7 +157,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { fun `when statement descriptor null, then PaymentIntent setStatementDescriptor NOT invoked`() = testBlocking { val expected: String? = null - action.createPaymentIntent(createPaymentInfo(statementDescriptor = expected)).toList() + action.createPaymentIntent(createPaymentInfo(statementDescriptor = expected)) verify(intentParametersBuilder, never()).setStatementDescriptor(any()) } @@ -195,14 +166,14 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { fun `when creating payment intent, then payment description set`() = testBlocking { val expectedDescription = "test description" - action.createPaymentIntent(createPaymentInfo(paymentDescription = expectedDescription)).toList() + action.createPaymentIntent(createPaymentInfo(paymentDescription = expectedDescription)) verify(intentParametersBuilder).setDescription(expectedDescription) } @Test fun `when statement fee null, then PaymentIntent setApplicationFeeAmount NOT invoked`() = testBlocking { - action.createPaymentIntent(createPaymentInfo()).toList() + action.createPaymentIntent(createPaymentInfo()) verify(intentParametersBuilder, never()).setApplicationFeeAmount(any()) } @@ -211,7 +182,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { fun `when statement fee is not null, then PaymentIntent setApplicationFeeAmount invoked`() = testBlocking { val expected = 100L - action.createPaymentIntent(createPaymentInfo(feeAmount = expected)).toList() + action.createPaymentIntent(createPaymentInfo(feeAmount = expected)) verify(intentParametersBuilder).setApplicationFeeAmount(expected) } @@ -219,12 +190,9 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent, then store name set`() = testBlocking { val expected = "dummy store name" - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo(storeName = expected)).toList() + action.createPaymentIntent(createPaymentInfo(storeName = expected)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.STORE.key]).isEqualTo(expected) @@ -233,12 +201,9 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent, then customer name set`() = testBlocking { val expected = "dummy customer name" - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo(customerName = expected)).toList() + action.createPaymentIntent(createPaymentInfo(customerName = expected)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.CUSTOMER_NAME.key]).isEqualTo(expected) @@ -247,12 +212,9 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent, then customer email set`() = testBlocking { val expected = "dummy customer email" - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo(customerEmail = expected)).toList() + action.createPaymentIntent(createPaymentInfo(customerEmail = expected)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.CUSTOMER_EMAIL.key]).isEqualTo(expected) @@ -261,12 +223,9 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent, then site url set`() = testBlocking { val expected = "dummy site url" - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo(siteUrl = expected)).toList() + action.createPaymentIntent(createPaymentInfo(siteUrl = expected)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.SITE_URL.key]).isEqualTo(expected) @@ -275,12 +234,9 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent, then order id set`() = testBlocking { val expected = 99L - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo(orderId = expected)).toList() + action.createPaymentIntent(createPaymentInfo(orderId = expected)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.ORDER_ID.key]).isEqualTo(expected.toString()) @@ -293,7 +249,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { val reader = mock { on { id }.thenReturn(readerId) } whenever(terminal.getConnectedReader()).thenReturn(reader) - action.createPaymentIntent(createPaymentInfo()).toList() + action.createPaymentIntent(createPaymentInfo()) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.READER_ID.key]).isEqualTo(readerId) @@ -306,7 +262,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { val reader = mock { on { type }.thenReturn(readerModel) } whenever(terminal.getConnectedReader()).thenReturn(reader) - action.createPaymentIntent(createPaymentInfo()).toList() + action.createPaymentIntent(createPaymentInfo()) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue["reader_model"]).isEqualTo(readerModel) @@ -317,7 +273,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { val captor = argumentCaptor>() whenever(terminal.getConnectedReader()).thenReturn(null) - action.createPaymentIntent(createPaymentInfo()).toList() + action.createPaymentIntent(createPaymentInfo()) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue["reader_model"]).isNull() @@ -325,12 +281,9 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent, then payment type set`() = testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo()).toList() + action.createPaymentIntent(createPaymentInfo()) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.PAYMENT_TYPE.key]).isEqualTo(MetaDataKeys.PaymentTypes.SINGLE.key) @@ -338,48 +291,36 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `when creating payment intent, then dollar amount converted to cents`() = testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val amount = BigDecimal(1) whenever(paymentUtils.convertToSmallestCurrencyUnit(eq(amount), eq("USD"))).thenReturn(100L) - action.createPaymentIntent(createPaymentInfo(amount = amount)).toList() + action.createPaymentIntent(createPaymentInfo(amount = amount)) verify(intentParametersBuilder).setAmount(100) } @Test - fun `given payment info with order key, when creating payment intent, then order key is set`() { - testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } - val captor = argumentCaptor>() - val orderKey = "order_key" + fun `given payment info with order key, when creating payment intent, then order key is set`() = testBlocking { + val captor = argumentCaptor>() + val orderKey = "order_key" - action.createPaymentIntent(createPaymentInfo(orderKey = orderKey)).toList() - verify(intentParametersBuilder).setMetadata(captor.capture()) + action.createPaymentIntent(createPaymentInfo(orderKey = orderKey)) + verify(intentParametersBuilder).setMetadata(captor.capture()) - assertThat(captor.firstValue[MetaDataKeys.ORDER_KEY.key]).isEqualTo(orderKey) - } + assertThat(captor.firstValue[MetaDataKeys.ORDER_KEY.key]).isEqualTo(orderKey) } @Test - fun `given payment info with order key is empty, when creating payment intent, then order key is not set`() { + fun `given payment info with order key is empty, when creating payment intent, then order key is not set`() = testBlocking { - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } val captor = argumentCaptor>() val orderKey = "" - action.createPaymentIntent(createPaymentInfo(orderKey = orderKey)).toList() + action.createPaymentIntent(createPaymentInfo(orderKey = orderKey)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.ORDER_KEY.key]).isNull() } - } @Test fun `given store in Canada, when creating payment intent, then payment method set`() = testBlocking { @@ -391,7 +332,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { CardReaderConfigForCanada ) - action.createPaymentIntent(createPaymentInfo(countryCode = "CA")).toList() + action.createPaymentIntent(createPaymentInfo(countryCode = "CA")) verify(paymentIntentParametersFactory).createBuilder(expectedPaymentMethod) } @@ -405,7 +346,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { CardReaderConfigForUSA ) - action.createPaymentIntent(createPaymentInfo(countryCode = "US")).toList() + action.createPaymentIntent(createPaymentInfo(countryCode = "US")) verify(paymentIntentParametersFactory).createBuilder(expectedPaymentMethod) } @@ -414,7 +355,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { fun `when creating payment intent, then platform set to android`() = testBlocking { val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo()).toList() + action.createPaymentIntent(createPaymentInfo()) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue["platform"]).isEqualTo("android") @@ -423,37 +364,29 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { @Test fun `given payment info with pos channel, when creating payment intent, then channel is set`() = testBlocking { val captor = argumentCaptor>() - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } - action.createPaymentIntent(createPaymentInfo(channel = PaymentInfo.PaymentChannel.Pos)).toList() + action.createPaymentIntent(createPaymentInfo(channel = PaymentInfo.PaymentChannel.Pos)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.CHANNEL.key]).isEqualTo("mobile_pos") } @Test - fun `given payment info with store manager channel, when creating payment intent, then channel is set`() = testBlocking { - val captor = argumentCaptor>() - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } + fun `given payment info with store manager channel, when creating payment intent, then channel is set`() = + testBlocking { + val captor = argumentCaptor>() - action.createPaymentIntent(createPaymentInfo(channel = PaymentInfo.PaymentChannel.StoreManager)).toList() - verify(intentParametersBuilder).setMetadata(captor.capture()) + action.createPaymentIntent(createPaymentInfo(channel = PaymentInfo.PaymentChannel.StoreManager)) + verify(intentParametersBuilder).setMetadata(captor.capture()) - assertThat(captor.firstValue[MetaDataKeys.CHANNEL.key]).isEqualTo("mobile_store_management") - } + assertThat(captor.firstValue[MetaDataKeys.CHANNEL.key]).isEqualTo("mobile_store_management") + } @Test fun `given payment info with no channel, when creating payment intent, then channel is not set`() = testBlocking { val captor = argumentCaptor>() - whenever(terminal.createPaymentIntent(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - } - action.createPaymentIntent(createPaymentInfo(channel = null)).toList() + action.createPaymentIntent(createPaymentInfo(channel = null)) verify(intentParametersBuilder).setMetadata(captor.capture()) assertThat(captor.firstValue[MetaDataKeys.CHANNEL.key]).isNull() diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundActionTest.kt deleted file mode 100644 index 7cd6798a600a..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundActionTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.Cancelable -import com.stripe.stripeterminal.external.callable.RefundCallback -import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessInteracRefundAction.ProcessRefundStatus -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.toList -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever - -@ExperimentalCoroutinesApi -@RunWith(MockitoJUnitRunner::class) -class ProcessInteracRefundActionTest : CardReaderBaseUnitTest() { - private lateinit var action: ProcessInteracRefundAction - private val terminal: TerminalWrapper = mock() - - @Before - fun setUp() { - action = ProcessInteracRefundAction(terminal) - } - - @Test - fun `when processing interac refund succeeds, then Success is emitted`() = testBlocking { - whenever(terminal.processRefund(any())).thenAnswer { - (it.arguments[0] as RefundCallback).onSuccess(mock()) - mock() - } - - val result = action.processRefund().first() - - assertThat(result).isExactlyInstanceOf(ProcessRefundStatus.Success::class.java) - } - - @Test - fun `when processing interac refund fails, then Failure is emitted`() = testBlocking { - whenever(terminal.processRefund(any())).thenAnswer { - (it.arguments[0] as RefundCallback).onFailure(mock()) - mock() - } - - val result = action.processRefund().first() - - assertThat(result).isExactlyInstanceOf(ProcessRefundStatus.Failure::class.java) - } - - @Test - fun `when processing interac refund succeeds, then flow is terminated`() = testBlocking { - whenever(terminal.processRefund(any())).thenAnswer { - (it.arguments[0] as RefundCallback).onSuccess(mock()) - mock() - } - - val result = action.processRefund().toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when processing interac refund fails, then flow is terminated`() = testBlocking { - whenever(terminal.processRefund(any())).thenAnswer { - (it.arguments[0] as RefundCallback).onFailure(mock()) - mock() - } - - val result = action.processRefund().toList() - - assertThat(result.size).isEqualTo(1) - } -} diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentActionTest.kt deleted file mode 100644 index df91895467f5..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentActionTest.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.woocommerce.android.cardreader.internal.payments.actions - -import com.stripe.stripeterminal.external.callable.Cancelable -import com.stripe.stripeterminal.external.callable.PaymentIntentCallback -import com.stripe.stripeterminal.external.models.PaymentIntent -import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest -import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentAction.ProcessPaymentStatus -import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.toList -import org.assertj.core.api.Assertions -import org.junit.Before -import org.junit.Test -import org.mockito.kotlin.any -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever - -@ExperimentalCoroutinesApi -internal class ProcessPaymentActionTest : CardReaderBaseUnitTest() { - private lateinit var action: ProcessPaymentAction - private val terminal: TerminalWrapper = mock() - - @Before - fun setUp() { - action = ProcessPaymentAction(terminal, mock()) - } - - @Test - fun `when processing payment succeeds, then Success is emitted`() = testBlocking { - whenever(terminal.processPayment(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - mock() - } - - val result = action.processPayment(mock()).first() - - Assertions.assertThat(result).isExactlyInstanceOf(ProcessPaymentStatus.Success::class.java) - } - - @Test - fun `when processing payment fails, then Failure is emitted`() = testBlocking { - whenever(terminal.processPayment(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onFailure(mock()) - mock() - } - - val result = action.processPayment(mock()).first() - - Assertions.assertThat(result).isExactlyInstanceOf(ProcessPaymentStatus.Failure::class.java) - } - - @Test - fun `when processing payment succeeds, then updated paymentIntent is returned`() = testBlocking { - val updatedPaymentIntent = mock() - whenever(terminal.processPayment(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(updatedPaymentIntent) - mock() - } - - val result = action.processPayment(mock()).first() - - Assertions.assertThat((result as ProcessPaymentStatus.Success).paymentIntent).isEqualTo(updatedPaymentIntent) - } - - @Test - fun `when processing payment succeeds, then flow is terminated`() = testBlocking { - whenever(terminal.processPayment(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onSuccess(mock()) - mock() - } - - val result = action.processPayment(mock()).toList() - - Assertions.assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when processing payment fails, then flow is terminated`() = testBlocking { - whenever(terminal.processPayment(any(), any())).thenAnswer { - (it.arguments[1] as PaymentIntentCallback).onFailure(mock()) - mock() - } - - val result = action.processPayment(mock()).toList() - - Assertions.assertThat(result.size).isEqualTo(1) - } -} diff --git a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundActionTest.kt new file mode 100644 index 000000000000..a37ef6d3a18b --- /dev/null +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundActionTest.kt @@ -0,0 +1,55 @@ +package com.woocommerce.android.cardreader.internal.payments.actions + +import com.stripe.stripeterminal.external.models.Refund +import com.stripe.stripeterminal.external.models.TerminalException +import com.woocommerce.android.cardreader.internal.CardReaderBaseUnitTest +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessRefundAction.ProcessRefundStatus.Failure +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessRefundAction.ProcessRefundStatus.Success +import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@Suppress("DoNotMockDataClass") +@ExperimentalCoroutinesApi +internal class ProcessRefundActionTest : CardReaderBaseUnitTest() { + private lateinit var action: ProcessRefundAction + private val terminal: TerminalWrapper = mock() + + @Before + fun setUp() { + action = ProcessRefundAction(terminal) + } + + @Test + fun `when process refund succeeds, then Success is returned`() = testBlocking { + whenever(terminal.processRefund(any(), any())).thenReturn(mock()) + + val result = action.processRefund(mock(), mock()) + + assertThat(result).isExactlyInstanceOf(Success::class.java) + } + + @Test + fun `when process refund fails, then Failure is returned`() = testBlocking { + whenever(terminal.processRefund(any(), any())).thenAnswer { throw mock() } + + val result = action.processRefund(mock(), mock()) + + assertThat(result).isExactlyInstanceOf(Failure::class.java) + } + + @Test + fun `when process refund succeeds, then refund is returned`() = testBlocking { + val refund = mock() + whenever(terminal.processRefund(any(), any())).thenReturn(refund) + + val result = action.processRefund(mock(), mock()) + + assertThat((result as Success).refund).isEqualTo(refund) + } +} diff --git a/libs/login/src/main/java/org/wordpress/android/login/LoginEmailFragment.java b/libs/login/src/main/java/org/wordpress/android/login/LoginEmailFragment.java index 8d4335a9b695..9c1a265763d1 100644 --- a/libs/login/src/main/java/org/wordpress/android/login/LoginEmailFragment.java +++ b/libs/login/src/main/java/org/wordpress/android/login/LoginEmailFragment.java @@ -275,7 +275,7 @@ protected void setupBottomButton(Button button) { @NonNull private Spanned formatTosText(int stringResId) { final int primaryColorResId = ContextExtensionsKt.getColorResIdFromAttribute(getContext(), - com.google.android.material.R.attr.colorPrimary); + androidx.appcompat.R.attr.colorPrimary); final String primaryColorHtml = HtmlUtils.colorResToHtmlColor(getContext(), primaryColorResId); return Html.fromHtml(getString(stringResId, "", "")); }