diff --git a/link/src/main/java/com/stripe/android/link/account/CookieStore.kt b/link/src/main/java/com/stripe/android/link/account/CookieStore.kt index db156780f60..4f2d7afad2a 100644 --- a/link/src/main/java/com/stripe/android/link/account/CookieStore.kt +++ b/link/src/main/java/com/stripe/android/link/account/CookieStore.kt @@ -15,7 +15,7 @@ class CookieStore @Inject internal constructor( private val store: EncryptedStore ) { - constructor(context: Context): this(EncryptedStore(context)) + constructor(context: Context) : this(EncryptedStore(context)) /** * Clear all local data. diff --git a/paymentsheet/api/paymentsheet.api b/paymentsheet/api/paymentsheet.api index 0e97ac163a9..edd0a6cc47d 100644 --- a/paymentsheet/api/paymentsheet.api +++ b/paymentsheet/api/paymentsheet.api @@ -878,11 +878,19 @@ public final class com/stripe/android/paymentsheet/addresselement/analytics/Defa } public final class com/stripe/android/paymentsheet/analytics/DefaultEventReporter_Factory : dagger/internal/Factory { - public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V - public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/analytics/DefaultEventReporter_Factory; + public fun (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)V + public static fun create (Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;Ljavax/inject/Provider;)Lcom/stripe/android/paymentsheet/analytics/DefaultEventReporter_Factory; public fun get ()Lcom/stripe/android/paymentsheet/analytics/DefaultEventReporter; public synthetic fun get ()Ljava/lang/Object; - public static fun newInstance (Lcom/stripe/android/paymentsheet/analytics/EventReporter$Mode;Lcom/stripe/android/core/networking/AnalyticsRequestExecutor;Lcom/stripe/android/networking/PaymentAnalyticsRequestFactory;Lkotlin/coroutines/CoroutineContext;)Lcom/stripe/android/paymentsheet/analytics/DefaultEventReporter; + public static fun newInstance (Lcom/stripe/android/paymentsheet/analytics/EventReporter$Mode;Lcom/stripe/android/core/networking/AnalyticsRequestExecutor;Lcom/stripe/android/networking/PaymentAnalyticsRequestFactory;Lcom/stripe/android/paymentsheet/analytics/EventTimeProvider;Lkotlin/coroutines/CoroutineContext;)Lcom/stripe/android/paymentsheet/analytics/DefaultEventReporter; +} + +public final class com/stripe/android/paymentsheet/analytics/EventTimeProvider_Factory : dagger/internal/Factory { + public fun ()V + public static fun create ()Lcom/stripe/android/paymentsheet/analytics/EventTimeProvider_Factory; + public fun get ()Lcom/stripe/android/paymentsheet/analytics/EventTimeProvider; + public synthetic fun get ()Ljava/lang/Object; + public static fun newInstance ()Lcom/stripe/android/paymentsheet/analytics/EventTimeProvider; } public final class com/stripe/android/paymentsheet/databinding/ActivityPaymentOptionsBinding : androidx/viewbinding/ViewBinding { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt index 3df2848c221..ee9040b60b6 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BaseAddPaymentMethodFragment.kt @@ -138,7 +138,10 @@ internal abstract class BaseAddPaymentMethodFragment : Fragment() { } } - sheetViewModel.eventReporter.onShowNewPaymentOptionForm() + sheetViewModel.eventReporter.onShowNewPaymentOptionForm( + linkEnabled = sheetViewModel.isLinkEnabled.value ?: false, + activeLinkSession = sheetViewModel.activeLinkSession.value ?: false + ) } private fun setupRecyclerView( diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BasePaymentMethodsListFragment.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BasePaymentMethodsListFragment.kt index 3787d01e4c2..166ba02144e 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BasePaymentMethodsListFragment.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/BasePaymentMethodsListFragment.kt @@ -54,7 +54,10 @@ internal abstract class BasePaymentMethodsListFragment( this.config = nullableConfig setHasOptionsMenu(!sheetViewModel.paymentMethods.value.isNullOrEmpty()) - sheetViewModel.eventReporter.onShowExistingPaymentOptions() + sheetViewModel.eventReporter.onShowExistingPaymentOptions( + linkEnabled = sheetViewModel.isLinkEnabled.value ?: false, + activeLinkSession = sheetViewModel.activeLinkSession.value ?: false + ) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt index 67781108a41..e9c256f450f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentOptionsViewModel.kt @@ -169,7 +169,8 @@ internal class PaymentOptionsViewModel @Inject constructor( stripeIntent.paymentMethodTypes.contains(PaymentMethod.Type.Link.code) ) { viewModelScope.launch { - when (linkLauncher.setup(stripeIntent, this)) { + val accountStatus = linkLauncher.setup(stripeIntent, this) + when (accountStatus) { AccountStatus.Verified, AccountStatus.VerificationStarted, AccountStatus.NeedsVerification -> { @@ -178,6 +179,7 @@ internal class PaymentOptionsViewModel @Inject constructor( } AccountStatus.SignedOut -> {} } + activeLinkSession.value = accountStatus == AccountStatus.Verified _isLinkEnabled.value = true } } else { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt index 9243dd03db0..06663c62496 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/PaymentSheetViewModel.kt @@ -433,7 +433,8 @@ internal class PaymentSheetViewModel @Inject internal constructor( stripeIntent.paymentMethodTypes.contains(PaymentMethod.Type.Link.code) ) { viewModelScope.launch { - when (linkLauncher.setup(stripeIntent, this)) { + val accountStatus = linkLauncher.setup(stripeIntent, this) + when (accountStatus) { AccountStatus.Verified -> launchLink() AccountStatus.VerificationStarted, AccountStatus.NeedsVerification -> { @@ -449,6 +450,7 @@ internal class PaymentSheetViewModel @Inject internal constructor( } AccountStatus.SignedOut -> {} } + activeLinkSession.value = accountStatus == AccountStatus.Verified _isLinkEnabled.value = true } } else { @@ -548,7 +550,9 @@ internal class PaymentSheetViewModel @Inject internal constructor( } } else -> { - eventReporter.onPaymentFailure(selection.value) + if (paymentResult is PaymentResult.Failed) { + eventReporter.onPaymentFailure(selection.value) + } runCatching { stripeIntentValidator.requireValid(stripeIntent) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt index 36254b8f30e..431be4301b3 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporter.kt @@ -16,8 +16,10 @@ internal class DefaultEventReporter @Inject internal constructor( private val mode: EventReporter.Mode, private val analyticsRequestExecutor: AnalyticsRequestExecutor, private val paymentAnalyticsRequestFactory: PaymentAnalyticsRequestFactory, + private val eventTimeProvider: EventTimeProvider, @IOContext private val workContext: CoroutineContext ) : EventReporter { + private var paymentSheetShownMillis: Long? = null override fun onInit(configuration: PaymentSheet.Configuration?) { fireEvent( @@ -36,18 +38,24 @@ internal class DefaultEventReporter @Inject internal constructor( ) } - override fun onShowExistingPaymentOptions() { + override fun onShowExistingPaymentOptions(linkEnabled: Boolean, activeLinkSession: Boolean) { + paymentSheetShownMillis = eventTimeProvider.currentTimeMillis() fireEvent( PaymentSheetEvent.ShowExistingPaymentOptions( - mode = mode + mode = mode, + linkEnabled = linkEnabled, + activeLinkSession = activeLinkSession ) ) } - override fun onShowNewPaymentOptionForm() { + override fun onShowNewPaymentOptionForm(linkEnabled: Boolean, activeLinkSession: Boolean) { + paymentSheetShownMillis = eventTimeProvider.currentTimeMillis() fireEvent( PaymentSheetEvent.ShowNewPaymentOptionForm( - mode = mode + mode = mode, + linkEnabled = linkEnabled, + activeLinkSession = activeLinkSession ) ) } @@ -66,6 +74,7 @@ internal class DefaultEventReporter @Inject internal constructor( PaymentSheetEvent.Payment( mode = mode, paymentSelection = paymentSelection, + durationMillis = durationMillisFrom(paymentSheetShownMillis), result = PaymentSheetEvent.Payment.Result.Success ) ) @@ -76,6 +85,7 @@ internal class DefaultEventReporter @Inject internal constructor( PaymentSheetEvent.Payment( mode = mode, paymentSelection = paymentSelection, + durationMillis = durationMillisFrom(paymentSheetShownMillis), result = PaymentSheetEvent.Payment.Result.Failure ) ) @@ -97,4 +107,8 @@ internal class DefaultEventReporter @Inject internal constructor( ) } } + + private fun durationMillisFrom(start: Long?) = start?.let { + eventTimeProvider.currentTimeMillis() - it + }?.takeIf { it > 0 } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/EventReporter.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/EventReporter.kt index b21028a9b99..6ffb3fd82e9 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/EventReporter.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/EventReporter.kt @@ -8,9 +8,9 @@ internal interface EventReporter { fun onDismiss() - fun onShowExistingPaymentOptions() + fun onShowExistingPaymentOptions(linkEnabled: Boolean, activeLinkSession: Boolean) - fun onShowNewPaymentOptionForm() + fun onShowNewPaymentOptionForm(linkEnabled: Boolean, activeLinkSession: Boolean) fun onSelectPaymentOption(paymentSelection: PaymentSelection) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/EventTimeProvider.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/EventTimeProvider.kt new file mode 100644 index 00000000000..a40cef525af --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/EventTimeProvider.kt @@ -0,0 +1,7 @@ +package com.stripe.android.paymentsheet.analytics + +import javax.inject.Inject + +internal class EventTimeProvider @Inject constructor() { + fun currentTimeMillis() = System.currentTimeMillis() +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt index 83a0c7598e0..6cfad056685 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEvent.kt @@ -88,17 +88,27 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent { } class ShowNewPaymentOptionForm( - mode: EventReporter.Mode + mode: EventReporter.Mode, + linkEnabled: Boolean, + activeLinkSession: Boolean ) : PaymentSheetEvent() { override val eventName: String = formatEventName(mode, "sheet_newpm_show") - override val additionalParams: Map = mapOf() + override val additionalParams: Map = mapOf( + "link_enabled" to linkEnabled, + "active_link_session" to activeLinkSession + ) } class ShowExistingPaymentOptions( - mode: EventReporter.Mode + mode: EventReporter.Mode, + linkEnabled: Boolean, + activeLinkSession: Boolean ) : PaymentSheetEvent() { override val eventName: String = formatEventName(mode, "sheet_savedpm_show") - override val additionalParams: Map = mapOf() + override val additionalParams: Map = mapOf( + "link_enabled" to linkEnabled, + "active_link_session" to activeLinkSession + ) } class SelectPaymentOption( @@ -113,11 +123,14 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent { class Payment( mode: EventReporter.Mode, result: Result, + durationMillis: Long?, paymentSelection: PaymentSelection? ) : PaymentSheetEvent() { override val eventName: String = formatEventName(mode, "payment_${analyticsValue(paymentSelection)}_$result") - override val additionalParams: Map = mapOf() + override val additionalParams: Map = durationMillis?.let { + mapOf("duration" to it / 1000f) + } ?: mapOf() enum class Result(private val code: String) { Success("success"), @@ -139,6 +152,8 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent { ) = when (paymentSelection) { PaymentSelection.GooglePay -> "googlepay" is PaymentSelection.Saved -> "savedpm" + PaymentSelection.Link, + is PaymentSelection.New.LinkInline -> "link" is PaymentSelection.New -> "newpm" else -> "unknown" } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt index 6c8a58ba701..6bd6485f416 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowController.kt @@ -6,10 +6,8 @@ import android.os.Parcelable import androidx.activity.result.ActivityResultCaller import androidx.activity.result.ActivityResultLauncher import androidx.annotation.VisibleForTesting -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ViewModelStoreOwner import com.stripe.android.PaymentConfiguration import com.stripe.android.core.injection.ENABLE_LOGGING @@ -134,9 +132,8 @@ internal class DefaultFlowController @Inject internal constructor( init { lifecycleOwner.lifecycle.addObserver( - object : LifecycleObserver { - @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) - fun onCreate() { + object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { paymentLauncher = paymentLauncherFactory.create( { lazyPaymentConfiguration.get().publishableKey }, { lazyPaymentConfiguration.get().stripeAccountId }, @@ -147,8 +144,7 @@ internal class DefaultFlowController @Inject internal constructor( ) } - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) - fun onDestroy() { + override fun onDestroy(owner: LifecycleOwner) { paymentLauncher = null } } @@ -316,7 +312,8 @@ internal class DefaultFlowController @Inject internal constructor( }.fold( onSuccess = { initData -> val paymentSelection = PaymentSelection.Saved( - googlePayResult.paymentMethod + googlePayResult.paymentMethod, + isGooglePay = true ) viewModel.paymentSelection = paymentSelection confirmPaymentSelection( @@ -422,6 +419,7 @@ internal class DefaultFlowController @Inject internal constructor( } internal fun onPaymentResult(paymentResult: PaymentResult) { + logPaymentResult(paymentResult) lifecycleScope.launch { paymentResultCallback.onPaymentSheetResult( paymentResult.convertToPaymentSheetResult() @@ -429,6 +427,21 @@ internal class DefaultFlowController @Inject internal constructor( } } + private fun logPaymentResult(paymentResult: PaymentResult?) { + when (paymentResult) { + is PaymentResult.Completed -> { + if ((viewModel.paymentSelection as? PaymentSelection.Saved)?.isGooglePay == true) { + // Google Pay is treated as a saved PM after confirmation + eventReporter.onPaymentSuccess(PaymentSelection.GooglePay) + } else { + eventReporter.onPaymentSuccess(viewModel.paymentSelection) + } + } + is PaymentResult.Failed -> eventReporter.onPaymentFailure(viewModel.paymentSelection) + else -> {} + } + } + private fun confirmLink( paymentSelection: PaymentSelection, initData: InitData diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt index fd5281e176f..318b88fbe68 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/model/PaymentSelection.kt @@ -25,7 +25,8 @@ sealed class PaymentSelection : Parcelable { @Parcelize @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) data class Saved( - val paymentMethod: PaymentMethod + val paymentMethod: PaymentMethod, + internal val isGooglePay: Boolean = false ) : PaymentSelection() @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetAddPaymentMethodFragmentTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetAddPaymentMethodFragmentTest.kt index 384a2c3338e..4dbdae6f0a2 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetAddPaymentMethodFragmentTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetAddPaymentMethodFragmentTest.kt @@ -42,6 +42,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -213,7 +214,7 @@ internal class PaymentSheetAddPaymentMethodFragmentTest : PaymentSheetViewModelT fun `started fragment should report onShowNewPaymentOptionForm() event`() { createFragment { _, _, _ -> idleLooper() - verify(eventReporter).onShowNewPaymentOptionForm() + verify(eventReporter).onShowNewPaymentOptionForm(any(), any()) } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetListFragmentTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetListFragmentTest.kt index 94104c1948e..bf7ccbe8285 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetListFragmentTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetListFragmentTest.kt @@ -28,6 +28,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.robolectric.RobolectricTestRunner @@ -192,7 +193,7 @@ internal class PaymentSheetListFragmentTest : PaymentSheetViewModelTestInjection @Test fun `started fragment should report onShowExistingPaymentOptions() event`() { createScenario().onFragment { - verify(eventReporter).onShowExistingPaymentOptions() + verify(eventReporter).onShowExistingPaymentOptions(any(), any()) } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt index 636ac3fc6e3..e58d7d9c966 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/DefaultEventReporterTest.kt @@ -13,7 +13,9 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import org.junit.runner.RunWith import org.mockito.kotlin.argWhere import org.mockito.kotlin.mock +import org.mockito.kotlin.reset import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.robolectric.RobolectricTestRunner import kotlin.test.Test @@ -21,7 +23,9 @@ import kotlin.test.Test @RunWith(RobolectricTestRunner::class) class DefaultEventReporterTest { private val testDispatcher = UnconfinedTestDispatcher() - + private val eventTimeProvider = mock().apply { + whenever(currentTimeMillis()).thenReturn(1000L) + } private val analyticsRequestExecutor = mock() private val analyticsRequestFactory = PaymentAnalyticsRequestFactory( ApplicationProvider.getApplicationContext(), @@ -33,6 +37,7 @@ class DefaultEventReporterTest { mode, analyticsRequestExecutor, analyticsRequestFactory, + eventTimeProvider, testDispatcher ) } @@ -50,14 +55,64 @@ class DefaultEventReporterTest { ) } + @Test + fun `onShowExistingPaymentOptions() should fire analytics request with expected event value`() { + completeEventReporter.onShowExistingPaymentOptions(true, false) + + verify(analyticsRequestExecutor).executeAsync( + argWhere { req -> + req.params["event"] == "mc_complete_sheet_savedpm_show" && + req.params["link_enabled"] == true && + req.params["active_link_session"] == false + } + ) + } + + @Test + fun `onShowNewPaymentOptionForm() should fire analytics request with expected event value`() { + completeEventReporter.onShowNewPaymentOptionForm(false, true) + + verify(analyticsRequestExecutor).executeAsync( + argWhere { req -> + req.params["event"] == "mc_complete_sheet_newpm_show" && + req.params["link_enabled"] == false && + req.params["active_link_session"] == true + } + ) + } + @Test fun `onPaymentSuccess() should fire analytics request with expected event value`() { + // Log initial event so that duration is tracked + completeEventReporter.onShowExistingPaymentOptions(false, false) + reset(analyticsRequestExecutor) + whenever(eventTimeProvider.currentTimeMillis()).thenReturn(2000L) + completeEventReporter.onPaymentSuccess( PaymentSelection.Saved(PaymentMethodFixtures.CARD_PAYMENT_METHOD) ) verify(analyticsRequestExecutor).executeAsync( argWhere { req -> - req.params["event"] == "mc_complete_payment_savedpm_success" + req.params["event"] == "mc_complete_payment_savedpm_success" && + req.params["duration"] == 1f + } + ) + } + + @Test + fun `onPaymentFailure() should fire analytics request with expected event value`() { + // Log initial event so that duration is tracked + completeEventReporter.onShowExistingPaymentOptions(false, false) + reset(analyticsRequestExecutor) + whenever(eventTimeProvider.currentTimeMillis()).thenReturn(2000L) + + completeEventReporter.onPaymentFailure( + PaymentSelection.Saved(PaymentMethodFixtures.CARD_PAYMENT_METHOD) + ) + verify(analyticsRequestExecutor).executeAsync( + argWhere { req -> + req.params["event"] == "mc_complete_payment_savedpm_failure" && + req.params["duration"] == 1f } ) } @@ -82,6 +137,7 @@ class DefaultEventReporterTest { EventReporter.Mode.Complete, analyticsRequestExecutor, analyticsRequestFactory, + eventTimeProvider, testDispatcher ) } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt index c27dc2347f8..7dffc0e5a96 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/analytics/PaymentSheetEventTest.kt @@ -1,9 +1,13 @@ package com.stripe.android.paymentsheet.analytics import com.google.common.truth.Truth.assertThat +import com.stripe.android.link.LinkPaymentDetails +import com.stripe.android.model.PaymentDetailsFixtures +import com.stripe.android.model.PaymentMethodCreateParamsFixtures import com.stripe.android.paymentsheet.PaymentSheetFixtures import com.stripe.android.paymentsheet.model.PaymentSelection import org.junit.runner.RunWith +import org.mockito.kotlin.mock import org.robolectric.RobolectricTestRunner import kotlin.test.Test @@ -36,15 +40,130 @@ class PaymentSheetEventTest { @Test fun `Payment event should return expected toString()`() { + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.New.Card(PaymentMethodCreateParamsFixtures.DEFAULT_CARD, mock(), mock()), + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Success + ).eventName + ).isEqualTo( + "mc_complete_payment_newpm_success" + ) + + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.Saved(mock()), + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Success + ).eventName + ).isEqualTo( + "mc_complete_payment_savedpm_success" + ) + assertThat( PaymentSheetEvent.Payment( mode = EventReporter.Mode.Complete, paymentSelection = PaymentSelection.GooglePay, + durationMillis = 1L, result = PaymentSheetEvent.Payment.Result.Success ).eventName ).isEqualTo( "mc_complete_payment_googlepay_success" ) + + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.Link, + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Success + ).eventName + ).isEqualTo( + "mc_complete_payment_link_success" + ) + + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.New.LinkInline( + LinkPaymentDetails.New( + PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first(), + mock(), + mock() + ) + ), + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Success + ).eventName + ).isEqualTo( + "mc_complete_payment_link_success" + ) + } + + @Test + fun `Payment failure event should return expected toString()`() { + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.New.Card(PaymentMethodCreateParamsFixtures.DEFAULT_CARD, mock(), mock()), + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Failure + ).eventName + ).isEqualTo( + "mc_complete_payment_newpm_failure" + ) + + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.Saved(mock()), + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Failure + ).eventName + ).isEqualTo( + "mc_complete_payment_savedpm_failure" + ) + + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.GooglePay, + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Failure + ).eventName + ).isEqualTo( + "mc_complete_payment_googlepay_failure" + ) + + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.Link, + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Failure + ).eventName + ).isEqualTo( + "mc_complete_payment_link_failure" + ) + + assertThat( + PaymentSheetEvent.Payment( + mode = EventReporter.Mode.Complete, + paymentSelection = PaymentSelection.New.LinkInline( + LinkPaymentDetails.New( + PaymentDetailsFixtures.CONSUMER_SINGLE_PAYMENT_DETAILS.paymentDetails.first(), + mock(), + mock() + ) + ), + durationMillis = 1L, + result = PaymentSheetEvent.Payment.Result.Failure + ).eventName + ).isEqualTo( + "mc_complete_payment_link_failure" + ) } @Test diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt index 38cd4a52d84..be18d0049e6 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/flowcontroller/DefaultFlowControllerTest.kt @@ -70,6 +70,7 @@ import org.mockito.Mockito.verifyNoInteractions import org.mockito.kotlin.any import org.mockito.kotlin.argWhere import org.mockito.kotlin.eq +import org.mockito.kotlin.isA import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -188,6 +189,30 @@ internal class DefaultFlowControllerTest { .onInit(PaymentSheetFixtures.CONFIG_CUSTOMER_WITH_GOOGLEPAY) } + @Test + fun `successful payment should fire analytics event`() { + val viewModel = ViewModelProvider(activity)[FlowControllerViewModel::class.java] + val flowController = createFlowController(viewModel = viewModel) + + viewModel.paymentSelection = PaymentSelection.New.Card(PaymentMethodCreateParamsFixtures.DEFAULT_CARD, mock(), mock()) + + flowController.onPaymentResult(PaymentResult.Completed) + + verify(eventReporter).onPaymentSuccess(isA()) + } + + @Test + fun `failed payment should fire analytics event`() { + val viewModel = ViewModelProvider(activity)[FlowControllerViewModel::class.java] + val flowController = createFlowController(viewModel = viewModel) + + viewModel.paymentSelection = PaymentSelection.New.Card(PaymentMethodCreateParamsFixtures.DEFAULT_CARD, mock(), mock()) + + flowController.onPaymentResult(PaymentResult.Failed(RuntimeException())) + + verify(eventReporter).onPaymentFailure(isA()) + } + @Test fun `getPaymentOption() when defaultPaymentMethodId is null should be null`() { assertThat(flowController.getPaymentOption()) @@ -857,19 +882,22 @@ internal class DefaultFlowControllerTest { private fun createFlowController( paymentMethods: List = emptyList(), savedSelection: SavedSelection = SavedSelection.None, - stripeIntent: StripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD + stripeIntent: StripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD, + viewModel: FlowControllerViewModel = ViewModelProvider(activity)[FlowControllerViewModel::class.java] ): DefaultFlowController { return createFlowController( FakeFlowControllerInitializer( paymentMethods, savedSelection, stripeIntent = stripeIntent - ) + ), + viewModel ) } private fun createFlowController( - flowControllerInitializer: FlowControllerInitializer + flowControllerInitializer: FlowControllerInitializer, + viewModel: FlowControllerViewModel = ViewModelProvider(activity)[FlowControllerViewModel::class.java] ) = DefaultFlowController( testScope, lifeCycleOwner, @@ -881,7 +909,7 @@ internal class DefaultFlowControllerTest { INJECTOR_KEY, flowControllerInitializer, eventReporter, - ViewModelProvider(activity)[FlowControllerViewModel::class.java], + viewModel, paymentLauncherAssistedFactory, mock(), mock(),