From 6451a04dd7e397e810081cc64559c763b407509d Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Dec 2025 19:30:16 +0800 Subject: [PATCH 01/39] Update Stripe Terminal SDK to 5.0.0 and add ktx module --- gradle/libs.versions.toml | 3 ++- libs/cardreader/build.gradle | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d8cd6928dba..5dc780f25807 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.0.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) From 5d81685ebf8c9dc79c144139fb832a85a6d5f7c4 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Dec 2025 19:30:32 +0800 Subject: [PATCH 02/39] Update Terminal.initTerminal() to Terminal.init() with OfflineListener --- .../android/cardreader/internal/wrappers/TerminalWrapper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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..abd504368428 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 @@ -40,7 +40,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 = [ From ce6b0ff82470543f8b99e0ff42abd6a94e0657c7 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Dec 2025 19:31:17 +0800 Subject: [PATCH 03/39] Rename RefundConfiguration to CollectRefundConfiguration and use CustomerCancellation enum --- .../internal/wrappers/TerminalWrapper.kt | 4 ++-- .../android/cardreader/payments/RefundConfig.kt | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) 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 abd504368428..16d8b2e46853 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 @@ -16,7 +16,7 @@ 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.CollectRefundConfiguration import com.stripe.stripeterminal.external.models.RefundParameters import com.stripe.stripeterminal.external.models.SimulateReaderUpdate import com.stripe.stripeterminal.external.models.SimulatedCard @@ -87,7 +87,7 @@ internal class TerminalWrapper { fun refundPayment( refundParameters: RefundParameters, - refundConfiguration: RefundConfiguration, + refundConfiguration: CollectRefundConfiguration, callback: Callback ) = Terminal.getInstance().collectRefundPaymentMethod(refundParameters, refundConfiguration, callback) 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() } From e66940aefd4fb963ff79c5a4f5d2d4ac5fa9a3fb Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Dec 2025 19:31:47 +0800 Subject: [PATCH 04/39] Update TapZone configuration to use new simplified API --- .../cardreader/internal/wrappers/TerminalWrapper.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 16d8b2e46853..79b764bad59d 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 @@ -121,12 +121,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)) From 3befd12faa4c2da84aecb67de9ecea06ec37ad87 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Dec 2025 19:33:35 +0800 Subject: [PATCH 05/39] Handle new ConnectionStatus.RECONNECTING and add CardReaderStatus.Reconnecting --- .../cardreader/CardReaderManagerFactory.kt | 2 +- .../cardreader/connection/CardReaderStatus.kt | 1 + .../connection/BluetoothReaderListenerImpl.kt | 18 ++++++++++++++++++ .../connection/TapToPayReaderListenerImpl.kt | 11 ++++++++--- 4 files changed, 28 insertions(+), 4 deletions(-) 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..7be658f54a60 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 @@ -42,7 +42,7 @@ object CardReaderManagerFactory { UpdateErrorMapper(batteryLevelProvider), terminalListener ) - val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper) + val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper, terminalListener) val cardReaderConfigFactory = CardReaderConfigFactory() val paymentUtils = PaymentUtils(logWrapper) 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/connection/BluetoothReaderListenerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt index 87f5c67edafb..a8d2e57fcb8a 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 @@ -117,6 +117,24 @@ internal class BluetoothReaderListenerImpl( terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected(errorCode = errorCode)) } + override fun onReaderReconnectFailed(reader: com.stripe.stripeterminal.external.models.Reader) { + logWrapper.d(LOG_TAG, "onReaderReconnectFailed") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) + } + + override fun onReaderReconnectStarted( + reader: com.stripe.stripeterminal.external.models.Reader, + cancelReconnect: Cancelable, + reason: DisconnectReason + ) { + logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) + } + + override fun onReaderReconnectSucceeded(reader: com.stripe.stripeterminal.external.models.Reader) { + logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") + } + fun resetConnectionState() { _updateStatusEvents.value = SoftwareUpdateStatus.Unknown _updateAvailabilityEvents.value = SoftwareUpdateAvailability.NotAvailable 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..68a94328ea9d 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 @@ -5,17 +5,21 @@ 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.CardReaderStatus import com.woocommerce.android.cardreader.internal.LOG_TAG class TapToPayReaderListenerImpl( - private val logWrapper: LogWrapper + private val logWrapper: LogWrapper, + private val terminalListenerImpl: TerminalListenerImpl ) : TapToPayReaderListener { 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") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } override fun onReaderReconnectStarted( @@ -23,7 +27,8 @@ class TapToPayReaderListenerImpl( cancelReconnect: Cancelable, reason: DisconnectReason ) { - logWrapper.d(LOG_TAG, "onReaderReconnectStarted") + logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) } override fun onReaderReconnectSucceeded(reader: Reader) { From 4949bac3f595f42484a4a75136960274838d6a44 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Dec 2025 19:34:13 +0800 Subject: [PATCH 06/39] Update clearCachedCredentials to handle new return type --- .../android/cardreader/internal/wrappers/TerminalWrapper.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 79b764bad59d..16fbc2febec5 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 @@ -69,7 +69,9 @@ internal class TerminalWrapper { fun disconnectReader(callback: Callback) = Terminal.getInstance().disconnectReader(callback) - fun clearCachedCredentials() = Terminal.getInstance().clearCachedCredentials() + fun clearCachedCredentials() { + Terminal.getInstance().clearCachedCredentials() + } fun createPaymentIntent(params: PaymentIntentParameters, callback: PaymentIntentCallback) = Terminal.getInstance().createPaymentIntent(params, callback) From bafde8af58d5d8b3ae28ddd1b9fd59f345825bd4 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 1 Dec 2025 19:38:35 +0800 Subject: [PATCH 07/39] Fix compilation errors: RefundParameters API, class visibility, suppress deprecations --- .../internal/connection/BluetoothReaderListenerImpl.kt | 9 ++++++--- .../internal/connection/TapToPayReaderListenerImpl.kt | 4 +++- .../payments/actions/CollectInteracRefundAction.kt | 4 ++-- .../cardreader/internal/wrappers/TerminalWrapper.kt | 4 +++- .../android/cardreader/payments/RefundParams.kt | 5 ++--- 5 files changed, 16 insertions(+), 10 deletions(-) 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 a8d2e57fcb8a..c6ad3fda6838 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 @@ -4,12 +4,14 @@ 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 @@ -117,13 +119,13 @@ internal class BluetoothReaderListenerImpl( terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected(errorCode = errorCode)) } - override fun onReaderReconnectFailed(reader: com.stripe.stripeterminal.external.models.Reader) { + override fun onReaderReconnectFailed(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectFailed") terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } override fun onReaderReconnectStarted( - reader: com.stripe.stripeterminal.external.models.Reader, + reader: Reader, cancelReconnect: Cancelable, reason: DisconnectReason ) { @@ -131,8 +133,9 @@ internal class BluetoothReaderListenerImpl( terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) } - override fun onReaderReconnectSucceeded(reader: com.stripe.stripeterminal.external.models.Reader) { + override fun onReaderReconnectSucceeded(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) } fun resetConnectionState() { 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 68a94328ea9d..5fe08e02515c 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 @@ -5,10 +5,11 @@ 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( +internal class TapToPayReaderListenerImpl( private val logWrapper: LogWrapper, private val terminalListenerImpl: TerminalListenerImpl ) : TapToPayReaderListener { @@ -33,5 +34,6 @@ class TapToPayReaderListenerImpl( override fun onReaderReconnectSucceeded(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) } } 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 index d79d2c4149e2..1b9a0bf2b198 100644 --- 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 @@ -1,7 +1,7 @@ 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.CollectRefundConfiguration import com.stripe.stripeterminal.external.models.RefundParameters import com.stripe.stripeterminal.external.models.TerminalException import com.woocommerce.android.cardreader.internal.wrappers.TerminalWrapper @@ -17,7 +17,7 @@ internal class CollectInteracRefundAction(private val terminal: TerminalWrapper) fun collectRefund( refundParameters: RefundParameters, - refundConfiguration: RefundConfiguration + refundConfiguration: CollectRefundConfiguration ): Flow { return callbackFlow { val cancelable = terminal.refundPayment( 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 16fbc2febec5..44dcb11d9d8b 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 @@ -11,12 +11,12 @@ 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.CollectRefundConfiguration 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.CollectRefundConfiguration import com.stripe.stripeterminal.external.models.RefundParameters import com.stripe.stripeterminal.external.models.SimulateReaderUpdate import com.stripe.stripeterminal.external.models.SimulatedCard @@ -87,12 +87,14 @@ internal class TerminalWrapper { fun cancelPayment(paymentIntent: PaymentIntent, callback: PaymentIntentCallback) = Terminal.getInstance().cancelPaymentIntent(paymentIntent, callback) + @Suppress("DEPRECATION") fun refundPayment( refundParameters: RefundParameters, refundConfiguration: CollectRefundConfiguration, callback: Callback ) = Terminal.getInstance().collectRefundPaymentMethod(refundParameters, refundConfiguration, callback) + @Suppress("DEPRECATION") fun processRefund(callback: RefundCallback) = Terminal.getInstance().confirmRefund(callback) 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() From fedada33fc3a8d8dfa2b1ee74cd8be54d00032f8 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 15:48:57 +0800 Subject: [PATCH 08/39] Remove unused Reconnecting status and reconnect callbacks --- .../cardreader/CardReaderManagerFactory.kt | 2 +- .../cardreader/connection/CardReaderStatus.kt | 1 - .../connection/BluetoothReaderListenerImpl.kt | 21 ------------------- .../connection/TapToPayReaderListenerImpl.kt | 15 ++++--------- 4 files changed, 5 insertions(+), 34 deletions(-) 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 7be658f54a60..3ebd9a418695 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 @@ -42,7 +42,7 @@ object CardReaderManagerFactory { UpdateErrorMapper(batteryLevelProvider), terminalListener ) - val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper, terminalListener) + val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper) val cardReaderConfigFactory = CardReaderConfigFactory() val paymentUtils = PaymentUtils(logWrapper) 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 8edaee24b334..e1edf87595e8 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,5 +13,4 @@ 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/connection/BluetoothReaderListenerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt index c6ad3fda6838..87f5c67edafb 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 @@ -4,14 +4,12 @@ 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 @@ -119,25 +117,6 @@ internal class BluetoothReaderListenerImpl( terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected(errorCode = errorCode)) } - override fun onReaderReconnectFailed(reader: Reader) { - logWrapper.d(LOG_TAG, "onReaderReconnectFailed") - terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) - } - - override fun onReaderReconnectStarted( - reader: Reader, - cancelReconnect: Cancelable, - reason: DisconnectReason - ) { - logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") - terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) - } - - override fun onReaderReconnectSucceeded(reader: Reader) { - logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") - terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) - } - fun resetConnectionState() { _updateStatusEvents.value = SoftwareUpdateStatus.Unknown _updateAvailabilityEvents.value = SoftwareUpdateAvailability.NotAvailable 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 5fe08e02515c..b5d08f4d6137 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 @@ -5,22 +5,17 @@ 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 -internal class TapToPayReaderListenerImpl( - private val logWrapper: LogWrapper, - private val terminalListenerImpl: TerminalListenerImpl +class TapToPayReaderListenerImpl( + private val logWrapper: LogWrapper ) : TapToPayReaderListener { override fun onDisconnect(reason: DisconnectReason) { - logWrapper.d(LOG_TAG, "onDisconnect: reason=$reason") - terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) + logWrapper.d(LOG_TAG, "onDisconnect") } override fun onReaderReconnectFailed(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectFailed") - terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } override fun onReaderReconnectStarted( @@ -28,12 +23,10 @@ internal class TapToPayReaderListenerImpl( cancelReconnect: Cancelable, reason: DisconnectReason ) { - logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") - terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) + logWrapper.d(LOG_TAG, "onReaderReconnectStarted") } override fun onReaderReconnectSucceeded(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") - terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) } } From 86bec234f40bc911aad18365c971e44ce79fafdf Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 16:26:53 +0800 Subject: [PATCH 09/39] Suppress lint warnings for mocking Stripe SDK data classes in tests --- .../android/cardreader/internal/CardReaderManagerImplTest.kt | 1 + .../cardreader/internal/payments/InteracRefundManagerTest.kt | 1 + .../android/cardreader/internal/payments/PaymentManagerTest.kt | 1 + .../android/cardreader/internal/payments/PaymentUtilsTest.kt | 1 + .../cardreader/internal/payments/RefundErrorMapperTest.kt | 1 + .../internal/payments/actions/CollectInteracRefundActionTest.kt | 1 + .../internal/payments/actions/CollectPaymentActionTest.kt | 1 + .../internal/payments/actions/CreatePaymentActionTest.kt | 1 + .../internal/payments/actions/ProcessInteracRefundActionTest.kt | 1 + .../internal/payments/actions/ProcessPaymentActionTest.kt | 1 + 10 files changed, 10 insertions(+) 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/payments/InteracRefundManagerTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/InteracRefundManagerTest.kt index 88b4764390bf..f7cdfa9dfbd3 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 @@ -37,6 +37,7 @@ 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() { 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..2feacd636ead 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 @@ -69,6 +69,7 @@ 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 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 index d64a9ce5706e..3e6dd6b08ad3 100644 --- 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 @@ -21,6 +21,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class CollectInteracRefundActionTest : CardReaderBaseUnitTest() { 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 index 6039d81a9135..1dec929f7793 100644 --- 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 @@ -20,6 +20,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi internal class CollectPaymentActionTest : CardReaderBaseUnitTest() { private lateinit var action: CollectPaymentAction 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..27a192e91c97 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 @@ -31,6 +31,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 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 index 7cd6798a600a..83ff9c4318b7 100644 --- 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 @@ -17,6 +17,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi @RunWith(MockitoJUnitRunner::class) class ProcessInteracRefundActionTest : CardReaderBaseUnitTest() { 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 index df91895467f5..aa75648b2e01 100644 --- 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 @@ -16,6 +16,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever +@Suppress("DoNotMockDataClass") @ExperimentalCoroutinesApi internal class ProcessPaymentActionTest : CardReaderBaseUnitTest() { private lateinit var action: ProcessPaymentAction From e3f22f9e287755806f38ca0ff890979b45475db4 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 17:48:48 +0800 Subject: [PATCH 10/39] Add CardReaderStatus.Reconnecting and implement reconnect callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../cardreader/CardReaderManagerFactory.kt | 2 +- .../cardreader/connection/CardReaderStatus.kt | 1 + .../connection/BluetoothReaderListenerImpl.kt | 21 +++++++++++++++++++ .../connection/TapToPayReaderListenerImpl.kt | 15 +++++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) 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..7be658f54a60 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 @@ -42,7 +42,7 @@ object CardReaderManagerFactory { UpdateErrorMapper(batteryLevelProvider), terminalListener ) - val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper) + val tapToPayReaderListener = TapToPayReaderListenerImpl(logWrapper, terminalListener) val cardReaderConfigFactory = CardReaderConfigFactory() val paymentUtils = PaymentUtils(logWrapper) 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/connection/BluetoothReaderListenerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt index 87f5c67edafb..c6ad3fda6838 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 @@ -4,12 +4,14 @@ 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 @@ -117,6 +119,25 @@ internal class BluetoothReaderListenerImpl( terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected(errorCode = errorCode)) } + override fun onReaderReconnectFailed(reader: Reader) { + logWrapper.d(LOG_TAG, "onReaderReconnectFailed") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) + } + + override fun onReaderReconnectStarted( + reader: Reader, + cancelReconnect: Cancelable, + reason: DisconnectReason + ) { + logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) + } + + override fun onReaderReconnectSucceeded(reader: Reader) { + logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) + } + fun resetConnectionState() { _updateStatusEvents.value = SoftwareUpdateStatus.Unknown _updateAvailabilityEvents.value = SoftwareUpdateAvailability.NotAvailable 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..5fe08e02515c 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 @@ -5,17 +5,22 @@ 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 { 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") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } override fun onReaderReconnectStarted( @@ -23,10 +28,12 @@ class TapToPayReaderListenerImpl( cancelReconnect: Cancelable, reason: DisconnectReason ) { - logWrapper.d(LOG_TAG, "onReaderReconnectStarted") + logWrapper.d(LOG_TAG, "onReaderReconnectStarted: reason=$reason") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Reconnecting) } override fun onReaderReconnectSucceeded(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectSucceeded") + terminalListenerImpl.updateReaderStatus(CardReaderStatus.Connected(CardReaderImpl(reader))) } } From 1f0a54938b41a0cc8057166dfb431c8ccff60a76 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 17:48:54 +0800 Subject: [PATCH 11/39] Add reconnecting status string resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- WooCommerce/src/main/res/values/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index ac0e6093b3d0..66d380e6fd21 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -3590,6 +3590,7 @@ Reader connected Connect your reader + Reconnecting… Check out Remove %s from cart Product %s, Price %s From 6fee71a2e826695763addc87873ebc6dce92e653 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 17:49:13 +0800 Subject: [PATCH 12/39] Display Reconnecting status in UI components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../cardreader/connect/CardReaderConnectViewModel.kt | 4 ++++ .../cardreader/detail/CardReaderDetailViewModel.kt | 4 ++++ .../woopos/home/toolbar/WooPosHomeFloatingToolbar.kt | 12 ++++++------ .../home/toolbar/WooPosHomeFloatingToolbarState.kt | 1 + .../toolbar/WooPosHomeFloatingToolbarViewModel.kt | 6 ++++++ .../ui/woopos/home/totals/WooPosTotalsViewModel.kt | 5 +++++ .../WooPosSettingsHardwareCardReaderViewModel.kt | 5 +++++ 7 files changed, 31 insertions(+), 6 deletions(-) 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/CardReaderDetailViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/payments/cardreader/detail/CardReaderDetailViewModel.kt index a77fa5141254..83378e21d2cf 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 -> { + // Keep current state while SDK attempts to reconnect + } } } } 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..4f2609de515b 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 -> { + // Do nothing while reconnecting + } } } 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/WooPosTotalsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModel.kt index 58ca9e213fe2..a05425feecba 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 -> { + // Keep current state while SDK attempts to reconnect + } + is Connected -> { val state = uiState.value if (state !is WooPosTotalsViewState.Checkout) return@collect 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 + } } } } From 54d63a3b21ddbf009db46597aaa4c4e791159214 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 18:02:44 +0800 Subject: [PATCH 13/39] Show reconnecting status in CardReaderDetailFragment --- .../detail/CardReaderDetailFragment.kt | 28 ++++++++++++++++++- .../detail/CardReaderDetailViewModel.kt | 17 ++++++++++- WooCommerce/src/main/res/values/strings.xml | 2 ++ 3 files changed, 45 insertions(+), 2 deletions(-) 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..5c8b8d6f9a60 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,28 @@ 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 + ) + UiHelpers.setTextOrHide(cardReaderDetailFirstHintLabel, state.firstHintLabel) + cardReaderDetailFirstHintNumberLabel.visibility = View.GONE + cardReaderDetailSecondHintLabel.visibility = View.GONE + cardReaderDetailSecondHintNumberLabel.visibility = View.GONE + cardReaderDetailThirdHintLabel.visibility = View.GONE + cardReaderDetailThirdHintNumberLabel.visibility = View.GONE + cardReaderDetailConnectBtn.visibility = View.GONE + with(cardReaderDetailLearnMoreTv.root) { + movementMethod = LinkMovementMethod.getInstance() + UiHelpers.setTextOrHide(this, state.learnMoreLabel) + setOnClickListener { state.onLearnMoreClicked.invoke() } + } + } + } } } } @@ -192,7 +215,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 83378e21d2cf..f41548ee7d26 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 @@ -88,7 +88,7 @@ class CardReaderDetailViewModel @Inject constructor( handleNotConnectedState() } Reconnecting -> { - // Keep current state while SDK attempts to reconnect + handleReconnectingState() } } } @@ -138,6 +138,10 @@ class CardReaderDetailViewModel @Inject constructor( NotConnectedState(onPrimaryActionClicked = ::onConnectBtnClicked, onLearnMoreClicked = ::onLearnMoreClicked) } + private fun handleReconnectingState() { + viewState.value = ViewState.ReconnectingState(onLearnMoreClicked = ::onLearnMoreClicked) + } + private fun cancelConnectedScopeJobs() { if (::softwareUpdateAvailabilityJob.isInitialized) softwareUpdateAvailabilityJob.cancel() if (::batteryStatusUpdateJob.isInitialized) batteryStatusUpdateJob.cancel() @@ -311,6 +315,17 @@ class CardReaderDetailViewModel @Inject constructor( } object Loading : ViewState() + + data class ReconnectingState( + val onLearnMoreClicked: (() -> Unit), + ) : ViewState() { + val headerLabel = UiStringRes(R.string.card_reader_detail_reconnecting_header) + + @DrawableRes + val illustration = R.drawable.img_card_reader_not_connected + val firstHintLabel = UiStringRes(R.string.card_reader_detail_reconnecting_hint) + val learnMoreLabel = UiStringRes(R.string.card_reader_detail_learn_more, containsHtml = true) + } } } diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 66d380e6fd21..538a9807d975 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… + Please wait while we attempt to reconnect <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 From 22181c19204aa25dd8776872ee7e5f35be4bcc4a Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 18:10:03 +0800 Subject: [PATCH 14/39] Fix Material Design 1.12 colorPrimary attribute migration --- .../java/org/wordpress/android/login/LoginEmailFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, "", "")); } From d86631d9e9cec98ff858654c5cc51e154dc1217b Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 18:19:00 +0800 Subject: [PATCH 15/39] Allow user to cancel reconnection by tapping status button --- .../woopos/home/toolbar/WooPosHomeFloatingToolbarViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 4f2609de515b..b9772f22a133 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 @@ -131,7 +131,9 @@ class WooPosHomeFloatingToolbarViewModel @Inject constructor( } WooPosHomeFloatingToolbarState.WooPosCardReaderStatus.Reconnecting -> { - // Do nothing while reconnecting + viewModelScope.launch { + cardReaderFacade.disconnectFromReader() + } } } } From a97d9c5abf639bd594d641f6d0dc90df2ceb7bcc Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 18:44:59 +0800 Subject: [PATCH 16/39] Add cancelReconnection to properly cancel auto-reconnect --- .../android/di/MockCardReaderManagerModule.kt | 2 ++ .../ui/woopos/cardreader/WooPosCardReaderFacade.kt | 4 ++++ .../toolbar/WooPosHomeFloatingToolbarViewModel.kt | 4 +--- .../android/cardreader/CardReaderManager.kt | 1 + .../cardreader/internal/CardReaderManagerImpl.kt | 5 +++++ .../connection/BluetoothReaderListenerImpl.kt | 10 ++++++++++ .../internal/connection/ConnectionManager.kt | 14 ++++++++++++++ .../connection/TapToPayReaderListenerImpl.kt | 11 +++++++++++ 8 files changed, 48 insertions(+), 3 deletions(-) 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..a00470a8384c 100644 --- a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt +++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt @@ -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/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/WooPosHomeFloatingToolbarViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/toolbar/WooPosHomeFloatingToolbarViewModel.kt index b9772f22a133..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 @@ -131,9 +131,7 @@ class WooPosHomeFloatingToolbarViewModel @Inject constructor( } WooPosHomeFloatingToolbarState.WooPosCardReaderStatus.Reconnecting -> { - viewModelScope.launch { - cardReaderFacade.disconnectFromReader() - } + cardReaderFacade.cancelReconnection() } } } 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..4f11159f8b89 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 @@ -47,6 +47,7 @@ interface CardReaderManager { 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/internal/CardReaderManagerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/CardReaderManagerImpl.kt index 6b7bfe9efb5d..0d555d206001 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 @@ -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/connection/BluetoothReaderListenerImpl.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/BluetoothReaderListenerImpl.kt index c6ad3fda6838..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,5 +1,6 @@ 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 @@ -47,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") @@ -121,6 +123,7 @@ internal class BluetoothReaderListenerImpl( override fun onReaderReconnectFailed(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectFailed") + cancelReconnectAction = null terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } @@ -130,11 +133,13 @@ internal class BluetoothReaderListenerImpl( 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))) } @@ -146,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..4f3526e2961b 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 @@ -163,6 +163,20 @@ internal class ConnectionManager( }) } + fun cancelReconnection() { + val callback = object : Callback { + override fun onFailure(e: TerminalException) { + updateReaderStatus(CardReaderStatus.NotConnected()) + } + + override fun onSuccess() { + updateReaderStatus(CardReaderStatus.NotConnected()) + } + } + bluetoothReaderListener.cancelReconnection(callback) + tapToPayReaderListener.cancelReconnection(callback) + } + private fun startStateResettingJobIfNeeded(currentStatus: CardReaderStatus) { if (currentStatus !is CardReaderStatus.Connecting) return 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 5fe08e02515c..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,5 +1,6 @@ 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 @@ -13,6 +14,8 @@ 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: reason=$reason") terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) @@ -20,6 +23,7 @@ internal class TapToPayReaderListenerImpl( override fun onReaderReconnectFailed(reader: Reader) { logWrapper.d(LOG_TAG, "onReaderReconnectFailed") + cancelReconnectAction = null terminalListenerImpl.updateReaderStatus(CardReaderStatus.NotConnected()) } @@ -29,11 +33,18 @@ internal class TapToPayReaderListenerImpl( 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 cancelReconnection(callback: Callback) { + cancelReconnectAction?.cancel(callback) + cancelReconnectAction = null + } } From 96d7194dab44ab41a0a338a855fad0414e4cf928 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 18:52:22 +0800 Subject: [PATCH 17/39] Add cancel button to CardReaderDetail reconnecting state --- .../cardreader/detail/CardReaderDetailFragment.kt | 11 ++++------- .../cardreader/detail/CardReaderDetailViewModel.kt | 13 +++++++++---- WooCommerce/src/main/res/values/strings.xml | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) 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 5c8b8d6f9a60..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 @@ -189,18 +189,15 @@ class CardReaderDetailFragment : BaseFragment(R.layout.fragment_card_reader_deta cardReaderDetailIllustration, state.illustration ) - UiHelpers.setTextOrHide(cardReaderDetailFirstHintLabel, state.firstHintLabel) + cardReaderDetailFirstHintLabel.visibility = View.GONE cardReaderDetailFirstHintNumberLabel.visibility = View.GONE cardReaderDetailSecondHintLabel.visibility = View.GONE cardReaderDetailSecondHintNumberLabel.visibility = View.GONE cardReaderDetailThirdHintLabel.visibility = View.GONE cardReaderDetailThirdHintNumberLabel.visibility = View.GONE - cardReaderDetailConnectBtn.visibility = View.GONE - with(cardReaderDetailLearnMoreTv.root) { - movementMethod = LinkMovementMethod.getInstance() - UiHelpers.setTextOrHide(this, state.learnMoreLabel) - setOnClickListener { state.onLearnMoreClicked.invoke() } - } + UiHelpers.setTextOrHide(cardReaderDetailConnectBtn, state.cancelBtnLabel) + cardReaderDetailConnectBtn.setOnClickListener { state.onCancelClicked.invoke() } + cardReaderDetailLearnMoreTv.root.visibility = View.GONE } } } 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 f41548ee7d26..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 @@ -139,7 +139,13 @@ class CardReaderDetailViewModel @Inject constructor( } private fun handleReconnectingState() { - viewState.value = ViewState.ReconnectingState(onLearnMoreClicked = ::onLearnMoreClicked) + viewState.value = ViewState.ReconnectingState( + onCancelClicked = ::onCancelReconnectionClicked + ) + } + + private fun onCancelReconnectionClicked() { + cardReaderManager.cancelReconnection() } private fun cancelConnectedScopeJobs() { @@ -317,14 +323,13 @@ class CardReaderDetailViewModel @Inject constructor( object Loading : ViewState() data class ReconnectingState( - val onLearnMoreClicked: (() -> Unit), + 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 firstHintLabel = UiStringRes(R.string.card_reader_detail_reconnecting_hint) - val learnMoreLabel = UiStringRes(R.string.card_reader_detail_learn_more, containsHtml = true) + val cancelBtnLabel = UiStringRes(R.string.card_reader_detail_reconnecting_cancel) } } } diff --git a/WooCommerce/src/main/res/values/strings.xml b/WooCommerce/src/main/res/values/strings.xml index 538a9807d975..7df88370f621 100644 --- a/WooCommerce/src/main/res/values/strings.xml +++ b/WooCommerce/src/main/res/values/strings.xml @@ -1635,7 +1635,7 @@ --> Connect your card reader Reconnecting to card reader… - Please wait while we attempt to reconnect + 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 From a4de3512acde581e519398c625cced14b8edf2ad Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 19:44:33 +0800 Subject: [PATCH 18/39] Update comment in WooPosTotalsViewModel to clarify handling of Reconnecting state --- .../android/ui/woopos/home/totals/WooPosTotalsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a05425feecba..4c12d5d66cf2 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 @@ -124,7 +124,7 @@ class WooPosTotalsViewModel @Inject constructor( } Reconnecting -> { - // Keep current state while SDK attempts to reconnect + // We start payment right away so this state not worth handling } is Connected -> { From 6f47449e727959235723ff77b5064faebe2489f9 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 19:55:11 +0800 Subject: [PATCH 19/39] Add unit tests for reconnecting state handling --- .../detail/CardReaderDetailViewModelTest.kt | 28 +++++++++++++++++++ .../WooPosHomeFloatingToolbarViewModelTest.kt | 25 +++++++++++++++++ 2 files changed, 53 insertions(+) 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/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, From abc47a08ccee5b9179e351f6d1bcccafbce90c27 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 20:22:50 +0800 Subject: [PATCH 20/39] Add ProcessRefundAction for single-call refund API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../payments/actions/ProcessRefundAction.kt | 55 +++++++++ .../actions/ProcessRefundActionTest.kt | 110 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundAction.kt create mode 100644 libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundActionTest.kt 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..534dd62c7696 --- /dev/null +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundAction.kt @@ -0,0 +1,55 @@ +package com.woocommerce.android.cardreader.internal.payments.actions + +import com.stripe.stripeterminal.external.callable.Callback +import com.stripe.stripeterminal.external.callable.RefundCallback +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 +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +internal class ProcessRefundAction(private val terminal: TerminalWrapper) { + sealed class ProcessRefundStatus { + data class Success(val refund: Refund) : ProcessRefundStatus() + data class Failure(val exception: TerminalException) : ProcessRefundStatus() + } + + fun processRefund( + refundParameters: RefundParameters, + refundConfiguration: CollectRefundConfiguration + ): Flow { + return callbackFlow { + val cancelable = terminal.processRefund( + refundParameters, + refundConfiguration, + object : RefundCallback { + override fun onSuccess(refund: Refund) { + trySend(ProcessRefundStatus.Success(refund)) + close() + } + + override fun onFailure(e: TerminalException) { + trySend(ProcessRefundStatus.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/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..d918a2962ec5 --- /dev/null +++ b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundActionTest.kt @@ -0,0 +1,110 @@ +package com.woocommerce.android.cardreader.internal.payments.actions + +import com.stripe.stripeterminal.external.callable.Cancelable +import com.stripe.stripeterminal.external.callable.RefundCallback +import com.stripe.stripeterminal.external.models.Refund +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 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 + +@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 emitted`() = testBlocking { + whenever(terminal.processRefund(any(), any(), any())).thenAnswer { + (it.arguments[2] as RefundCallback).onSuccess(mock()) + mock() + } + + val result = action.processRefund(mock(), mock()).first() + + assertThat(result).isExactlyInstanceOf(Success::class.java) + } + + @Test + fun `when process refund fails, then Failure is emitted`() = testBlocking { + whenever(terminal.processRefund(any(), any(), any())).thenAnswer { + (it.arguments[2] as RefundCallback).onFailure(mock()) + mock() + } + + val result = action.processRefund(mock(), mock()).first() + + 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(), any())).thenAnswer { + (it.arguments[2] as RefundCallback).onSuccess(refund) + mock() + } + + val result = action.processRefund(mock(), mock()).first() + + assertThat((result as Success).refund).isEqualTo(refund) + } + + @Test + fun `when process refund succeeds, then flow is terminated`() = testBlocking { + whenever(terminal.processRefund(any(), any(), any())).thenAnswer { + (it.arguments[2] as RefundCallback).onSuccess(mock()) + mock() + } + + val result = action.processRefund(mock(), mock()).toList() + + assertThat(result.size).isEqualTo(1) + } + + @Test + fun `when process refund fails, then flow is terminated`() = testBlocking { + whenever(terminal.processRefund(any(), any(), any())).thenAnswer { + (it.arguments[2] as RefundCallback).onFailure(mock()) + mock() + } + + val result = action.processRefund(mock(), mock()).toList() + + assertThat(result.size).isEqualTo(1) + } + + @Test + fun `when job canceled, then process refund gets canceled`() = + testBlocking { + val cancelable = mock() + whenever(cancelable.isCompleted).thenReturn(false) + whenever(terminal.processRefund(any(), any(), any())).thenAnswer { cancelable } + val job = launch { + action.processRefund(mock(), mock()).collect { } + } + + job.cancel() + joinAll(job) + + verify(cancelable).cancel(any()) + } +} From 75a4124c360689b1049359b5b0ced3698bbed937 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 20:22:59 +0800 Subject: [PATCH 21/39] Replace deprecated refund methods with processRefund in TerminalWrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../cardreader/internal/wrappers/TerminalWrapper.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) 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 44dcb11d9d8b..59e644e201ef 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 @@ -87,16 +87,11 @@ internal class TerminalWrapper { fun cancelPayment(paymentIntent: PaymentIntent, callback: PaymentIntentCallback) = Terminal.getInstance().cancelPaymentIntent(paymentIntent, callback) - @Suppress("DEPRECATION") - fun refundPayment( + fun processRefund( refundParameters: RefundParameters, refundConfiguration: CollectRefundConfiguration, - callback: Callback - ) = Terminal.getInstance().collectRefundPaymentMethod(refundParameters, refundConfiguration, callback) - - @Suppress("DEPRECATION") - fun processRefund(callback: RefundCallback) = - Terminal.getInstance().confirmRefund(callback) + callback: RefundCallback + ): Cancelable = Terminal.getInstance().processRefund(refundParameters, refundConfiguration, callback) fun installSoftwareUpdate() = Terminal.getInstance().installAvailableUpdate() From 3c178ca638418232bf9867081b52bf60b7ee78d3 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 20:23:09 +0800 Subject: [PATCH 22/39] Migrate InteracRefundManager to single-call processRefund API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../cardreader/CardReaderManagerFactory.kt | 6 +- .../internal/payments/InteracRefundManager.kt | 36 ++-------- .../payments/InteracRefundManagerTest.kt | 68 +++++++------------ 3 files changed, 32 insertions(+), 78 deletions(-) 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 7be658f54a60..1067b93d7847 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,10 @@ 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.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 @@ -69,8 +68,7 @@ object CardReaderManagerFactory { cardReaderConfigFactory ), InteracRefundManager( - CollectInteracRefundAction(terminal), - ProcessInteracRefundAction(terminal), + ProcessRefundAction(terminal), RefundErrorMapper(), paymentUtils, ), 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..cc299114a8ef 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,37 +18,16 @@ 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( + 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)) - } - } - } - } - - private suspend fun FlowCollector.processInteracRefund(refundParameters: RefundParams) { - emit(CardInteracRefundStatus.ProcessingInteracRefund) - processInteracRefundAction.processRefund().collect { status -> + ).collect { status -> when (status) { - is ProcessInteracRefundAction.ProcessRefundStatus.Success -> { + is ProcessRefundAction.ProcessRefundStatus.Success -> { emit(CardInteracRefundStatus.InteracRefundSuccess) } - is ProcessInteracRefundAction.ProcessRefundStatus.Failure -> { + is ProcessRefundAction.ProcessRefundStatus.Failure -> { emit(refundErrorMapper.mapTerminalError(refundParameters, status.exception)) } } 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 f7cdfa9dfbd3..7f1879c20e0c 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 @@ -24,10 +23,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 @@ -43,22 +39,19 @@ private const val TIMEOUT = 1000L 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, ) @@ -73,34 +66,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(flow { emit(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(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(Generic, "", mock()) @@ -111,11 +91,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(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(Generic, expectedErrorMessage, mock()) @@ -129,11 +109,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(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(expectedErrorType, "Declined", mock()) @@ -147,11 +127,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(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure( @@ -169,15 +149,15 @@ 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(flow { emit(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 = From df9e84c8d6e69c3f91b85ab204f51172b3532a7c Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 20:23:18 +0800 Subject: [PATCH 23/39] Remove deprecated two-step refund actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../actions/CollectInteracRefundAction.kt | 53 ---------- .../actions/ProcessInteracRefundAction.kt | 33 ------- .../actions/CollectInteracRefundActionTest.kt | 98 ------------------- .../actions/ProcessInteracRefundActionTest.kt | 79 --------------- 4 files changed, 263 deletions(-) delete mode 100644 libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundAction.kt delete mode 100644 libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundAction.kt delete mode 100644 libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundActionTest.kt delete mode 100644 libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundActionTest.kt 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 1b9a0bf2b198..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.CollectRefundConfiguration -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: CollectRefundConfiguration - ): 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/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/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 3e6dd6b08ad3..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectInteracRefundActionTest.kt +++ /dev/null @@ -1,98 +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 - -@Suppress("DoNotMockDataClass") -@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/ProcessInteracRefundActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundActionTest.kt deleted file mode 100644 index 83ff9c4318b7..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessInteracRefundActionTest.kt +++ /dev/null @@ -1,79 +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 - -@Suppress("DoNotMockDataClass") -@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) - } -} From 33bd653337d304ed20df428e163a5829d20333c4 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 21:30:45 +0800 Subject: [PATCH 24/39] Add ProcessPaymentIntentAction for single-call payment API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../actions/ProcessPaymentIntentAction.kt | 60 +++++++++++++++++++ .../internal/wrappers/TerminalWrapper.kt | 14 +++-- 2 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentIntentAction.kt 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..9e482b53c827 --- /dev/null +++ b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentIntentAction.kt @@ -0,0 +1,60 @@ +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.ProcessPaymentIntentAction.ProcessPaymentIntentStatus.Failure +import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction.ProcessPaymentIntentStatus.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 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() + } + + fun processPaymentIntent(paymentIntent: PaymentIntent): Flow { + return callbackFlow { + logWrapper.d(LOG_TAG, "Processing payment intent") + val cancelable = terminal.processPaymentIntent( + paymentIntent, + object : PaymentIntentCallback { + override fun onSuccess(paymentIntent: PaymentIntent) { + logWrapper.d(LOG_TAG, "Processing payment intent succeeded") + this@callbackFlow.sendAndLog(Success(paymentIntent), logWrapper) + this@callbackFlow.close() + } + + override fun onFailure(e: TerminalException) { + logWrapper.e( + LOG_TAG, + "Processing payment intent 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) = Unit + + override fun onSuccess() = Unit +} 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 59e644e201ef..24f9338612f8 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 @@ -11,7 +11,9 @@ 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 @@ -76,13 +78,15 @@ internal class TerminalWrapper { fun createPaymentIntent(params: PaymentIntentParameters, callback: PaymentIntentCallback) = Terminal.getInstance().createPaymentIntent(params, callback) - fun collectPaymentMethod( + fun processPaymentIntent( paymentIntent: PaymentIntent, callback: PaymentIntentCallback - ): Cancelable = Terminal.getInstance().collectPaymentMethod(paymentIntent, callback) - - fun processPayment(paymentIntent: PaymentIntent, callback: PaymentIntentCallback): Cancelable = - Terminal.getInstance().confirmPaymentIntent(paymentIntent, callback) + ): Cancelable = Terminal.getInstance().processPaymentIntent( + paymentIntent, + CollectPaymentIntentConfiguration.Builder().build(), + ConfirmPaymentIntentConfiguration.Builder().build(), + callback + ) fun cancelPayment(paymentIntent: PaymentIntent, callback: PaymentIntentCallback) = Terminal.getInstance().cancelPaymentIntent(paymentIntent, callback) From 5584551931a23f1e6244fec2046087cf16596c12 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 21:31:02 +0800 Subject: [PATCH 25/39] Update PaymentManager to use single-call processPaymentIntent API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../cardreader/CardReaderManagerFactory.kt | 6 +- .../internal/payments/PaymentManager.kt | 46 +++--- .../internal/payments/PaymentManagerTest.kt | 138 +++++------------- 3 files changed, 53 insertions(+), 137 deletions(-) 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 1067b93d7847..0ce5f3fcd605 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,9 +18,8 @@ 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.CollectPaymentAction import com.woocommerce.android.cardreader.internal.payments.actions.CreatePaymentAction -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 @@ -60,8 +59,7 @@ object CardReaderManagerFactory { cardReaderConfigFactory, paymentUtils, ), - CollectPaymentAction(terminal, logWrapper), - ProcessPaymentAction(terminal, logWrapper), + ProcessPaymentIntentAction(terminal, logWrapper), CancelPaymentAction(terminal), paymentUtils, PaymentErrorMapper(), 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..11f6b379258d 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) } @@ -125,29 +127,15 @@ internal class PaymentManager( 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 { + processPaymentIntentAction.processPaymentIntent(paymentIntent).collect { when (it) { - is ProcessPaymentStatus.Failure -> emit(errorMapper.mapTerminalError(paymentIntent, it.exception)) - is ProcessPaymentStatus.Success -> { + is ProcessPaymentIntentStatus.Failure -> emit(errorMapper.mapTerminalError(paymentIntent, it.exception)) + is ProcessPaymentIntentStatus.Success -> { val paymentMethodType = determinePaymentMethodType(it) emit(ProcessingPaymentCompleted(paymentMethodType)) result = it.paymentIntent @@ -188,7 +176,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/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 2feacd636ead..8283f2f24c74 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 @@ -76,8 +73,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { 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() @@ -85,7 +81,6 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { private val expectedSequence = listOf( InitializingPayment::class, - CollectingPayment::class, ProcessingPayment::class, ProcessingPaymentCompleted::class, CapturingPayment::class, @@ -98,8 +93,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { terminalWrapper, cardReaderStore, createPaymentAction, - collectPaymentAction, - processPaymentAction, + processPaymentIntentAction, cancelPaymentAction, paymentUtils, paymentErrorMapper, @@ -113,11 +107,8 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { } ) - whenever(collectPaymentAction.collectPayment(anyOrNull())) - .thenReturn(flow { emit(CollectPaymentStatus.Success(createPaymentIntent(REQUIRES_CONFIRMATION))) }) - - whenever(processPaymentAction.processPayment(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentStatus.Success(createPaymentIntent(REQUIRES_CAPTURE))) }) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(createPaymentIntent(REQUIRES_CAPTURE))) }) whenever(cardReaderStore.capturePaymentIntent(any(), anyString())) .thenReturn(CapturePaymentResponse.Successful.Success) @@ -133,7 +124,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) @@ -160,8 +150,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()) @@ -204,60 +192,10 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { .toList() } - assertThat(result).isNotNull // verify the flow did not timeout - verify(collectPaymentAction, never()).collectPayment(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()) + assertThat(result).isNotNull + verify(processPaymentIntentAction, never()).processPaymentIntent(anyOrNull()) } - // END - Collecting Payment - // BEGIN - Processing Payment @Test fun `when processing payment starts, then ProcessingPayment is emitted`() = testBlocking { val result = manager.acceptPayment(createPaymentInfo()) @@ -268,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(flow { emit(ProcessPaymentIntentStatus.Failure(mock())) }) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -279,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(flow { emit(ProcessPaymentIntentStatus.Failure(mock())) }) manager .acceptPayment(createPaymentInfo()).toList() @@ -291,8 +229,8 @@ 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))) }) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(createPaymentIntent(CANCELED))) }) val result = withTimeoutOrNull(TIMEOUT) { manager @@ -300,18 +238,18 @@ 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())) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) .thenReturn( flow { emit( - ProcessPaymentStatus.Success( + ProcessPaymentIntentStatus.Success( createPaymentIntent(SUCCEEDED, interacPresentDetails = mock()) ) ) @@ -327,11 +265,11 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given interac payment, when processing payment finishes with canceled status, then flow terminates`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) .thenReturn( flow { emit( - ProcessPaymentStatus.Success( + ProcessPaymentIntentStatus.Success( createPaymentIntent(CANCELED, interacPresentDetails = mock()) ) ) @@ -344,7 +282,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { .toList() } - assertThat(result).isNotNull // verify the flow did not timeout + assertThat(result).isNotNull verify(cardReaderStore, never()).capturePaymentIntent(any(), anyString()) } @@ -361,8 +299,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(flow { emit(ProcessPaymentIntentStatus.Success(intent)) }) val result = manager.acceptPayment(createPaymentInfo()).toList() @@ -382,8 +320,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(flow { emit(ProcessPaymentIntentStatus.Success(intent)) }) val result = manager.acceptPayment(createPaymentInfo()).toList() @@ -393,23 +331,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))) }) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) + .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(createPaymentIntent(REQUIRES_CAPTURE))) }) 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())) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) .thenReturn( flow { emit( - ProcessPaymentStatus.Success( + ProcessPaymentIntentStatus.Success( createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) ) ) @@ -424,11 +360,11 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when receiptUrl is empty, then PaymentData for retry are empty`() = testBlocking { - whenever(processPaymentAction.processPayment(anyOrNull())) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) .thenReturn( flow { emit( - ProcessPaymentStatus.Success( + ProcessPaymentIntentStatus.Success( createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) ) ) @@ -460,11 +396,11 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when capturing payment succeeds, then PaymentCompleted event contains receipt url`() = testBlocking { val expectedReceiptUrl = "abcd" - whenever(processPaymentAction.processPayment(anyOrNull())) + whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) .thenReturn( flow { emit( - ProcessPaymentStatus.Success( + ProcessPaymentIntentStatus.Success( createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = expectedReceiptUrl) ) ) @@ -509,11 +445,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) @@ -522,7 +456,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 @@ -574,9 +508,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 { @@ -609,7 +541,6 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { verify(cancelPaymentAction, never()).cancelPayment(paymentIntent) } - // END - Cancel private fun createPaymentIntent( status: PaymentIntentStatus, @@ -628,7 +559,6 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { 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) { From a07a792c4e71c6fa520102465475cc5bc6d019c9 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 21:31:13 +0800 Subject: [PATCH 26/39] Remove CollectingPayment from CardPaymentStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../woocommerce/android/cardreader/payments/CardPaymentStatus.kt | 1 - 1 file changed, 1 deletion(-) 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() From 413a152b46ec2ac71eba7858521064f48c35bef3 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 21:31:27 +0800 Subject: [PATCH 27/39] Remove CollectingPayment UI state and consolidate into ProcessingPayment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../android/di/MockCardReaderManagerModule.kt | 2 +- ...CardReaderPaymentStateToViewStateMapper.kt | 16 ---- .../controller/CardReaderPaymentController.kt | 13 +--- .../CardReaderPaymentOrRefundState.kt | 26 ++----- .../CardReaderPaymentStateProvider.kt | 16 ---- .../CardReaderTrackCanceledFlowAction.kt | 1 - .../totals/WooPosTotalsAnalyticsTracker.kt | 6 +- .../home/totals/WooPosTotalsViewModel.kt | 13 ++-- .../CardReaderPaymentViewModelTest.kt | 27 ++++--- .../CardReaderPaymentControllerTest.kt | 76 ++++++++----------- .../home/totals/WooPosTotalsViewModelTest.kt | 42 +++++----- 11 files changed, 81 insertions(+), 157 deletions(-) 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 a00470a8384c..78750dd8f2c9 100644 --- a/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt +++ b/WooCommerce/src/androidTest/kotlin/com/woocommerce/android/di/MockCardReaderManagerModule.kt @@ -85,7 +85,7 @@ class MockCardReaderManagerModule { 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, 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..5165f693c635 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( 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/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 4c12d5d66cf2..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 @@ -380,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 -> { @@ -412,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) } @@ -428,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/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..542041f6b70c 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 @@ -262,7 +261,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -282,7 +281,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -301,7 +300,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -320,7 +319,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -340,7 +339,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -361,7 +360,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -382,7 +381,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -403,7 +402,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -424,7 +423,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { } whenever(cardReaderManager.collectPayment(any())).thenAnswer { - flow { emit(CollectingPayment) } + flow { emit(ProcessingPayment) } } viewModel.start() @@ -532,7 +531,7 @@ 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() @@ -544,7 +543,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) @@ -1511,7 +1510,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 +1526,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() 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/totals/WooPosTotalsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/totals/WooPosTotalsViewModelTest.kt index 85923257f711..687546230094 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.ProcessingPayment.ExternalReaderProcessingPayment("", {}) 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) @@ -871,13 +871,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("", {}) advanceUntilIdle() // THEN @@ -916,11 +916,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 +957,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 +999,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 +1038,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, {} ) @@ -1269,13 +1269,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("", {}) advanceUntilIdle() vm.onUIEvent(OnBackClicked) @@ -1295,7 +1295,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 +1313,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 +1321,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 +1346,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, {} ) From 31efce39ec63d0903c18e397992faff380d72340 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 2 Dec 2025 21:31:40 +0800 Subject: [PATCH 28/39] Remove deprecated two-step payment actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../payments/actions/CollectPaymentAction.kt | 56 --------- .../payments/actions/ProcessPaymentAction.kt | 60 ---------- .../actions/CollectPaymentActionTest.kt | 110 ------------------ .../actions/ProcessPaymentActionTest.kt | 90 -------------- 4 files changed, 316 deletions(-) delete mode 100644 libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentAction.kt delete mode 100644 libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentAction.kt delete mode 100644 libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentActionTest.kt delete mode 100644 libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentActionTest.kt 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/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/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 1dec929f7793..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/CollectPaymentActionTest.kt +++ /dev/null @@ -1,110 +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 - -@Suppress("DoNotMockDataClass") -@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/ProcessPaymentActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentActionTest.kt deleted file mode 100644 index aa75648b2e01..000000000000 --- a/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentActionTest.kt +++ /dev/null @@ -1,90 +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 - -@Suppress("DoNotMockDataClass") -@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) - } -} From 1dae70270f8321b7bd914bbf6bda1b22292d5c0c Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 09:48:35 +0800 Subject: [PATCH 29/39] Fix ViewState to support dynamic hint labels for processing payment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ...CardReaderPaymentStateToViewStateMapper.kt | 4 ++- .../payment/CardReaderPaymentViewState.kt | 6 ++-- .../CardReaderPaymentViewModelTest.kt | 30 +++++++++---------- 3 files changed, 20 insertions(+), 20 deletions(-) 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 5165f693c635..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 @@ -87,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/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/payments/cardreader/CardReaderPaymentViewModelTest.kt index 542041f6b70c..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 @@ -64,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 @@ -267,7 +265,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -286,7 +284,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -305,7 +303,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -324,7 +322,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -345,7 +343,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -366,7 +364,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -387,7 +385,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -408,7 +406,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -429,7 +427,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { 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) } @@ -536,7 +534,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { viewModel.start() - assertThat(viewModel.viewStateData.value).isInstanceOf(ExternalReaderCollectPaymentState::class.java) + assertThat(viewModel.viewStateData.value).isInstanceOf(ExternalReaderProcessingPaymentState::class.java) } @Test @@ -549,7 +547,7 @@ class CardReaderPaymentViewModelTest : BaseUnitTest() { viewModel.start() - assertThat(viewModel.viewStateData.value).isInstanceOf(BuiltInReaderCollectPaymentState::class.java) + assertThat(viewModel.viewStateData.value).isInstanceOf(BuiltInReaderProcessingPaymentState::class.java) } @Test @@ -1571,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 From 04f834b101b88896c3a35732b6089a60196a2266 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 15:23:48 +0800 Subject: [PATCH 30/39] Convert TerminalWrapper to use suspend functions from stripeterminal-ktx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../cardreader/internal/FlowExtensions.kt | 13 ---- .../internal/wrappers/TerminalWrapper.kt | 71 +++++++++---------- 2 files changed, 32 insertions(+), 52 deletions(-) delete mode 100644 libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/FlowExtensions.kt 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/wrappers/TerminalWrapper.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/wrappers/TerminalWrapper.kt index 24f9338612f8..610a2b1c5192 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,13 +3,7 @@ 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 @@ -19,6 +13,7 @@ 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.Refund import com.stripe.stripeterminal.external.models.RefundParameters import com.stripe.stripeterminal.external.models.SimulateReaderUpdate import com.stripe.stripeterminal.external.models.SimulatedCard @@ -26,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.discoverReaders +import com.stripe.stripeterminal.ktx.disconnectReader +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. @@ -50,52 +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 processPaymentIntent( - paymentIntent: PaymentIntent, - callback: PaymentIntentCallback - ): Cancelable = Terminal.getInstance().processPaymentIntent( - paymentIntent, - CollectPaymentIntentConfiguration.Builder().build(), - ConfirmPaymentIntentConfiguration.Builder().build(), - callback - ) + suspend fun createPaymentIntent(params: PaymentIntentParameters): PaymentIntent = + Terminal.getInstance().createPaymentIntent(params, null) + + 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 processRefund( + suspend fun processRefund( refundParameters: RefundParameters, - refundConfiguration: CollectRefundConfiguration, - callback: RefundCallback - ): Cancelable = Terminal.getInstance().processRefund(refundParameters, refundConfiguration, callback) + refundConfiguration: CollectRefundConfiguration + ): Refund = Terminal.getInstance().processRefund(refundParameters, refundConfiguration) fun installSoftwareUpdate() = Terminal.getInstance().installAvailableUpdate() From dad1bfab3f0ed574a6571ca9bbbcaf07ec236b3d Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 15:26:17 +0800 Subject: [PATCH 31/39] Convert payment actions to use suspend functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../internal/payments/PaymentManager.kt | 30 ++++++------ .../payments/actions/CancelPaymentAction.kt | 14 +----- .../payments/actions/CreatePaymentAction.kt | 32 +++--------- .../actions/ProcessPaymentIntentAction.kt | 49 +++++-------------- 4 files changed, 35 insertions(+), 90 deletions(-) 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 11f6b379258d..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 @@ -116,33 +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.processPayment( paymentIntent: PaymentIntent ): PaymentIntent { - var result = paymentIntent emit(ProcessingPayment) - processPaymentIntentAction.processPaymentIntent(paymentIntent).collect { - when (it) { - is ProcessPaymentIntentStatus.Failure -> emit(errorMapper.mapTerminalError(paymentIntent, it.exception)) - is ProcessPaymentIntentStatus.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 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/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/ProcessPaymentIntentAction.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessPaymentIntentAction.kt index 9e482b53c827..3defdad5ee5b 100644 --- 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 @@ -1,18 +1,12 @@ 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.ProcessPaymentIntentAction.ProcessPaymentIntentStatus.Failure import com.woocommerce.android.cardreader.internal.payments.actions.ProcessPaymentIntentAction.ProcessPaymentIntentStatus.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 ProcessPaymentIntentAction( private val terminal: TerminalWrapper, @@ -23,38 +17,19 @@ internal class ProcessPaymentIntentAction( data class Failure(val exception: TerminalException) : ProcessPaymentIntentStatus() } - fun processPaymentIntent(paymentIntent: PaymentIntent): Flow { - return callbackFlow { - logWrapper.d(LOG_TAG, "Processing payment intent") - val cancelable = terminal.processPaymentIntent( - paymentIntent, - object : PaymentIntentCallback { - override fun onSuccess(paymentIntent: PaymentIntent) { - logWrapper.d(LOG_TAG, "Processing payment intent succeeded") - this@callbackFlow.sendAndLog(Success(paymentIntent), logWrapper) - this@callbackFlow.close() - } - - override fun onFailure(e: TerminalException) { - logWrapper.e( - LOG_TAG, - "Processing payment intent failed. " + - "Message: ${e.errorMessage}, DeclineCode: ${e.apiError?.declineCode}" - ) - this@callbackFlow.sendAndLog(Failure(e), logWrapper) - this@callbackFlow.close() - } - } + 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}" ) - awaitClose { - if (!cancelable.isCompleted) cancelable.cancel(noop) - } + Failure(e) } } } - -private val noop = object : Callback { - override fun onFailure(e: TerminalException) = Unit - - override fun onSuccess() = Unit -} From 9ffa362083de5fc1aa597b78bd68e9ec06f8fb8a Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 15:27:14 +0800 Subject: [PATCH 32/39] Convert ProcessRefundAction to use suspend function --- .../internal/payments/InteracRefundManager.kt | 17 ++++---- .../payments/actions/ProcessRefundAction.kt | 43 +++---------------- 2 files changed, 15 insertions(+), 45 deletions(-) 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 cc299114a8ef..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 @@ -19,17 +19,16 @@ internal class InteracRefundManager( refundConfig: RefundConfig, ): Flow = flow { emit(CardInteracRefundStatus.CollectingInteracRefund) - processRefundAction.processRefund( + val status = processRefundAction.processRefund( refundParameters.toStripeRefundParameters(paymentsUtils), refundConfig.toStripeRefundConfiguration() - ).collect { status -> - when (status) { - is ProcessRefundAction.ProcessRefundStatus.Success -> { - emit(CardInteracRefundStatus.InteracRefundSuccess) - } - is ProcessRefundAction.ProcessRefundStatus.Failure -> { - emit(refundErrorMapper.mapTerminalError(refundParameters, status.exception)) - } + ) + when (status) { + is ProcessRefundAction.ProcessRefundStatus.Success -> { + emit(CardInteracRefundStatus.InteracRefundSuccess) + } + is ProcessRefundAction.ProcessRefundStatus.Failure -> { + emit(refundErrorMapper.mapTerminalError(refundParameters, status.exception)) } } } 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 index 534dd62c7696..2ef4de3e55bb 100644 --- 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 @@ -1,15 +1,10 @@ package com.woocommerce.android.cardreader.internal.payments.actions -import com.stripe.stripeterminal.external.callable.Callback -import com.stripe.stripeterminal.external.callable.RefundCallback 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 -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow internal class ProcessRefundAction(private val terminal: TerminalWrapper) { sealed class ProcessRefundStatus { @@ -17,39 +12,15 @@ internal class ProcessRefundAction(private val terminal: TerminalWrapper) { data class Failure(val exception: TerminalException) : ProcessRefundStatus() } - fun processRefund( + suspend fun processRefund( refundParameters: RefundParameters, refundConfiguration: CollectRefundConfiguration - ): Flow { - return callbackFlow { - val cancelable = terminal.processRefund( - refundParameters, - refundConfiguration, - object : RefundCallback { - override fun onSuccess(refund: Refund) { - trySend(ProcessRefundStatus.Success(refund)) - close() - } - - override fun onFailure(e: TerminalException) { - trySend(ProcessRefundStatus.Failure(e)) - close() - } - } - ) - awaitClose { - if (!cancelable.isCompleted) cancelable.cancel(noop) - } + ): ProcessRefundStatus { + return try { + val refund = terminal.processRefund(refundParameters, refundConfiguration) + ProcessRefundStatus.Success(refund) + } catch (e: TerminalException) { + ProcessRefundStatus.Failure(e) } } } - -private val noop = object : Callback { - override fun onFailure(e: TerminalException) { - // noop - } - - override fun onSuccess() { - // noop - } -} From 4f70a4ee9f56bec84985ca571c80c45d23314595 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 15:28:25 +0800 Subject: [PATCH 33/39] Convert DiscoverReadersAction and ConnectionManager to use suspend/Flow --- .../android/cardreader/CardReaderManager.kt | 2 +- .../internal/CardReaderManagerImpl.kt | 2 +- .../internal/connection/ConnectionManager.kt | 77 ++++++++----------- .../actions/DiscoverReadersAction.kt | 61 ++++++--------- 4 files changed, 56 insertions(+), 86 deletions(-) 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 4f11159f8b89..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,7 +45,7 @@ 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() 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 0d555d206001..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) } 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 4f3526e2961b..dd056f35200e 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 @@ -32,8 +31,6 @@ 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 @@ -124,43 +121,35 @@ 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() = suspendCoroutine { continuation -> - terminal.disconnectReader(object : Callback { - override fun onFailure(e: TerminalException) { - updateReaderStatus(CardReaderStatus.NotConnected()) - continuation.resume(false) - } - - override fun onSuccess() { - updateReaderStatus(CardReaderStatus.NotConnected()) - continuation.resume(true) - } - }) + suspend fun disconnectReader(): Boolean { + return try { + terminal.disconnectReader() + updateReaderStatus(CardReaderStatus.NotConnected()) + true + } catch (e: TerminalException) { + updateReaderStatus(CardReaderStatus.NotConnected()) + false + } } fun cancelReconnection() { @@ -213,31 +202,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/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() {} -} From 7c577fed553fc52eda40a8f765071f7bf62b96c3 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 15:28:55 +0800 Subject: [PATCH 34/39] Update unit tests for suspend function signatures --- .../internal/connection/ConnectionManager.kt | 2 +- .../internal/wrappers/TerminalWrapper.kt | 2 +- .../connection/ConnectionManagerTest.kt | 35 +--- .../actions/DiscoverReadersActionTest.kt | 174 ++++++------------ .../payments/InteracRefundManagerTest.kt | 15 +- .../internal/payments/PaymentManagerTest.kt | 92 ++++----- .../actions/CreatePaymentActionTest.kt | 172 ++++++----------- .../actions/ProcessRefundActionTest.kt | 73 +------- 8 files changed, 161 insertions(+), 404 deletions(-) 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 dd056f35200e..5d5716c8deb7 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 @@ -146,7 +146,7 @@ internal class ConnectionManager( terminal.disconnectReader() updateReaderStatus(CardReaderStatus.NotConnected()) true - } catch (e: TerminalException) { + } catch (@Suppress("SwallowedException") e: TerminalException) { updateReaderStatus(CardReaderStatus.NotConnected()) false } 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 610a2b1c5192..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 @@ -24,8 +24,8 @@ 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.discoverReaders 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 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..2d33714e3d1a 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,8 +1,6 @@ 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 @@ -60,7 +58,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, @@ -246,9 +243,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 +265,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 +292,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 +313,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 +325,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 +334,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 +343,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 +352,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 7f1879c20e0c..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 @@ -10,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 @@ -69,7 +68,7 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { fun `given process refund success, when refund starts, then InteracRefundSuccess is emitted`() = testBlocking { whenever(processRefundAction.processRefund(any(), any())) - .thenReturn(flow { emit(ProcessRefundAction.ProcessRefundStatus.Success(mock())) }) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Success(mock())) val result = manager.refundInteracPayment(createRefundParams(), refundConfig) .takeUntil(CardInteracRefundStatus.InteracRefundSuccess::class).toList() @@ -80,7 +79,7 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { fun `given process refund failure, when refund starts, then failure is emitted`() = testBlocking { whenever(processRefundAction.processRefund(any(), any())) - .thenReturn(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(Generic, "", mock()) @@ -95,7 +94,7 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { testBlocking { val expectedErrorMessage = "Generic Error" whenever(processRefundAction.processRefund(any(), any())) - .thenReturn(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(Generic, expectedErrorMessage, mock()) @@ -113,7 +112,7 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { testBlocking { val expectedErrorType = DeclinedByBackendError.Unknown whenever(processRefundAction.processRefund(any(), any())) - .thenReturn(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure(expectedErrorType, "Declined", mock()) @@ -131,7 +130,7 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { testBlocking { val expectedRefundParams = createRefundParams() whenever(processRefundAction.processRefund(any(), any())) - .thenReturn(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) whenever(refundErrorMapper.mapTerminalError(any(), any())) .thenReturn( CardInteracRefundStatus.InteracRefundFailure( @@ -152,7 +151,7 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { fun `given process refund failure, when refund starts, then flow terminates`() = testBlocking { whenever(processRefundAction.processRefund(any(), any())) - .thenReturn(flow { emit(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) }) + .thenReturn(ProcessRefundAction.ProcessRefundStatus.Failure(mock())) val result = withTimeoutOrNull(TIMEOUT) { manager.refundInteracPayment(createRefundParams(), refundConfig).toList() } @@ -162,8 +161,6 @@ class InteracRefundManagerTest : CardReaderBaseUnitTest() { 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 8283f2f24c74..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 @@ -34,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 @@ -100,15 +99,15 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { cardReaderConfigFactory, ) whenever(terminalWrapper.isInitialized()).thenReturn(true) + + val createPaymentIntentResponse = createPaymentIntent(REQUIRES_PAYMENT_METHOD) + val processPaymentIntentResponse = createPaymentIntent(REQUIRES_CAPTURE) + whenever(createPaymentAction.createPaymentIntent(any())) - .thenReturn( - flow { - emit(CreatePaymentStatus.Success(createPaymentIntent(REQUIRES_PAYMENT_METHOD))) - } - ) + .thenReturn(CreatePaymentStatus.Success(createPaymentIntentResponse)) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(createPaymentIntent(REQUIRES_CAPTURE))) }) + .thenReturn(ProcessPaymentIntentStatus.Success(processPaymentIntentResponse)) whenever(cardReaderStore.capturePaymentIntent(any(), anyString())) .thenReturn(CapturePaymentResponse.Successful.Success) @@ -161,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() @@ -172,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() @@ -183,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 @@ -207,7 +207,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when processing payment fails, then error emitted`() = testBlocking { whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentIntentStatus.Failure(mock())) }) + .thenReturn(ProcessPaymentIntentStatus.Failure(mock())) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -218,7 +218,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when processing payment fails, then mapTerminalError invoked`() = testBlocking { whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentIntentStatus.Failure(mock())) }) + .thenReturn(ProcessPaymentIntentStatus.Failure(mock())) manager .acceptPayment(createPaymentInfo()).toList() @@ -229,8 +229,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given status not REQUIRES_CAPTURE, when processing payment finishes, then flow terminates`() = testBlocking { + val canceledIntent = createPaymentIntent(CANCELED) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(createPaymentIntent(CANCELED))) }) + .thenReturn(ProcessPaymentIntentStatus.Success(canceledIntent)) val result = withTimeoutOrNull(TIMEOUT) { manager @@ -245,16 +246,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given interac payment, when processing payment finishes successfully, then capture payment is emitted`() = testBlocking { + val succeededInteracIntent = createPaymentIntent(SUCCEEDED, interacPresentDetails = mock()) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentIntentStatus.Success( - createPaymentIntent(SUCCEEDED, interacPresentDetails = mock()) - ) - ) - } - ) + .thenReturn(ProcessPaymentIntentStatus.Success(succeededInteracIntent)) val result = manager .acceptPayment(createPaymentInfo()).takeUntil(CapturingPayment::class).toList() @@ -265,16 +259,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given interac payment, when processing payment finishes with canceled status, then flow terminates`() = testBlocking { + val canceledInteracIntent = createPaymentIntent(CANCELED, interacPresentDetails = mock()) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentIntentStatus.Success( - createPaymentIntent(CANCELED, interacPresentDetails = mock()) - ) - ) - } - ) + .thenReturn(ProcessPaymentIntentStatus.Success(canceledInteracIntent)) val result = withTimeoutOrNull(TIMEOUT) { manager @@ -300,7 +287,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { val charges = listOf(charge) whenever(intent.getCharges()).thenReturn(charges) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(intent)) }) + .thenReturn(ProcessPaymentIntentStatus.Success(intent)) val result = manager.acceptPayment(createPaymentInfo()).toList() @@ -321,7 +308,7 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { val charges = listOf(charge) whenever(intent.getCharges()).thenReturn(charges) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(intent)) }) + .thenReturn(ProcessPaymentIntentStatus.Success(intent)) val result = manager.acceptPayment(createPaymentInfo()).toList() @@ -331,8 +318,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `given processing payment suc with unknown, when processing, then ProcessingPaymentCompleted emitted`() = testBlocking { + val captureIntent = createPaymentIntent(REQUIRES_CAPTURE) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn(flow { emit(ProcessPaymentIntentStatus.Success(createPaymentIntent(REQUIRES_CAPTURE))) }) + .thenReturn(ProcessPaymentIntentStatus.Success(captureIntent)) val result = manager.acceptPayment(createPaymentInfo()).toList() @@ -341,15 +329,10 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when receiptUrl is empty, then PaymentFailed emitted`() = testBlocking { + val noReceiptIntent = createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) .thenReturn( - flow { - emit( - ProcessPaymentIntentStatus.Success( - createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) - ) - ) - } + ProcessPaymentIntentStatus.Success(noReceiptIntent) ) val result = manager @@ -360,16 +343,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when receiptUrl is empty, then PaymentData for retry are empty`() = testBlocking { + val noReceiptIntent = createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentIntentStatus.Success( - createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = null) - ) - ) - } - ) + .thenReturn(ProcessPaymentIntentStatus.Success(noReceiptIntent)) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -396,16 +372,9 @@ class PaymentManagerTest : CardReaderBaseUnitTest() { @Test fun `when capturing payment succeeds, then PaymentCompleted event contains receipt url`() = testBlocking { val expectedReceiptUrl = "abcd" + val intentWithReceipt = createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = expectedReceiptUrl) whenever(processPaymentIntentAction.processPaymentIntent(anyOrNull())) - .thenReturn( - flow { - emit( - ProcessPaymentIntentStatus.Success( - createPaymentIntent(REQUIRES_CAPTURE, receiptUrl = expectedReceiptUrl) - ) - ) - } - ) + .thenReturn(ProcessPaymentIntentStatus.Success(intentWithReceipt)) val result = manager .acceptPayment(createPaymentInfo()).toList() @@ -550,10 +519,11 @@ 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)) } 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 27a192e91c97..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 @@ -42,7 +40,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { private val paymentUtils: PaymentUtils = mock() @Before - fun setUp() { + fun setUp() = testBlocking { action = CreatePaymentAction( paymentIntentParametersFactory, terminal, @@ -55,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) } @@ -90,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 { @@ -131,7 +101,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { customerEmail = expectedEmail, wcpayCanSendReceipt = true ) - ).toList() + ) verify(intentParametersBuilder, never()).setReceiptEmail(any()) } @@ -146,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()) } @@ -169,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) } @@ -178,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()) } @@ -187,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()) } @@ -196,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()) } @@ -212,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) } @@ -220,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) @@ -234,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) @@ -248,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) @@ -262,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) @@ -276,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()) @@ -294,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) @@ -307,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) @@ -318,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() @@ -326,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) @@ -339,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 { @@ -392,7 +332,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { CardReaderConfigForCanada ) - action.createPaymentIntent(createPaymentInfo(countryCode = "CA")).toList() + action.createPaymentIntent(createPaymentInfo(countryCode = "CA")) verify(paymentIntentParametersFactory).createBuilder(expectedPaymentMethod) } @@ -406,7 +346,7 @@ internal class CreatePaymentActionTest : CardReaderBaseUnitTest() { CardReaderConfigForUSA ) - action.createPaymentIntent(createPaymentInfo(countryCode = "US")).toList() + action.createPaymentIntent(createPaymentInfo(countryCode = "US")) verify(paymentIntentParametersFactory).createBuilder(expectedPaymentMethod) } @@ -415,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") @@ -424,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/ProcessRefundActionTest.kt b/libs/cardreader/src/test/java/com/woocommerce/android/cardreader/internal/payments/actions/ProcessRefundActionTest.kt index d918a2962ec5..a37ef6d3a18b 100644 --- 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 @@ -1,23 +1,17 @@ package com.woocommerce.android.cardreader.internal.payments.actions -import com.stripe.stripeterminal.external.callable.Cancelable -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.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 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 @Suppress("DoNotMockDataClass") @@ -32,25 +26,19 @@ internal class ProcessRefundActionTest : CardReaderBaseUnitTest() { } @Test - fun `when process refund succeeds, then Success is emitted`() = testBlocking { - whenever(terminal.processRefund(any(), any(), any())).thenAnswer { - (it.arguments[2] as RefundCallback).onSuccess(mock()) - mock() - } + fun `when process refund succeeds, then Success is returned`() = testBlocking { + whenever(terminal.processRefund(any(), any())).thenReturn(mock()) - val result = action.processRefund(mock(), mock()).first() + val result = action.processRefund(mock(), mock()) assertThat(result).isExactlyInstanceOf(Success::class.java) } @Test - fun `when process refund fails, then Failure is emitted`() = testBlocking { - whenever(terminal.processRefund(any(), any(), any())).thenAnswer { - (it.arguments[2] as RefundCallback).onFailure(mock()) - mock() - } + fun `when process refund fails, then Failure is returned`() = testBlocking { + whenever(terminal.processRefund(any(), any())).thenAnswer { throw mock() } - val result = action.processRefund(mock(), mock()).first() + val result = action.processRefund(mock(), mock()) assertThat(result).isExactlyInstanceOf(Failure::class.java) } @@ -58,53 +46,10 @@ internal class ProcessRefundActionTest : CardReaderBaseUnitTest() { @Test fun `when process refund succeeds, then refund is returned`() = testBlocking { val refund = mock() - whenever(terminal.processRefund(any(), any(), any())).thenAnswer { - (it.arguments[2] as RefundCallback).onSuccess(refund) - mock() - } + whenever(terminal.processRefund(any(), any())).thenReturn(refund) - val result = action.processRefund(mock(), mock()).first() + val result = action.processRefund(mock(), mock()) assertThat((result as Success).refund).isEqualTo(refund) } - - @Test - fun `when process refund succeeds, then flow is terminated`() = testBlocking { - whenever(terminal.processRefund(any(), any(), any())).thenAnswer { - (it.arguments[2] as RefundCallback).onSuccess(mock()) - mock() - } - - val result = action.processRefund(mock(), mock()).toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when process refund fails, then flow is terminated`() = testBlocking { - whenever(terminal.processRefund(any(), any(), any())).thenAnswer { - (it.arguments[2] as RefundCallback).onFailure(mock()) - mock() - } - - val result = action.processRefund(mock(), mock()).toList() - - assertThat(result.size).isEqualTo(1) - } - - @Test - fun `when job canceled, then process refund gets canceled`() = - testBlocking { - val cancelable = mock() - whenever(cancelable.isCompleted).thenReturn(false) - whenever(terminal.processRefund(any(), any(), any())).thenAnswer { cancelable } - val job = launch { - action.processRefund(mock(), mock()).collect { } - } - - job.cancel() - joinAll(job) - - verify(cancelable).cancel(any()) - } } From 45b94ee93eec67e914a5b12d6748408a01dc9f1c Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 15:58:14 +0800 Subject: [PATCH 35/39] Add logging to ConnectionManager disconnect error handling --- .../android/cardreader/CardReaderManagerFactory.kt | 1 + .../cardreader/internal/connection/ConnectionManager.kt | 7 ++++++- .../internal/connection/ConnectionManagerTest.kt | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) 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 0ce5f3fcd605..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 @@ -77,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/internal/connection/ConnectionManager.kt b/libs/cardreader/src/main/java/com/woocommerce/android/cardreader/internal/connection/ConnectionManager.kt index 5d5716c8deb7..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 @@ -12,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 @@ -33,7 +34,9 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch 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, @@ -41,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 @@ -146,7 +150,8 @@ internal class ConnectionManager( terminal.disconnectReader() updateReaderStatus(CardReaderStatus.NotConnected()) true - } catch (@Suppress("SwallowedException") e: TerminalException) { + } catch (e: TerminalException) { + logWrapper.e(LOG_TAG, "Failed to disconnect reader: ${e.errorMessage}") updateReaderStatus(CardReaderStatus.NotConnected()) false } 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 2d33714e3d1a..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 @@ -5,6 +5,7 @@ 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 @@ -45,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( @@ -65,6 +67,7 @@ class ConnectionManagerTest : CardReaderBaseUnitTest() { discoverReadersAction, terminalListenerImpl, application, + logWrapper, ) } From 8e720dc1a643ae3c04c34d5b30e8d99609591b7f Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 3 Dec 2025 17:04:29 +0800 Subject: [PATCH 36/39] Update Stripe SDK to 5.0.0 From 54a45ddcff2d4f593036e7f0f78cdfcce57e69ac Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 4 Dec 2025 15:49:21 +0800 Subject: [PATCH 37/39] Fix WooPosTotalsViewModelTest for payment state handling Updated tests to match actual ViewModel behavior where ProcessingPayment keeps Checkout state with ReadyForPayment reader status, while PaymentCapturing triggers PaymentInProgress view state. --- .../home/totals/WooPosTotalsViewModelTest.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) 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 687546230094..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 @@ -782,7 +782,7 @@ class WooPosTotalsViewModelTest { val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) // WHEN - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) + paymentState.value = CardReaderPaymentState.PaymentCapturing.ExternalReaderPaymentCapturing("") advanceUntilIdle() // THEN @@ -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() @@ -881,12 +881,10 @@ class WooPosTotalsViewModelTest { 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 @@ -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() @@ -1275,7 +1273,7 @@ class WooPosTotalsViewModelTest { // WHEN val vm = createViewModelAndSetupForSuccessfulOrderCreation(controllerFactory = factory) - paymentState.value = CardReaderPaymentState.ProcessingPayment.ExternalReaderProcessingPayment("", {}) + paymentState.value = CardReaderPaymentState.PaymentCapturing.ExternalReaderPaymentCapturing("") advanceUntilIdle() vm.onUIEvent(OnBackClicked) From 9dc9f4c188c2e85a6b46f58a3f32e2659102fdac Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 4 Dec 2025 16:18:49 +0800 Subject: [PATCH 38/39] Fix MockCardReaderManagerModule suspend function override --- .../com/woocommerce/android/di/MockCardReaderManagerModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 78750dd8f2c9..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,7 +80,7 @@ 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 From d76dfc02f9d8777221e34316b2973549f267033c Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 5 Dec 2025 09:49:21 +0800 Subject: [PATCH 39/39] Update stripe-terminal version to 5.1.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3ef8270652a2..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 = '5.0.0' +stripe-terminal = '5.1.0' swiperefreshlayout = "1.1.0" tinder-statemachine = '0.2.0' volley = "1.2.1"