diff --git a/paymentsheet/detekt-baseline.xml b/paymentsheet/detekt-baseline.xml
index d305136d5bc..075560e5f83 100644
--- a/paymentsheet/detekt-baseline.xml
+++ b/paymentsheet/detekt-baseline.xml
@@ -85,6 +85,8 @@
NestedBlockDepth:SupportedPaymentMethodTest.kt$SupportedPaymentMethodTest$private fun generatePaymentIntentScenarios(): List<PaymentIntentTestInput>
ReturnCount:AddressUtils.kt$internal fun CharSequence.levenshtein(other: CharSequence): Int
ThrowsCount:PaymentSheetConfigurationKtx.kt$internal fun PaymentSheet.Configuration.validate()
+ TooManyFunctions:CustomerSheetEventReporter.kt$CustomerSheetEventReporter
+ TooManyFunctions:DefaultCustomerSheetEventReporter.kt$DefaultCustomerSheetEventReporter : CustomerSheetEventReporter
TooManyFunctions:DefaultEventReporter.kt$DefaultEventReporter : EventReporter
TooManyFunctions:DefaultFlowController.kt$DefaultFlowController : FlowController
TooManyFunctions:EventReporter.kt$EventReporter
diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt
index 11f4924b6fd..fa4713d3c86 100644
--- a/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/CustomerSheetViewModel.kt
@@ -47,6 +47,7 @@ import com.stripe.android.paymentsheet.parseAppearance
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.paymentsheet.state.toInternal
+import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PaymentMethodRemovalDelayMillis
import com.stripe.android.paymentsheet.ui.PrimaryButton
@@ -330,6 +331,10 @@ internal class CustomerSheetViewModel @Inject constructor(
)
} else {
backStack.update {
+ it.last().eventReporterScreen?.let { screen ->
+ eventReporter.onScreenHidden(screen)
+ }
+
it.dropLast(1)
}
}
@@ -456,6 +461,15 @@ internal class CustomerSheetViewModel @Inject constructor(
).onSuccess { updatedMethod ->
onBackPressed()
updatePaymentMethodInState(updatedMethod)
+
+ eventReporter.onUpdatePaymentMethodSucceeded(
+ selectedBrand = brand
+ )
+ }.onFailure { cause, _ ->
+ eventReporter.onUpdatePaymentMethodFailed(
+ selectedBrand = brand,
+ error = cause
+ )
}
}
@@ -512,6 +526,22 @@ internal class CustomerSheetViewModel @Inject constructor(
to = CustomerSheetViewState.EditPaymentMethod(
editPaymentMethodInteractor = editInteractorFactory.create(
initialPaymentMethod = paymentMethod,
+ eventHandler = { event ->
+ when (event) {
+ is EditPaymentMethodViewInteractor.Event.ShowBrands -> {
+ eventReporter.onShowPaymentOptionBrands(
+ source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = event.brand
+ )
+ }
+ is EditPaymentMethodViewInteractor.Event.HideBrands -> {
+ eventReporter.onHidePaymentOptionBrands(
+ source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = event.brand
+ )
+ }
+ }
+ },
displayName = providePaymentMethodName(paymentMethod.type?.code),
removeExecutor = { pm ->
removePaymentMethod(pm).onSuccess {
@@ -1137,6 +1167,8 @@ internal class CustomerSheetViewModel @Inject constructor(
eventReporter.onScreenPresented(CustomerSheetEventReporter.Screen.AddPaymentMethod)
is CustomerSheetViewState.SelectPaymentMethod ->
eventReporter.onScreenPresented(CustomerSheetEventReporter.Screen.SelectPaymentMethod)
+ is CustomerSheetViewState.EditPaymentMethod ->
+ eventReporter.onScreenPresented(CustomerSheetEventReporter.Screen.EditPaymentMethod)
else -> { }
}
@@ -1157,6 +1189,14 @@ internal class CustomerSheetViewModel @Inject constructor(
}
}
+ private val CustomerSheetViewState.eventReporterScreen: CustomerSheetEventReporter.Screen?
+ get() = when (this) {
+ is CustomerSheetViewState.AddPaymentMethod -> CustomerSheetEventReporter.Screen.AddPaymentMethod
+ is CustomerSheetViewState.SelectPaymentMethod -> CustomerSheetEventReporter.Screen.SelectPaymentMethod
+ is CustomerSheetViewState.EditPaymentMethod -> CustomerSheetEventReporter.Screen.EditPaymentMethod
+ else -> null
+ }
+
object Factory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEvent.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEvent.kt
index 2243eaa199a..8cf52795b08 100644
--- a/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEvent.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEvent.kt
@@ -1,6 +1,7 @@
package com.stripe.android.customersheet.analytics
import com.stripe.android.core.networking.AnalyticsEvent
+import com.stripe.android.model.CardBrand
internal sealed class CustomerSheetEvent : AnalyticsEvent {
@@ -15,6 +16,21 @@ internal sealed class CustomerSheetEvent : AnalyticsEvent {
CS_ADD_PAYMENT_METHOD_SCREEN_PRESENTED
CustomerSheetEventReporter.Screen.SelectPaymentMethod ->
CS_SELECT_PAYMENT_METHOD_SCREEN_PRESENTED
+ CustomerSheetEventReporter.Screen.EditPaymentMethod ->
+ CS_SHOW_EDITABLE_PAYMENT_OPTION
+ }
+ }
+
+ class ScreenHidden(
+ screen: CustomerSheetEventReporter.Screen,
+ ) : CustomerSheetEvent() {
+ override val additionalParams: Map = mapOf()
+ override val eventName: String = when (screen) {
+ CustomerSheetEventReporter.Screen.EditPaymentMethod ->
+ CS_HIDE_EDITABLE_PAYMENT_OPTION
+ else -> throw IllegalArgumentException(
+ "${screen.name} has no supported event for hiding screen!"
+ )
}
}
@@ -85,6 +101,60 @@ internal sealed class CustomerSheetEvent : AnalyticsEvent {
}
}
+ class ShowPaymentOptionBrands(
+ source: Source,
+ selectedBrand: CardBrand
+ ) : CustomerSheetEvent() {
+ override val eventName: String = CS_SHOW_PAYMENT_OPTION_BRANDS
+
+ override val additionalParams: Map = mapOf(
+ FIELD_CBC_EVENT_SOURCE to source.value,
+ FIELD_SELECTED_CARD_BRAND to selectedBrand.code
+ )
+
+ enum class Source(val value: String) {
+ Edit(VALUE_EDIT_CBC_EVENT_SOURCE), Add(VALUE_ADD_CBC_EVENT_SOURCE)
+ }
+ }
+
+ class HidePaymentOptionBrands(
+ source: Source,
+ selectedBrand: CardBrand?
+ ) : CustomerSheetEvent() {
+ override val eventName: String = CS_HIDE_PAYMENT_OPTION_BRANDS
+
+ override val additionalParams: Map = mapOf(
+ FIELD_CBC_EVENT_SOURCE to source.value,
+ FIELD_SELECTED_CARD_BRAND to selectedBrand?.code
+ )
+
+ enum class Source(val value: String) {
+ Edit(VALUE_EDIT_CBC_EVENT_SOURCE), Add(VALUE_ADD_CBC_EVENT_SOURCE)
+ }
+ }
+
+ class UpdatePaymentOptionSucceeded(
+ selectedBrand: CardBrand,
+ ) : CustomerSheetEvent() {
+ override val eventName: String = CS_UPDATE_PAYMENT_METHOD
+
+ override val additionalParams: Map = mapOf(
+ FIELD_SELECTED_CARD_BRAND to selectedBrand.code
+ )
+ }
+
+ class UpdatePaymentOptionFailed(
+ selectedBrand: CardBrand,
+ error: Throwable,
+ ) : CustomerSheetEvent() {
+ override val eventName: String = CS_UPDATE_PAYMENT_METHOD_FAILED
+
+ override val additionalParams: Map = mapOf(
+ FIELD_SELECTED_CARD_BRAND to selectedBrand.code,
+ FIELD_ERROR_MESSAGE to error.message,
+ )
+ }
+
internal companion object {
const val CS_ADD_PAYMENT_METHOD_SCREEN_PRESENTED =
"cs_add_payment_method_screen_presented"
@@ -118,6 +188,21 @@ internal sealed class CustomerSheetEvent : AnalyticsEvent {
const val CS_ADD_PAYMENT_METHOD_VIA_CREATE_ATTACH_FAILED =
"cs_add_payment_method_via_createAttach_failure"
+ const val CS_SHOW_EDITABLE_PAYMENT_OPTION = "cs_open_edit_screen"
+ const val CS_HIDE_EDITABLE_PAYMENT_OPTION = "cs_cancel_edit_screen"
+
+ const val CS_SHOW_PAYMENT_OPTION_BRANDS = "cs_open_cbc_dropdown"
+ const val CS_HIDE_PAYMENT_OPTION_BRANDS = "cs_close_cbc_dropdown"
+
+ const val CS_UPDATE_PAYMENT_METHOD = "cs_update_card"
+ const val CS_UPDATE_PAYMENT_METHOD_FAILED = "cs_update_card_failed"
+
+ const val FIELD_CBC_EVENT_SOURCE = "cbc_event_source"
+ const val FIELD_SELECTED_CARD_BRAND = "selected_card_brand"
+ const val FIELD_ERROR_MESSAGE = "error_message"
const val FIELD_PAYMENT_METHOD_TYPE = "payment_method_type"
+
+ const val VALUE_EDIT_CBC_EVENT_SOURCE = "edit"
+ const val VALUE_ADD_CBC_EVENT_SOURCE = "add"
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEventReporter.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEventReporter.kt
index f41c17e7012..007af15c5f6 100644
--- a/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEventReporter.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/CustomerSheetEventReporter.kt
@@ -1,5 +1,7 @@
package com.stripe.android.customersheet.analytics
+import com.stripe.android.model.CardBrand
+
internal interface CustomerSheetEventReporter {
/**
@@ -7,6 +9,11 @@ internal interface CustomerSheetEventReporter {
*/
fun onScreenPresented(screen: Screen)
+ /**
+ * [Screen] was hidden to user
+ */
+ fun onScreenHidden(screen: Screen)
+
/**
* User attempted to confirm their saved payment method selection and succeeded
*/
@@ -52,13 +59,53 @@ internal interface CustomerSheetEventReporter {
*/
fun onAttachPaymentMethodFailed(style: AddPaymentMethodStyle)
+ /**
+ * User attempted to show payment option brands if payment
+ * option support card brand choice selection.
+ */
+ fun onShowPaymentOptionBrands(
+ source: CardBrandChoiceEventSource,
+ selectedBrand: CardBrand,
+ )
+
+ /**
+ * User attempted to hide payment option brands if payment
+ * option support card brand choice selection.
+ */
+ fun onHidePaymentOptionBrands(
+ source: CardBrandChoiceEventSource,
+ selectedBrand: CardBrand?,
+ )
+
+ /**
+ * User successfully updated the card brand choice selection for
+ * a payment method.
+ */
+ fun onUpdatePaymentMethodSucceeded(
+ selectedBrand: CardBrand,
+ )
+
+ /**
+ * User failed to updated the card brand choice selection for
+ * a payment method.
+ */
+ fun onUpdatePaymentMethodFailed(
+ selectedBrand: CardBrand,
+ error: Throwable,
+ )
+
enum class Screen(val value: String) {
AddPaymentMethod("add_payment_method"),
SelectPaymentMethod("select_payment_method"),
+ EditPaymentMethod("edit_payment_method"),
}
enum class AddPaymentMethodStyle(val value: String) {
SetupIntent("setup_intent"),
CreateAttach("create_attach")
}
+
+ enum class CardBrandChoiceEventSource {
+ Add, Edit
+ }
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/DefaultCustomerSheetEventReporter.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/DefaultCustomerSheetEventReporter.kt
index 80f8aa73e19..34070e94bdc 100644
--- a/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/DefaultCustomerSheetEventReporter.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/analytics/DefaultCustomerSheetEventReporter.kt
@@ -3,6 +3,7 @@ package com.stripe.android.customersheet.analytics
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.core.networking.AnalyticsRequestFactory
+import com.stripe.android.model.CardBrand
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -21,6 +22,19 @@ internal class DefaultCustomerSheetEventReporter @Inject constructor(
)
}
+ override fun onScreenHidden(screen: CustomerSheetEventReporter.Screen) {
+ when (screen) {
+ CustomerSheetEventReporter.Screen.EditPaymentMethod -> {
+ fireEvent(
+ CustomerSheetEvent.ScreenHidden(
+ screen = screen
+ )
+ )
+ }
+ else -> Unit
+ }
+ }
+
override fun onConfirmPaymentMethodSucceeded(type: String) {
fireEvent(
CustomerSheetEvent.ConfirmPaymentMethodSucceeded(
@@ -91,6 +105,62 @@ internal class DefaultCustomerSheetEventReporter @Inject constructor(
)
}
+ override fun onShowPaymentOptionBrands(
+ source: CustomerSheetEventReporter.CardBrandChoiceEventSource,
+ selectedBrand: CardBrand
+ ) {
+ fireEvent(
+ CustomerSheetEvent.ShowPaymentOptionBrands(
+ source = when (source) {
+ CustomerSheetEventReporter.CardBrandChoiceEventSource.Add ->
+ CustomerSheetEvent.ShowPaymentOptionBrands.Source.Add
+ CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit ->
+ CustomerSheetEvent.ShowPaymentOptionBrands.Source.Edit
+ },
+ selectedBrand = selectedBrand
+ )
+ )
+ }
+
+ override fun onHidePaymentOptionBrands(
+ source: CustomerSheetEventReporter.CardBrandChoiceEventSource,
+ selectedBrand: CardBrand?
+ ) {
+ fireEvent(
+ CustomerSheetEvent.HidePaymentOptionBrands(
+ source = when (source) {
+ CustomerSheetEventReporter.CardBrandChoiceEventSource.Add ->
+ CustomerSheetEvent.HidePaymentOptionBrands.Source.Add
+ CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit ->
+ CustomerSheetEvent.HidePaymentOptionBrands.Source.Edit
+ },
+ selectedBrand = selectedBrand
+ )
+ )
+ }
+
+ override fun onUpdatePaymentMethodSucceeded(
+ selectedBrand: CardBrand,
+ ) {
+ fireEvent(
+ CustomerSheetEvent.UpdatePaymentOptionSucceeded(
+ selectedBrand = selectedBrand
+ )
+ )
+ }
+
+ override fun onUpdatePaymentMethodFailed(
+ selectedBrand: CardBrand,
+ error: Throwable,
+ ) {
+ fireEvent(
+ CustomerSheetEvent.UpdatePaymentOptionFailed(
+ selectedBrand = selectedBrand,
+ error = error
+ )
+ )
+ }
+
private fun fireEvent(event: CustomerSheetEvent) {
CoroutineScope(workContext).launch {
analyticsRequestExecutor.executeAsync(
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 15c24479ce6..4d6ec7b8baa 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
@@ -3,6 +3,7 @@ package com.stripe.android.paymentsheet.analytics
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.core.utils.DurationProvider
+import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethodCode
import com.stripe.android.networking.PaymentAnalyticsRequestFactory
import com.stripe.android.paymentsheet.DeferredIntentConfirmationType
@@ -215,6 +216,82 @@ internal class DefaultEventReporter @Inject internal constructor(
)
}
+ override fun onShowEditablePaymentOption() {
+ fireEvent(
+ PaymentSheetEvent.ShowEditablePaymentOption(
+ isDeferred = isDeferred
+ )
+ )
+ }
+
+ override fun onHideEditablePaymentOption() {
+ fireEvent(
+ PaymentSheetEvent.HideEditablePaymentOption(
+ isDeferred = isDeferred
+ )
+ )
+ }
+
+ override fun onShowPaymentOptionBrands(
+ source: EventReporter.CardBrandChoiceEventSource,
+ selectedBrand: CardBrand
+ ) {
+ fireEvent(
+ PaymentSheetEvent.ShowPaymentOptionBrands(
+ selectedBrand = selectedBrand,
+ source = when (source) {
+ EventReporter.CardBrandChoiceEventSource.Add ->
+ PaymentSheetEvent.ShowPaymentOptionBrands.Source.Add
+ EventReporter.CardBrandChoiceEventSource.Edit ->
+ PaymentSheetEvent.ShowPaymentOptionBrands.Source.Edit
+ },
+ isDeferred = isDeferred
+ )
+ )
+ }
+
+ override fun onHidePaymentOptionBrands(
+ source: EventReporter.CardBrandChoiceEventSource,
+ selectedBrand: CardBrand?
+ ) {
+ fireEvent(
+ PaymentSheetEvent.HidePaymentOptionBrands(
+ selectedBrand = selectedBrand,
+ source = when (source) {
+ EventReporter.CardBrandChoiceEventSource.Add ->
+ PaymentSheetEvent.HidePaymentOptionBrands.Source.Add
+ EventReporter.CardBrandChoiceEventSource.Edit ->
+ PaymentSheetEvent.HidePaymentOptionBrands.Source.Edit
+ },
+ isDeferred = isDeferred
+ )
+ )
+ }
+
+ override fun onUpdatePaymentMethodSucceeded(
+ selectedBrand: CardBrand
+ ) {
+ fireEvent(
+ PaymentSheetEvent.UpdatePaymentOptionSucceeded(
+ selectedBrand = selectedBrand,
+ isDeferred = isDeferred
+ )
+ )
+ }
+
+ override fun onUpdatePaymentMethodFailed(
+ selectedBrand: CardBrand,
+ error: Throwable,
+ ) {
+ fireEvent(
+ PaymentSheetEvent.UpdatePaymentOptionFailed(
+ selectedBrand = selectedBrand,
+ error = error,
+ isDeferred = isDeferred
+ )
+ )
+ }
+
private fun fireEvent(event: PaymentSheetEvent) {
CoroutineScope(workContext).launch {
analyticsRequestExecutor.executeAsync(
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 ae69c4f6e23..90382af26a3 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
@@ -1,6 +1,7 @@
package com.stripe.android.paymentsheet.analytics
import androidx.annotation.Keep
+import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethodCode
import com.stripe.android.paymentsheet.DeferredIntentConfirmationType
import com.stripe.android.paymentsheet.PaymentSheet
@@ -103,6 +104,48 @@ internal interface EventReporter {
type: String,
)
+ /**
+ * The customer has chosen to show the edit screen for an editable payment option.
+ */
+ fun onShowEditablePaymentOption()
+
+ /**
+ * The customer has chosen to hide the edit screen for an editable payment option.
+ */
+ fun onHideEditablePaymentOption()
+
+ /**
+ * The customer has chosen to show the payment option brands for an editable payment option.
+ */
+ fun onShowPaymentOptionBrands(
+ source: CardBrandChoiceEventSource,
+ selectedBrand: CardBrand,
+ )
+
+ /**
+ * The customer has chosen to hide the payment option brands for an editable payment option. The customer may
+ * have also chosen a new card brand selection as well.
+ */
+ fun onHidePaymentOptionBrands(
+ source: CardBrandChoiceEventSource,
+ selectedBrand: CardBrand?,
+ )
+
+ /**
+ * The customer has successfully updated a payment method with a new card brand selection.
+ */
+ fun onUpdatePaymentMethodSucceeded(
+ selectedBrand: CardBrand,
+ )
+
+ /**
+ * The customer has failed to update a payment method with a new card brand selection.
+ */
+ fun onUpdatePaymentMethodFailed(
+ selectedBrand: CardBrand,
+ error: Throwable,
+ )
+
enum class Mode(val code: String) {
Complete("complete"),
Custom("custom");
@@ -110,4 +153,8 @@ internal interface EventReporter {
@Keep
override fun toString(): String = code
}
+
+ enum class CardBrandChoiceEventSource {
+ Edit, Add
+ }
}
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 c5644592014..e20beae7460 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
@@ -1,6 +1,7 @@
package com.stripe.android.paymentsheet.analytics
import com.stripe.android.core.networking.AnalyticsEvent
+import com.stripe.android.model.CardBrand
import com.stripe.android.paymentsheet.DeferredIntentConfirmationType
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.model.PaymentSelection
@@ -140,6 +141,12 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent {
.name,
)
+ val preferredNetworks = configuration.preferredNetworks.takeIf { brands ->
+ brands.isNotEmpty()
+ }?.joinToString { brand ->
+ brand.code
+ }
+
@Suppress("DEPRECATION")
val configurationMap = mapOf(
FIELD_CUSTOMER to (configuration.customer != null),
@@ -150,6 +157,7 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent {
FIELD_APPEARANCE to appearanceConfigMap,
FIELD_BILLING_DETAILS_COLLECTION_CONFIGURATION to
billingDetailsCollectionConfigMap,
+ FIELD_PREFERRED_NETWORKS to preferredNetworks
)
return mapOf(
FIELD_MOBILE_PAYMENT_ELEMENT_CONFIGURATION to configurationMap,
@@ -303,6 +311,80 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent {
override val additionalParams: Map = emptyMap()
}
+ class ShowEditablePaymentOption(
+ override val isDeferred: Boolean,
+ ) : PaymentSheetEvent() {
+ override val eventName: String = "mc_open_edit_screen"
+
+ override val additionalParams: Map = emptyMap()
+ }
+
+ class HideEditablePaymentOption(
+ override val isDeferred: Boolean,
+ ) : PaymentSheetEvent() {
+ override val eventName: String = "mc_cancel_edit_screen"
+
+ override val additionalParams: Map = emptyMap()
+ }
+
+ class ShowPaymentOptionBrands(
+ source: Source,
+ selectedBrand: CardBrand,
+ override val isDeferred: Boolean,
+ ) : PaymentSheetEvent() {
+ override val eventName: String = "mc_open_cbc_dropdown"
+
+ override val additionalParams: Map = mapOf(
+ FIELD_CBC_EVENT_SOURCE to source.value,
+ FIELD_SELECTED_CARD_BRAND to selectedBrand.code
+ )
+
+ enum class Source(val value: String) {
+ Edit(VALUE_EDIT_CBC_EVENT_SOURCE), Add(VALUE_ADD_CBC_EVENT_SOURCE)
+ }
+ }
+
+ class HidePaymentOptionBrands(
+ source: Source,
+ selectedBrand: CardBrand?,
+ override val isDeferred: Boolean,
+ ) : PaymentSheetEvent() {
+ override val eventName: String = "mc_close_cbc_dropdown"
+
+ override val additionalParams: Map = mapOf(
+ FIELD_CBC_EVENT_SOURCE to source.value,
+ FIELD_SELECTED_CARD_BRAND to selectedBrand?.code
+ )
+
+ enum class Source(val value: String) {
+ Edit(VALUE_EDIT_CBC_EVENT_SOURCE), Add(VALUE_ADD_CBC_EVENT_SOURCE)
+ }
+ }
+
+ class UpdatePaymentOptionSucceeded(
+ selectedBrand: CardBrand,
+ override val isDeferred: Boolean,
+ ) : PaymentSheetEvent() {
+ override val eventName: String = "mc_update_card"
+
+ override val additionalParams: Map = mapOf(
+ FIELD_SELECTED_CARD_BRAND to selectedBrand.code
+ )
+ }
+
+ class UpdatePaymentOptionFailed(
+ selectedBrand: CardBrand,
+ error: Throwable,
+ override val isDeferred: Boolean,
+ ) : PaymentSheetEvent() {
+ override val eventName: String = "mc_update_card_failed"
+
+ override val additionalParams: Map = mapOf(
+ FIELD_SELECTED_CARD_BRAND to selectedBrand.code,
+ FIELD_ERROR_MESSAGE to error.message,
+ )
+ }
+
private fun standardParams(
isDecoupled: Boolean,
): Map = mapOf(
@@ -329,6 +411,7 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent {
const val FIELD_GOOGLE_PAY = "googlepay"
const val FIELD_PRIMARY_BUTTON_COLOR = "primary_button_color"
const val FIELD_BILLING = "default_billing_details"
+ const val FIELD_PREFERRED_NETWORKS = "preferred_networks"
const val FIELD_DELAYED_PMS = "allows_delayed_payment_methods"
const val FIELD_MOBILE_PAYMENT_ELEMENT_CONFIGURATION = "mpe_config"
const val FIELD_APPEARANCE = "appearance"
@@ -354,6 +437,11 @@ internal sealed class PaymentSheetEvent : AnalyticsEvent {
const val FIELD_CURRENCY = "currency"
const val FIELD_SELECTED_LPM = "selected_lpm"
const val FIELD_ERROR_MESSAGE = "error_message"
+ const val FIELD_CBC_EVENT_SOURCE = "cbc_event_source"
+ const val FIELD_SELECTED_CARD_BRAND = "selected_card_brand"
+
+ const val VALUE_EDIT_CBC_EVENT_SOURCE = "edit"
+ const val VALUE_ADD_CBC_EVENT_SOURCE = "add"
}
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethod.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethod.kt
index 9c73ea570f5..4386ef924de 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethod.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethod.kt
@@ -231,7 +231,11 @@ private fun Dropdown(
Box(
modifier = Modifier
.clickable {
- expanded = true
+ if (!expanded) {
+ expanded = true
+
+ viewActionHandler.invoke(EditPaymentMethodViewAction.OnBrandChoiceOptionsShown)
+ }
}
.testTag(DROPDOWN_MENU_CLICKABLE_TEST_TAG)
) {
@@ -271,6 +275,8 @@ private fun Dropdown(
},
onDismiss = {
expanded = false
+
+ viewActionHandler.invoke(EditPaymentMethodViewAction.OnBrandChoiceOptionsDismissed)
}
)
}
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewAction.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewAction.kt
index b200e76b955..cde7db8bf88 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewAction.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewAction.kt
@@ -1,6 +1,10 @@
package com.stripe.android.paymentsheet.ui
internal sealed interface EditPaymentMethodViewAction {
+ object OnBrandChoiceOptionsShown : EditPaymentMethodViewAction
+
+ object OnBrandChoiceOptionsDismissed : EditPaymentMethodViewAction
+
data class OnBrandChoiceChanged(
val choice: EditPaymentMethodViewState.CardBrandChoice
) : EditPaymentMethodViewAction
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewInteractor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewInteractor.kt
index 6efda73e25f..0eb9509260e 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewInteractor.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/ui/EditPaymentMethodViewInteractor.kt
@@ -28,6 +28,12 @@ internal interface EditPaymentMethodViewInteractor {
val viewState: StateFlow
fun handleViewAction(viewAction: EditPaymentMethodViewAction)
+
+ sealed interface Event {
+ data class ShowBrands(val brand: CardBrand) : Event
+
+ data class HideBrands(val brand: CardBrand?) : Event
+ }
}
internal interface ModifiableEditPaymentMethodViewInteractor : EditPaymentMethodViewInteractor {
@@ -36,6 +42,7 @@ internal interface ModifiableEditPaymentMethodViewInteractor : EditPaymentMethod
interface Factory {
fun create(
initialPaymentMethod: PaymentMethod,
+ eventHandler: (EditPaymentMethodViewInteractor.Event) -> Unit,
removeExecutor: PaymentMethodRemoveOperation,
updateExecutor: PaymentMethodUpdateOperation,
displayName: String,
@@ -46,6 +53,7 @@ internal interface ModifiableEditPaymentMethodViewInteractor : EditPaymentMethod
internal class DefaultEditPaymentMethodViewInteractor constructor(
initialPaymentMethod: PaymentMethod,
displayName: String,
+ private val eventHandler: (EditPaymentMethodViewInteractor.Event) -> Unit,
private val removeExecutor: PaymentMethodRemoveOperation,
private val updateExecutor: PaymentMethodUpdateOperation,
workContext: CoroutineContext = Dispatchers.Default,
@@ -97,6 +105,8 @@ internal class DefaultEditPaymentMethodViewInteractor constructor(
is EditPaymentMethodViewAction.OnRemovePressed -> onRemovePressed()
is EditPaymentMethodViewAction.OnRemoveConfirmed -> onRemoveConfirmed()
is EditPaymentMethodViewAction.OnUpdatePressed -> onUpdatePressed()
+ is EditPaymentMethodViewAction.OnBrandChoiceOptionsShown -> onBrandChoiceOptionsShown()
+ is EditPaymentMethodViewAction.OnBrandChoiceOptionsDismissed -> onBrandChoiceOptionsDismissed()
is EditPaymentMethodViewAction.OnBrandChoiceChanged -> onBrandChoiceChanged(viewAction.choice)
is EditPaymentMethodViewAction.OnRemoveConfirmationDismissed -> onRemoveConfirmationDismissed()
}
@@ -147,8 +157,18 @@ internal class DefaultEditPaymentMethodViewInteractor constructor(
}
}
+ private fun onBrandChoiceOptionsShown() {
+ eventHandler(EditPaymentMethodViewInteractor.Event.ShowBrands(choice.value.brand))
+ }
+
+ private fun onBrandChoiceOptionsDismissed() {
+ eventHandler(EditPaymentMethodViewInteractor.Event.HideBrands(brand = null))
+ }
+
private fun onBrandChoiceChanged(choice: EditPaymentMethodViewState.CardBrandChoice) {
this.choice.value = choice
+
+ eventHandler(EditPaymentMethodViewInteractor.Event.HideBrands(brand = choice.brand))
}
private fun onRemoveConfirmationDismissed() {
@@ -183,12 +203,14 @@ internal class DefaultEditPaymentMethodViewInteractor constructor(
object Factory : ModifiableEditPaymentMethodViewInteractor.Factory {
override fun create(
initialPaymentMethod: PaymentMethod,
+ eventHandler: (EditPaymentMethodViewInteractor.Event) -> Unit,
removeExecutor: PaymentMethodRemoveOperation,
updateExecutor: PaymentMethodUpdateOperation,
displayName: String,
): ModifiableEditPaymentMethodViewInteractor {
return DefaultEditPaymentMethodViewInteractor(
initialPaymentMethod = initialPaymentMethod,
+ eventHandler = eventHandler,
removeExecutor = removeExecutor,
updateExecutor = updateExecutor,
displayName = displayName,
diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt
index 870bd345888..584b956a971 100644
--- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt
+++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/viewmodels/BaseSheetViewModel.kt
@@ -38,6 +38,7 @@ import com.stripe.android.paymentsheet.repositories.CustomerRepository
import com.stripe.android.paymentsheet.state.GooglePayState
import com.stripe.android.paymentsheet.state.WalletsState
import com.stripe.android.paymentsheet.toPaymentSelection
+import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.HeaderTextFactory
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PaymentMethodRemovalDelayMillis
@@ -267,7 +268,7 @@ internal abstract class BaseSheetViewModel(
private fun reportPaymentSheetShown(currentScreen: PaymentSheetScreen) {
when (currentScreen) {
- is PaymentSheetScreen.Loading, AddAnotherPaymentMethod -> {
+ is PaymentSheetScreen.Loading, AddAnotherPaymentMethod, is PaymentSheetScreen.EditPaymentMethod -> {
// Nothing to do here
}
is PaymentSheetScreen.SelectSavedPaymentMethods -> {
@@ -276,8 +277,16 @@ internal abstract class BaseSheetViewModel(
is AddFirstPaymentMethod -> {
eventReporter.onShowNewPaymentOptionForm()
}
+ }
+ }
+
+ private fun reportPaymentSheetHidden(hiddenScreen: PaymentSheetScreen) {
+ when (hiddenScreen) {
is PaymentSheetScreen.EditPaymentMethod -> {
- // TODO(tillh-stripe) Add reporting
+ eventReporter.onHideEditablePaymentOption()
+ }
+ else -> {
+ // Events for hiding other screens not supported
}
}
}
@@ -447,10 +456,28 @@ internal abstract class BaseSheetViewModel(
}
fun modifyPaymentMethod(paymentMethod: PaymentMethod) {
+ eventReporter.onShowEditablePaymentOption()
+
transitionTo(
PaymentSheetScreen.EditPaymentMethod(
editInteractorFactory.create(
initialPaymentMethod = paymentMethod,
+ eventHandler = { event ->
+ when (event) {
+ is EditPaymentMethodViewInteractor.Event.ShowBrands -> {
+ eventReporter.onShowPaymentOptionBrands(
+ source = EventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = event.brand
+ )
+ }
+ is EditPaymentMethodViewInteractor.Event.HideBrands -> {
+ eventReporter.onHidePaymentOptionBrands(
+ source = EventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = event.brand
+ )
+ }
+ }
+ },
displayName = providePaymentMethodName(paymentMethod.type?.code),
removeExecutor = { method ->
removePaymentMethodInEditScreen(method)
@@ -510,6 +537,15 @@ internal abstract class BaseSheetViewModel(
}
handleBackPressed()
+
+ eventReporter.onUpdatePaymentMethodSucceeded(
+ selectedBrand = brand
+ )
+ }.onFailure { error ->
+ eventReporter.onUpdatePaymentMethodFailed(
+ selectedBrand = brand,
+ error = error,
+ )
}
}
@@ -568,7 +604,11 @@ internal abstract class BaseSheetViewModel(
backStack.update { screens ->
val modifiableScreens = screens.toMutableList()
- modifiableScreens.removeLast().onClose()
+ val lastScreen = modifiableScreens.removeLast()
+
+ lastScreen.onClose()
+
+ reportPaymentSheetHidden(lastScreen)
modifiableScreens.toList()
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetEventReporterTest.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetEventReporterTest.kt
index 56ac1fcb2e8..4eb49ad1753 100644
--- a/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetEventReporterTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetEventReporterTest.kt
@@ -12,6 +12,8 @@ import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.C
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_ADD_PAYMENT_METHOD_VIA_SETUP_INTENT_CANCELED
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_ADD_PAYMENT_METHOD_VIA_SETUP_INTENT_FAILED
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_ADD_PAYMENT_METHOD_VIA_SETUP_INTENT_SUCCEEDED
+import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_HIDE_EDITABLE_PAYMENT_OPTION
+import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_HIDE_PAYMENT_OPTION_BRANDS
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SELECT_PAYMENT_METHOD_CONFIRMED_SAVED_PM_FAILED
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SELECT_PAYMENT_METHOD_CONFIRMED_SAVED_PM_SUCCEEDED
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SELECT_PAYMENT_METHOD_DONE_TAPPED
@@ -19,9 +21,14 @@ import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.C
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SELECT_PAYMENT_METHOD_REMOVE_PM_FAILED
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SELECT_PAYMENT_METHOD_REMOVE_PM_SUCCEEDED
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SELECT_PAYMENT_METHOD_SCREEN_PRESENTED
+import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SHOW_EDITABLE_PAYMENT_OPTION
+import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_SHOW_PAYMENT_OPTION_BRANDS
+import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_UPDATE_PAYMENT_METHOD
+import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.CS_UPDATE_PAYMENT_METHOD_FAILED
import com.stripe.android.customersheet.analytics.CustomerSheetEvent.Companion.FIELD_PAYMENT_METHOD_TYPE
import com.stripe.android.customersheet.analytics.CustomerSheetEventReporter
import com.stripe.android.customersheet.analytics.DefaultCustomerSheetEventReporter
+import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethod
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -68,6 +75,23 @@ class CustomerSheetEventReporterTest {
req.params["event"] == CS_SELECT_PAYMENT_METHOD_SCREEN_PRESENTED
}
)
+
+ eventReporter.onScreenPresented(screen = CustomerSheetEventReporter.Screen.EditPaymentMethod)
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == CS_SHOW_EDITABLE_PAYMENT_OPTION
+ }
+ )
+ }
+
+ @Test
+ fun `onScreenHidden should fire analytics request with expected event value`() {
+ eventReporter.onScreenHidden(screen = CustomerSheetEventReporter.Screen.EditPaymentMethod)
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == CS_HIDE_EDITABLE_PAYMENT_OPTION
+ }
+ )
}
@Test
@@ -190,4 +214,66 @@ class CustomerSheetEventReporterTest {
}
)
}
+
+ @Test
+ fun `onShowPaymentOptionBrands() should fire analytics request with expected event value`() {
+ eventReporter.onShowPaymentOptionBrands(
+ source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.Visa
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == CS_SHOW_PAYMENT_OPTION_BRANDS &&
+ req.params["cbc_event_source"] == "edit" &&
+ req.params["selected_card_brand"] == "visa"
+ }
+ )
+ }
+
+ @Test
+ fun `onHidePaymentOptionBrands() should fire analytics request with expected event value`() {
+ eventReporter.onHidePaymentOptionBrands(
+ source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.CartesBancaires,
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == CS_HIDE_PAYMENT_OPTION_BRANDS &&
+ req.params["cbc_event_source"] == "edit" &&
+ req.params["selected_card_brand"] == "cartes_bancaires"
+ }
+ )
+ }
+
+ @Test
+ fun `onUpdatePaymentMethodSucceeded() should fire analytics request with expected event value`() {
+ eventReporter.onUpdatePaymentMethodSucceeded(
+ selectedBrand = CardBrand.CartesBancaires,
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == CS_UPDATE_PAYMENT_METHOD &&
+ req.params["selected_card_brand"] == "cartes_bancaires"
+ }
+ )
+ }
+
+ @Test
+ fun `onUpdatePaymentMethodFailed() should fire analytics request with expected event value`() {
+ eventReporter.onUpdatePaymentMethodFailed(
+ selectedBrand = CardBrand.CartesBancaires,
+ error = Exception("No network available!")
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == CS_UPDATE_PAYMENT_METHOD_FAILED &&
+ req.params["selected_card_brand"] == "cartes_bancaires" &&
+ req.params["error_message"] == "No network available!"
+ }
+ )
+ }
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetScreenshotTest.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetScreenshotTest.kt
index 3c5a2b2b601..b814f7874c9 100644
--- a/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetScreenshotTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetScreenshotTest.kt
@@ -384,6 +384,7 @@ internal class CustomerSheetScreenshotTest {
displayName = "Card",
removeExecutor = { null },
updateExecutor = { pm, _ -> Result.success(pm) },
+ eventHandler = {}
),
isLiveMode = true,
cbcEligibility = CardBrandChoiceEligibility.Eligible(preferredNetworks = emptyList()),
diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetViewModelTest.kt
index 20bac8f8bba..b3b59e4afea 100644
--- a/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetViewModelTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/CustomerSheetViewModelTest.kt
@@ -13,6 +13,7 @@ import com.stripe.android.customersheet.CustomerSheetViewState.SelectPaymentMeth
import com.stripe.android.customersheet.analytics.CustomerSheetEventReporter
import com.stripe.android.customersheet.injection.CustomerSheetViewModelModule
import com.stripe.android.customersheet.utils.CustomerSheetTestHelper.addPaymentMethodViewState
+import com.stripe.android.customersheet.utils.CustomerSheetTestHelper.createModifiableEditPaymentMethodViewInteractorFactory
import com.stripe.android.customersheet.utils.CustomerSheetTestHelper.createViewModel
import com.stripe.android.customersheet.utils.CustomerSheetTestHelper.selectPaymentMethodViewState
import com.stripe.android.customersheet.utils.FakeCustomerSheetLoader
@@ -22,6 +23,7 @@ import com.stripe.android.financialconnections.model.PaymentAccount
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodFixtures.CARD_PAYMENT_METHOD
+import com.stripe.android.model.PaymentMethodFixtures.CARD_WITH_NETWORKS_PAYMENT_METHOD
import com.stripe.android.model.PaymentMethodFixtures.US_BANK_ACCOUNT
import com.stripe.android.model.PaymentMethodFixtures.US_BANK_ACCOUNT_VERIFIED
import com.stripe.android.model.SetupIntentFixtures
@@ -32,6 +34,7 @@ import com.stripe.android.paymentsheet.forms.FormFieldValues
import com.stripe.android.paymentsheet.forms.FormViewModel
import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormScreenState
+import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction
import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction.OnBrandChoiceChanged
import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction.OnRemoveConfirmed
import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction.OnRemovePressed
@@ -52,6 +55,8 @@ import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -1264,6 +1269,40 @@ class CustomerSheetViewModelTest {
verify(eventReporter).onScreenPresented(CustomerSheetEventReporter.Screen.AddPaymentMethod)
}
+ @Test
+ fun `When edit payment screen is presented, event is reported`() {
+ val eventReporter: CustomerSheetEventReporter = mock()
+
+ val viewModel = createViewModel(
+ workContext = testDispatcher,
+ eventReporter = eventReporter,
+ )
+
+ viewModel.handleViewAction(
+ CustomerSheetViewAction.OnModifyItem(paymentMethod = CARD_PAYMENT_METHOD)
+ )
+
+ verify(eventReporter).onScreenPresented(CustomerSheetEventReporter.Screen.EditPaymentMethod)
+ }
+
+ @Test
+ fun `When edit payment screen is hidden, event is reported`() {
+ val eventReporter: CustomerSheetEventReporter = mock()
+
+ val viewModel = createViewModel(
+ workContext = testDispatcher,
+ eventReporter = eventReporter,
+ )
+
+ viewModel.handleViewAction(
+ CustomerSheetViewAction.OnModifyItem(paymentMethod = CARD_PAYMENT_METHOD)
+ )
+
+ viewModel.handleViewAction(CustomerSheetViewAction.OnBackPressed)
+
+ verify(eventReporter).onScreenHidden(CustomerSheetEventReporter.Screen.EditPaymentMethod)
+ }
+
@Test
fun `When edit is tapped, event is reported`() = runTest(testDispatcher) {
val eventReporter: CustomerSheetEventReporter = mock()
@@ -2536,24 +2575,87 @@ class CustomerSheetViewModelTest {
}
@Test
- fun `Updating payment method in edit screen goes through expected states`() = runTest(testDispatcher) {
- val paymentMethods = PaymentMethodFactory.cards(size = 1)
+ fun `Updating payment method in edit screen goes through expected states & reports event`() =
+ runTest(testDispatcher) {
+ val eventReporter: CustomerSheetEventReporter = mock()
+ val paymentMethods = PaymentMethodFactory.cards(size = 1)
- val firstMethod = paymentMethods.single()
+ val firstMethod = paymentMethods.single()
- val updatedMethod = firstMethod.copy(
- card = firstMethod.card?.copy(
- networks = PaymentMethod.Card.Networks(
- available = setOf("visa", "cartes_bancaires"),
- preferred = "visa"
+ val updatedMethod = firstMethod.copy(
+ card = firstMethod.card?.copy(
+ networks = PaymentMethod.Card.Networks(
+ available = setOf("visa", "cartes_bancaires"),
+ preferred = "visa"
+ )
)
)
- )
+
+ val customerAdapter = FakeCustomerAdapter(
+ paymentMethods = CustomerAdapter.Result.Success(paymentMethods),
+ onUpdatePaymentMethod = { _, _ ->
+ CustomerAdapter.Result.Success(updatedMethod)
+ }
+ )
+
+ val viewModel = createViewModel(
+ workContext = testDispatcher,
+ initialBackStack = listOf(
+ selectPaymentMethodViewState.copy(
+ savedPaymentMethods = paymentMethods,
+ )
+ ),
+ eventReporter = eventReporter,
+ customerPaymentMethods = paymentMethods,
+ customerAdapter = customerAdapter,
+ editInteractorFactory = createModifiableEditPaymentMethodViewInteractorFactory(
+ workContext = testDispatcher
+ ),
+ )
+
+ viewModel.viewState.test {
+ assertThat(awaitItem()).isInstanceOf(SelectPaymentMethod::class.java)
+ viewModel.handleViewAction(CustomerSheetViewAction.OnModifyItem(firstMethod))
+
+ val editViewState = awaitViewState()
+ editViewState.editPaymentMethodInteractor.handleViewAction(
+ OnBrandChoiceChanged(
+ EditPaymentMethodViewState.CardBrandChoice(
+ brand = CardBrand.Visa
+ )
+ )
+ )
+ editViewState.editPaymentMethodInteractor.handleViewAction(OnUpdatePressed)
+
+ // Confirm that nothing has changed yet. We're waiting to update the payment method
+ // once we return to the SPM screen.
+ val updatedViewState = awaitViewState()
+ assertThat(updatedViewState.savedPaymentMethods).containsExactlyElementsIn(paymentMethods)
+
+ // Simulate the delay
+ testDispatcher.scheduler.advanceUntilIdle()
+
+ verify(eventReporter).onUpdatePaymentMethodSucceeded(CardBrand.Visa)
+
+ val finalViewState = awaitViewState()
+ assertThat(finalViewState.savedPaymentMethods).containsExactlyElementsIn(listOf(updatedMethod))
+ }
+ }
+
+ @Test
+ fun `Failed update payment method in edit screen reports event`() = runTest(testDispatcher) {
+ val eventReporter: CustomerSheetEventReporter = mock()
+ val paymentMethods = PaymentMethodFactory.cards(size = 1)
+
+ val firstMethod = paymentMethods.single()
val customerAdapter = FakeCustomerAdapter(
paymentMethods = CustomerAdapter.Result.Success(paymentMethods),
onUpdatePaymentMethod = { _, _ ->
- CustomerAdapter.Result.Success(updatedMethod)
+ CustomerAdapter.Result.failure(
+ Exception("No network found!"),
+ "No network found!"
+ )
}
)
@@ -2564,8 +2666,12 @@ class CustomerSheetViewModelTest {
savedPaymentMethods = paymentMethods,
)
),
+ eventReporter = eventReporter,
customerPaymentMethods = paymentMethods,
customerAdapter = customerAdapter,
+ editInteractorFactory = createModifiableEditPaymentMethodViewInteractorFactory(
+ workContext = testDispatcher
+ ),
)
viewModel.viewState.test {
@@ -2582,16 +2688,113 @@ class CustomerSheetViewModelTest {
)
editViewState.editPaymentMethodInteractor.handleViewAction(OnUpdatePressed)
- // Confirm that nothing has changed yet. We're waiting to update the payment method
- // once we return to the SPM screen.
- val updatedViewState = awaitViewState()
- assertThat(updatedViewState.savedPaymentMethods).containsExactlyElementsIn(paymentMethods)
-
// Simulate the delay
testDispatcher.scheduler.advanceUntilIdle()
- val finalViewState = awaitViewState()
- assertThat(finalViewState.savedPaymentMethods).containsExactlyElementsIn(listOf(updatedMethod))
+ verify(eventReporter).onUpdatePaymentMethodFailed(
+ eq(CardBrand.Visa),
+ argThat {
+ message == "No network found!"
+ }
+ )
+ }
+ }
+
+ @Test
+ fun `Showing payment option brands in edit screen reports event`() = runTest(testDispatcher) {
+ val eventReporter: CustomerSheetEventReporter = mock()
+ val paymentMethods = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ workContext = testDispatcher,
+ eventReporter = eventReporter,
+ initialBackStack = listOf(
+ selectPaymentMethodViewState.copy(
+ savedPaymentMethods = paymentMethods,
+ )
+ ),
+ customerPaymentMethods = paymentMethods
+ )
+
+ viewModel.viewState.test {
+ assertThat(awaitItem()).isInstanceOf(SelectPaymentMethod::class.java)
+ viewModel.handleViewAction(CustomerSheetViewAction.OnModifyItem(paymentMethods.single()))
+
+ val editViewState = awaitViewState()
+ editViewState.editPaymentMethodInteractor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceOptionsShown
+ )
+
+ verify(eventReporter).onShowPaymentOptionBrands(
+ source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.CartesBancaires
+ )
+ }
+ }
+
+ @Test
+ fun `Hiding payment option brands in edit screen reports event`() = runTest(testDispatcher) {
+ val eventReporter: CustomerSheetEventReporter = mock()
+ val paymentMethods = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ workContext = testDispatcher,
+ eventReporter = eventReporter,
+ initialBackStack = listOf(
+ selectPaymentMethodViewState.copy(
+ savedPaymentMethods = paymentMethods,
+ )
+ ),
+ customerPaymentMethods = paymentMethods,
+ )
+
+ viewModel.viewState.test {
+ assertThat(awaitItem()).isInstanceOf(SelectPaymentMethod::class.java)
+ viewModel.handleViewAction(CustomerSheetViewAction.OnModifyItem(paymentMethods.single()))
+
+ val editViewState = awaitViewState()
+ editViewState.editPaymentMethodInteractor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceOptionsDismissed
+ )
+
+ verify(eventReporter).onHidePaymentOptionBrands(
+ source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = null
+ )
+ }
+ }
+
+ @Test
+ fun `Changing payment option brand in edit screen reports event`() = runTest(testDispatcher) {
+ val eventReporter: CustomerSheetEventReporter = mock()
+ val paymentMethods = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ workContext = testDispatcher,
+ eventReporter = eventReporter,
+ initialBackStack = listOf(
+ selectPaymentMethodViewState.copy(
+ savedPaymentMethods = paymentMethods,
+ )
+ ),
+ customerPaymentMethods = paymentMethods,
+ )
+
+ viewModel.viewState.test {
+ assertThat(awaitItem()).isInstanceOf(SelectPaymentMethod::class.java)
+ viewModel.handleViewAction(CustomerSheetViewAction.OnModifyItem(paymentMethods.single()))
+
+ val editViewState = awaitViewState()
+ editViewState.editPaymentMethodInteractor.handleViewAction(
+ OnBrandChoiceChanged(
+ EditPaymentMethodViewState.CardBrandChoice(brand = CardBrand.Visa)
+ )
+ )
+
+ verify(eventReporter).onHidePaymentOptionBrands(
+ source = CustomerSheetEventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.Visa
+ )
}
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt
index ad22fe76820..556f170ddd8 100644
--- a/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/CustomerSheetTestHelper.kt
@@ -32,6 +32,7 @@ import com.stripe.android.paymentsheet.model.PaymentSelection
import com.stripe.android.paymentsheet.paymentdatacollection.FormArguments
import com.stripe.android.paymentsheet.paymentdatacollection.ach.USBankAccountFormArguments
import com.stripe.android.paymentsheet.ui.DefaultEditPaymentMethodViewInteractor
+import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PaymentMethodRemoveOperation
import com.stripe.android.paymentsheet.ui.PaymentMethodUpdateOperation
@@ -42,6 +43,7 @@ import com.stripe.android.uicore.address.AddressRepository
import com.stripe.android.utils.DummyActivityResultCaller
import com.stripe.android.utils.FakeIntentConfirmationInterceptor
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.StandardTestDispatcher
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -53,22 +55,6 @@ import kotlin.coroutines.EmptyCoroutineContext
internal object CustomerSheetTestHelper {
internal val application = ApplicationProvider.getApplicationContext()
- private val editPaymentMethodInteractorFactory = object : ModifiableEditPaymentMethodViewInteractor.Factory {
- override fun create(
- initialPaymentMethod: PaymentMethod,
- removeExecutor: PaymentMethodRemoveOperation,
- updateExecutor: PaymentMethodUpdateOperation,
- displayName: String
- ): ModifiableEditPaymentMethodViewInteractor {
- return DefaultEditPaymentMethodViewInteractor(
- initialPaymentMethod = initialPaymentMethod,
- displayName = "Card",
- removeExecutor = removeExecutor,
- updateExecutor = updateExecutor,
- )
- }
- }
-
internal val usBankAccountFormArguments = USBankAccountFormArguments(
onBehalfOf = null,
isCompleteFlow = false,
@@ -221,7 +207,8 @@ internal object CustomerSheetTestHelper {
paymentSelection = savedPaymentSelection,
isGooglePayAvailable = isGooglePayAvailable,
),
- editInteractorFactory: ModifiableEditPaymentMethodViewInteractor.Factory = editPaymentMethodInteractorFactory,
+ editInteractorFactory: ModifiableEditPaymentMethodViewInteractor.Factory =
+ createModifiableEditPaymentMethodViewInteractorFactory(),
): CustomerSheetViewModel {
return CustomerSheetViewModel(
application = application,
@@ -258,4 +245,27 @@ internal object CustomerSheetTestHelper {
registerFromActivity(DummyActivityResultCaller(), TestLifecycleOwner())
}
}
+
+ internal fun createModifiableEditPaymentMethodViewInteractorFactory(
+ workContext: CoroutineContext = StandardTestDispatcher(),
+ ): ModifiableEditPaymentMethodViewInteractor.Factory {
+ return object : ModifiableEditPaymentMethodViewInteractor.Factory {
+ override fun create(
+ initialPaymentMethod: PaymentMethod,
+ eventHandler: (EditPaymentMethodViewInteractor.Event) -> Unit,
+ removeExecutor: PaymentMethodRemoveOperation,
+ updateExecutor: PaymentMethodUpdateOperation,
+ displayName: String
+ ): ModifiableEditPaymentMethodViewInteractor {
+ return DefaultEditPaymentMethodViewInteractor(
+ initialPaymentMethod = initialPaymentMethod,
+ displayName = "Card",
+ removeExecutor = removeExecutor,
+ updateExecutor = updateExecutor,
+ eventHandler = eventHandler,
+ workContext = workContext
+ )
+ }
+ }
+ }
}
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FakeEditPaymentMethodInteractor.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FakeEditPaymentMethodInteractor.kt
index 517ff0b9ff5..fd807d46242 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FakeEditPaymentMethodInteractor.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/FakeEditPaymentMethodInteractor.kt
@@ -3,6 +3,7 @@ package com.stripe.android.paymentsheet
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction
+import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewState
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PaymentMethodRemoveOperation
@@ -39,6 +40,7 @@ internal class FakeEditPaymentMethodInteractor(
object Factory : ModifiableEditPaymentMethodViewInteractor.Factory {
override fun create(
initialPaymentMethod: PaymentMethod,
+ eventHandler: (EditPaymentMethodViewInteractor.Event) -> Unit,
removeExecutor: PaymentMethodRemoveOperation,
updateExecutor: PaymentMethodUpdateOperation,
displayName: String
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt
index 0f74ec11cff..9ecfbef2267 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt
@@ -31,6 +31,7 @@ import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.PaymentMethodCreateParamsFixtures
import com.stripe.android.model.PaymentMethodFixtures
+import com.stripe.android.model.PaymentMethodFixtures.CARD_WITH_NETWORKS_PAYMENT_METHOD
import com.stripe.android.model.PaymentMethodFixtures.SEPA_DEBIT_PAYMENT_METHOD
import com.stripe.android.model.PaymentMethodOptionsParams
import com.stripe.android.model.PaymentMethodUpdateParams
@@ -87,6 +88,7 @@ import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argThat
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
@@ -194,7 +196,105 @@ internal class PaymentSheetViewModelTest {
}
@Test
- fun `modifyPaymentMethod updates payment methods on successful update`() = runTest {
+ fun `correct event is sent when dropdown is opened in EditPaymentMethod`() = runTest {
+ val paymentMethods = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ customerPaymentMethods = paymentMethods
+ )
+
+ viewModel.currentScreen.test {
+ awaitItem()
+
+ viewModel.modifyPaymentMethod(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val currentScreen = awaitItem()
+
+ assertThat(currentScreen).isInstanceOf(PaymentSheetScreen.EditPaymentMethod::class.java)
+
+ if (currentScreen is PaymentSheetScreen.EditPaymentMethod) {
+ val interactor = currentScreen.interactor
+
+ interactor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceOptionsShown
+ )
+
+ verify(eventReporter).onShowPaymentOptionBrands(
+ source = EventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.CartesBancaires
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `correct event is sent when dropdown is dismissed in EditPaymentMethod`() = runTest {
+ val paymentMethods = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ customerPaymentMethods = paymentMethods
+ )
+
+ viewModel.currentScreen.test {
+ awaitItem()
+
+ viewModel.modifyPaymentMethod(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val currentScreen = awaitItem()
+
+ assertThat(currentScreen).isInstanceOf(PaymentSheetScreen.EditPaymentMethod::class.java)
+
+ if (currentScreen is PaymentSheetScreen.EditPaymentMethod) {
+ val interactor = currentScreen.interactor
+
+ interactor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceOptionsDismissed
+ )
+
+ verify(eventReporter).onHidePaymentOptionBrands(
+ source = EventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = null
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `correct event is sent when dropdown is dismissed with change in EditPaymentMethod`() = runTest {
+ val paymentMethods = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ customerPaymentMethods = paymentMethods
+ )
+
+ viewModel.currentScreen.test {
+ awaitItem()
+
+ viewModel.modifyPaymentMethod(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val currentScreen = awaitItem()
+
+ assertThat(currentScreen).isInstanceOf(PaymentSheetScreen.EditPaymentMethod::class.java)
+
+ if (currentScreen is PaymentSheetScreen.EditPaymentMethod) {
+ val interactor = currentScreen.interactor
+
+ interactor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceChanged(
+ EditPaymentMethodViewState.CardBrandChoice(CardBrand.Visa)
+ )
+ )
+
+ verify(eventReporter).onHidePaymentOptionBrands(
+ source = EventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.Visa
+ )
+ }
+ }
+ }
+
+ @Test
+ fun `modifyPaymentMethod updates payment methods and sends event on successful update`() = runTest {
val paymentMethods = PaymentMethodFixtures.createCards(5)
val firstPaymentMethod = paymentMethods.first()
@@ -243,6 +343,8 @@ internal class PaymentSheetViewModelTest {
assertThat(awaitItem()).isInstanceOf(SelectSavedPaymentMethods::class.java)
}
+ verify(eventReporter).onUpdatePaymentMethodSucceeded(CardBrand.Visa)
+
val idCaptor = argumentCaptor()
val paramsCaptor = argumentCaptor()
@@ -269,6 +371,54 @@ internal class PaymentSheetViewModelTest {
)
}
+ @Test
+ fun `modifyPaymentMethod sends event on failed update`() = runTest {
+ val paymentMethods = PaymentMethodFixtures.createCards(5)
+
+ val firstPaymentMethod = paymentMethods.first()
+
+ val customerRepository = spy(
+ FakeCustomerRepository(
+ onUpdatePaymentMethod = {
+ Result.failure(Exception("No network found!"))
+ }
+ )
+ )
+ val viewModel = createViewModel(
+ customerPaymentMethods = paymentMethods,
+ customerRepository = customerRepository
+ )
+
+ viewModel.currentScreen.test {
+ awaitItem()
+
+ viewModel.modifyPaymentMethod(firstPaymentMethod)
+
+ val currentScreen = awaitItem()
+
+ assertThat(currentScreen).isInstanceOf(PaymentSheetScreen.EditPaymentMethod::class.java)
+
+ if (currentScreen is PaymentSheetScreen.EditPaymentMethod) {
+ val interactor = currentScreen.interactor
+
+ interactor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceChanged(
+ EditPaymentMethodViewState.CardBrandChoice(CardBrand.Visa)
+ )
+ )
+
+ interactor.handleViewAction(EditPaymentMethodViewAction.OnUpdatePressed)
+ }
+ }
+
+ verify(eventReporter).onUpdatePaymentMethodFailed(
+ selectedBrand = eq(CardBrand.Visa),
+ error = argThat {
+ message == "No network found!"
+ }
+ )
+ }
+
@Test
fun `checkout() should confirm saved card payment methods`() = runTest {
val stripeIntent = PAYMENT_INTENT
@@ -1173,6 +1323,35 @@ internal class PaymentSheetViewModelTest {
verifyNoMoreInteractions(eventReporter)
}
+ @Test
+ fun `Sends correct event when navigating to EditPaymentMethod screen`() = runTest {
+ val cards = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD,
+ customerPaymentMethods = cards,
+ )
+
+ viewModel.modifyPaymentMethod(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ verify(eventReporter).onShowEditablePaymentOption()
+ }
+
+ @Test
+ fun `Sends correct event when navigating out of EditPaymentMethod screen`() = runTest {
+ val cards = listOf(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+
+ val viewModel = createViewModel(
+ stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD,
+ customerPaymentMethods = cards,
+ )
+
+ viewModel.modifyPaymentMethod(CARD_WITH_NETWORKS_PAYMENT_METHOD)
+ viewModel.handleBackPressed()
+
+ verify(eventReporter).onHideEditablePaymentOption()
+ }
+
@Test
fun `Sets editing to false when removing the last payment method while editing`() = runTest {
val customerPaymentMethods = PaymentMethodFixtures.createCards(1)
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 da5d31e7434..b22c26763a4 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
@@ -7,6 +7,7 @@ import com.stripe.android.PaymentConfiguration
import com.stripe.android.core.exception.APIException
import com.stripe.android.core.networking.AnalyticsRequest
import com.stripe.android.core.networking.AnalyticsRequestExecutor
+import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethodFixtures
import com.stripe.android.networking.PaymentAnalyticsRequestFactory
import com.stripe.android.paymentsheet.PaymentSheet
@@ -217,6 +218,114 @@ class DefaultEventReporterTest {
)
}
+ @Test
+ fun `onShowEditablePaymentOption() should fire analytics request with expected event value`() {
+ val customEventReporter = createEventReporter(EventReporter.Mode.Custom) {
+ simulateSuccessfulSetup()
+ }
+
+ customEventReporter.onShowEditablePaymentOption()
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == "mc_open_edit_screen"
+ }
+ )
+ }
+
+ @Test
+ fun `onHideEditablePaymentOption() should fire analytics request with expected event value`() {
+ val customEventReporter = createEventReporter(EventReporter.Mode.Custom) {
+ simulateSuccessfulSetup()
+ }
+
+ customEventReporter.onHideEditablePaymentOption()
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == "mc_cancel_edit_screen"
+ }
+ )
+ }
+
+ @Test
+ fun `onShowPaymentOptionBrands() should fire analytics request with expected event value`() {
+ val customEventReporter = createEventReporter(EventReporter.Mode.Custom) {
+ simulateSuccessfulSetup()
+ }
+
+ customEventReporter.onShowPaymentOptionBrands(
+ source = EventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.Visa
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == "mc_open_cbc_dropdown" &&
+ req.params["cbc_event_source"] == "edit" &&
+ req.params["selected_card_brand"] == "visa"
+ }
+ )
+ }
+
+ @Test
+ fun `onHidePaymentOptionBrands() should fire analytics request with expected event value`() {
+ val customEventReporter = createEventReporter(EventReporter.Mode.Custom) {
+ simulateSuccessfulSetup()
+ }
+
+ customEventReporter.onHidePaymentOptionBrands(
+ source = EventReporter.CardBrandChoiceEventSource.Edit,
+ selectedBrand = CardBrand.CartesBancaires,
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == "mc_close_cbc_dropdown" &&
+ req.params["cbc_event_source"] == "edit" &&
+ req.params["selected_card_brand"] == "cartes_bancaires"
+ }
+ )
+ }
+
+ @Test
+ fun `onUpdatePaymentMethodSucceeded() should fire analytics request with expected event value`() {
+ val customEventReporter = createEventReporter(EventReporter.Mode.Custom) {
+ simulateSuccessfulSetup()
+ }
+
+ customEventReporter.onUpdatePaymentMethodSucceeded(
+ selectedBrand = CardBrand.CartesBancaires,
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == "mc_update_card" &&
+ req.params["selected_card_brand"] == "cartes_bancaires"
+ }
+ )
+ }
+
+ @Test
+ fun `onUpdatePaymentMethodFailed() should fire analytics request with expected event value`() {
+ val customEventReporter = createEventReporter(EventReporter.Mode.Custom) {
+ simulateSuccessfulSetup()
+ }
+
+ customEventReporter.onUpdatePaymentMethodFailed(
+ selectedBrand = CardBrand.CartesBancaires,
+ error = Exception("No network available!")
+ )
+
+ verify(analyticsRequestExecutor).executeAsync(
+ argWhere { req ->
+ req.params["event"] == "mc_update_card_failed" &&
+ req.params["selected_card_brand"] == "cartes_bancaires" &&
+ req.params["error_message"] == "No network available!"
+ }
+ )
+ }
+
@Test
fun `constructor does not read from PaymentConfiguration`() {
PaymentConfiguration.clearInstance()
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 39ee6bac2f5..824564309f4 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
@@ -3,6 +3,7 @@ package com.stripe.android.paymentsheet.analytics
import com.google.common.truth.Truth.assertThat
import com.stripe.android.core.exception.APIException
import com.stripe.android.link.LinkPaymentDetails
+import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentDetailsFixtures
import com.stripe.android.model.PaymentMethodCreateParamsFixtures
import com.stripe.android.model.PaymentMethodFixtures
@@ -15,6 +16,7 @@ import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
@RunWith(RobolectricTestRunner::class)
+@Suppress("LargeClass")
class PaymentSheetEventTest {
@Test
@@ -60,6 +62,7 @@ class PaymentSheetEventTest {
"phone" to "Automatic",
"address" to "Automatic",
),
+ "preferred_networks" to null,
)
assertThat(event.params).run {
@@ -70,6 +73,60 @@ class PaymentSheetEventTest {
@Test
fun `Init event with minimum config should return expected params`() {
+ val event = PaymentSheetEvent.Init(
+ mode = EventReporter.Mode.Complete,
+ configuration = PaymentSheetFixtures.CONFIG_MINIMUM.copy(
+ preferredNetworks = listOf(CardBrand.CartesBancaires, CardBrand.Visa)
+ ),
+ isDeferred = false,
+ )
+
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_complete_init_default"
+ )
+
+ val expectedConfig = mapOf(
+ "customer" to false,
+ "googlepay" to false,
+ "primary_button_color" to false,
+ "default_billing_details" to false,
+ "allows_delayed_payment_methods" to false,
+ "appearance" to mapOf(
+ "colorsLight" to false,
+ "colorsDark" to false,
+ "corner_radius" to false,
+ "border_width" to false,
+ "font" to false,
+ "size_scale_factor" to false,
+ "primary_button" to mapOf(
+ "colorsLight" to false,
+ "colorsDark" to false,
+ "corner_radius" to false,
+ "border_width" to false,
+ "font" to false,
+ ),
+ "usage" to false,
+ ),
+ "billing_details_collection_configuration" to mapOf(
+ "attach_defaults" to false,
+ "name" to "Automatic",
+ "email" to "Automatic",
+ "phone" to "Automatic",
+ "address" to "Automatic",
+ ),
+ "preferred_networks" to "cartes_bancaires, visa",
+ )
+
+ assertThat(event.params).run {
+ containsEntry("is_decoupled", false)
+ containsEntry("mpe_config", expectedConfig)
+ }
+ }
+
+ @Test
+ fun `Init event with preferred networks`() {
val event = PaymentSheetEvent.Init(
mode = EventReporter.Mode.Complete,
configuration = PaymentSheetFixtures.CONFIG_MINIMUM,
@@ -111,6 +168,7 @@ class PaymentSheetEventTest {
"phone" to "Automatic",
"address" to "Automatic",
),
+ "preferred_networks" to null,
)
assertThat(event.params).run {
@@ -455,6 +513,181 @@ class PaymentSheetEventTest {
)
}
+ @Test
+ fun `ShowEditablePaymentOption event should return expected toString()`() {
+ val event = PaymentSheetEvent.ShowEditablePaymentOption(
+ isDeferred = false,
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_open_edit_screen",
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "is_decoupled" to false,
+ )
+ )
+ }
+
+ @Test
+ fun `HideEditablePaymentOption event should return expected toString()`() {
+ val event = PaymentSheetEvent.HideEditablePaymentOption(
+ isDeferred = false,
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_cancel_edit_screen",
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "is_decoupled" to false,
+ )
+ )
+ }
+
+ @Test
+ fun `ShowPaymentOptionBrands event with edit source should return expected toString()`() {
+ val event = PaymentSheetEvent.ShowPaymentOptionBrands(
+ selectedBrand = CardBrand.Visa,
+ source = PaymentSheetEvent.ShowPaymentOptionBrands.Source.Edit,
+ isDeferred = false,
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_open_cbc_dropdown"
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "cbc_event_source" to "edit",
+ "selected_card_brand" to "visa",
+ "is_decoupled" to false,
+ )
+ )
+ }
+
+ @Test
+ fun `ShowPaymentOptionBrands event with add source should return expected toString()`() {
+ val event = PaymentSheetEvent.ShowPaymentOptionBrands(
+ selectedBrand = CardBrand.Visa,
+ source = PaymentSheetEvent.ShowPaymentOptionBrands.Source.Add,
+ isDeferred = false,
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_open_cbc_dropdown"
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "cbc_event_source" to "add",
+ "selected_card_brand" to "visa",
+ "is_decoupled" to false,
+ )
+ )
+ }
+
+ @Test
+ fun `HidePaymentOptionBrands event with add source should return expected toString()`() {
+ val event = PaymentSheetEvent.HidePaymentOptionBrands(
+ selectedBrand = CardBrand.CartesBancaires,
+ source = PaymentSheetEvent.HidePaymentOptionBrands.Source.Add,
+ isDeferred = false,
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_close_cbc_dropdown"
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "cbc_event_source" to "add",
+ "selected_card_brand" to "cartes_bancaires",
+ "is_decoupled" to false,
+ )
+ )
+ }
+
+ @Test
+ fun `HidePaymentOptionBrands event with edit source should return expected toString()`() {
+ val event = PaymentSheetEvent.HidePaymentOptionBrands(
+ selectedBrand = CardBrand.CartesBancaires,
+ source = PaymentSheetEvent.HidePaymentOptionBrands.Source.Edit,
+ isDeferred = false,
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_close_cbc_dropdown"
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "cbc_event_source" to "edit",
+ "selected_card_brand" to "cartes_bancaires",
+ "is_decoupled" to false,
+ )
+ )
+ }
+
+ @Test
+ fun `UpdatePaymentOptionSucceeded event should return expected toString()`() {
+ val event = PaymentSheetEvent.UpdatePaymentOptionSucceeded(
+ selectedBrand = CardBrand.CartesBancaires,
+ isDeferred = false,
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_update_card"
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "selected_card_brand" to "cartes_bancaires",
+ "is_decoupled" to false,
+ )
+ )
+ }
+
+ @Test
+ fun `UpdatePaymentOptionFailed event should return expected toString()`() {
+ val event = PaymentSheetEvent.UpdatePaymentOptionFailed(
+ selectedBrand = CardBrand.CartesBancaires,
+ error = Exception("No network available!"),
+ isDeferred = false,
+
+ )
+ assertThat(
+ event.eventName
+ ).isEqualTo(
+ "mc_update_card_failed"
+ )
+ assertThat(
+ event.params
+ ).isEqualTo(
+ mapOf(
+ "selected_card_brand" to "cartes_bancaires",
+ "error_message" to "No network available!",
+ "is_decoupled" to false,
+ )
+ )
+ }
+
@Test
fun `Init event should have default params if config is all defaults`() {
val expectedPrimaryButton = mapOf(
@@ -489,6 +722,7 @@ class PaymentSheetEventTest {
"allows_delayed_payment_methods" to false,
"appearance" to expectedAppearance,
"billing_details_collection_configuration" to expectedBillingDetailsCollection,
+ "preferred_networks" to null,
)
assertThat(
PaymentSheetEvent.Init(
@@ -538,6 +772,7 @@ class PaymentSheetEventTest {
"allows_delayed_payment_methods" to true,
"appearance" to expectedAppearance,
"billing_details_collection_configuration" to expectedBillingDetailsCollection,
+ "preferred_networks" to null,
)
assertThat(
PaymentSheetEvent.Init(
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/ui/DefaultEditPaymentMethodViewInteractorTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/ui/DefaultEditPaymentMethodViewInteractorTest.kt
index 0e2039605e5..67cabd05690 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/ui/DefaultEditPaymentMethodViewInteractorTest.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/ui/DefaultEditPaymentMethodViewInteractorTest.kt
@@ -46,31 +46,62 @@ class DefaultEditPaymentMethodViewInteractorTest {
}
@Test
- fun `on selection changed, 'canUpdate' should be true & selected brand should be changed`() = runTest {
- val interactor = createInteractor()
+ fun `on selection options shown, should report event specific to showing payment option brands`() = runTest {
+ val eventHandler = mock<(EditPaymentMethodViewInteractor.Event) -> Unit>()
+ val interactor = createInteractor(eventHandler = eventHandler)
interactor.handleViewAction(
- EditPaymentMethodViewAction.OnBrandChoiceChanged(VISA_BRAND_CHOICE)
+ EditPaymentMethodViewAction.OnBrandChoiceOptionsShown
)
- interactor.viewState.test {
- val state = awaitItem()
+ verify(eventHandler).invoke(
+ EditPaymentMethodViewInteractor.Event.ShowBrands(brand = CardBrand.CartesBancaires)
+ )
+ }
- assertThat(state).isEqualTo(
- EditPaymentMethodViewState(
- status = EditPaymentMethodViewState.Status.Idle,
- last4 = "4242",
- canUpdate = true,
- selectedBrand = VISA_BRAND_CHOICE,
- availableBrands = listOf(
- VISA_BRAND_CHOICE,
- CARTES_BANCAIRES_BRAND_CHOICE
- ),
- displayName = "Card",
- )
+ @Test
+ fun `on selection options hidden without change, should report event specific to hiding payment option brands`() =
+ runTest {
+ val eventHandler = mock<(EditPaymentMethodViewInteractor.Event) -> Unit>()
+ val interactor = createInteractor(eventHandler = eventHandler)
+
+ interactor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceOptionsDismissed
)
+
+ verify(eventHandler).invoke(EditPaymentMethodViewInteractor.Event.HideBrands(brand = null))
+ }
+
+ @Test
+ fun `on selection changed, should report event, 'canUpdate' should be true & selected brand should be changed`() =
+ runTest {
+ val eventHandler = mock<(EditPaymentMethodViewInteractor.Event) -> Unit>()
+ val interactor = createInteractor(eventHandler = eventHandler)
+
+ interactor.handleViewAction(
+ EditPaymentMethodViewAction.OnBrandChoiceChanged(VISA_BRAND_CHOICE)
+ )
+
+ verify(eventHandler).invoke(EditPaymentMethodViewInteractor.Event.HideBrands(brand = CardBrand.Visa))
+
+ interactor.viewState.test {
+ val state = awaitItem()
+
+ assertThat(state).isEqualTo(
+ EditPaymentMethodViewState(
+ status = EditPaymentMethodViewState.Status.Idle,
+ last4 = "4242",
+ canUpdate = true,
+ selectedBrand = VISA_BRAND_CHOICE,
+ availableBrands = listOf(
+ VISA_BRAND_CHOICE,
+ CARTES_BANCAIRES_BRAND_CHOICE
+ ),
+ displayName = "Card",
+ )
+ )
+ }
}
- }
@Test
fun `on selection changed & reverted, 'canUpdate' should be false`() = runTest {
@@ -229,6 +260,7 @@ class DefaultEditPaymentMethodViewInteractorTest {
}
private fun createInteractor(
+ eventHandler: (EditPaymentMethodViewInteractor.Event) -> Unit = {},
onRemove: PaymentMethodRemoveOperation = { null },
onUpdate: PaymentMethodUpdateOperation = { _, _ -> Result.success(CARD_WITH_NETWORKS_PAYMENT_METHOD) },
workContext: CoroutineContext = UnconfinedTestDispatcher()
@@ -236,6 +268,7 @@ class DefaultEditPaymentMethodViewInteractorTest {
return DefaultEditPaymentMethodViewInteractor(
initialPaymentMethod = CARD_WITH_NETWORKS_PAYMENT_METHOD,
displayName = "Card",
+ eventHandler = eventHandler,
removeExecutor = onRemove,
updateExecutor = onUpdate,
viewStateSharingStarted = SharingStarted.Eagerly,
diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/FakeEditPaymentMethodInteractorFactory.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/FakeEditPaymentMethodInteractorFactory.kt
index 0cbcfda9df9..b6ede46b2fd 100644
--- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/FakeEditPaymentMethodInteractorFactory.kt
+++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/utils/FakeEditPaymentMethodInteractorFactory.kt
@@ -2,6 +2,7 @@ package com.stripe.android.paymentsheet.utils
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.ui.DefaultEditPaymentMethodViewInteractor
+import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.ModifiableEditPaymentMethodViewInteractor
import com.stripe.android.paymentsheet.ui.PaymentMethodRemoveOperation
import com.stripe.android.paymentsheet.ui.PaymentMethodUpdateOperation
@@ -14,12 +15,14 @@ internal class FakeEditPaymentMethodInteractorFactory(
) : ModifiableEditPaymentMethodViewInteractor.Factory {
override fun create(
initialPaymentMethod: PaymentMethod,
+ eventHandler: (EditPaymentMethodViewInteractor.Event) -> Unit,
removeExecutor: PaymentMethodRemoveOperation,
updateExecutor: PaymentMethodUpdateOperation,
displayName: String
): ModifiableEditPaymentMethodViewInteractor {
return DefaultEditPaymentMethodViewInteractor(
initialPaymentMethod = initialPaymentMethod,
+ eventHandler = eventHandler,
removeExecutor = removeExecutor,
updateExecutor = updateExecutor,
displayName = displayName,