Skip to content

Commit

Permalink
Add Card Brand Choice eventing support in edit screen for `PaymentShe…
Browse files Browse the repository at this point in the history
…et` & `CustomerSheet` (#7751)

* Add Card Brand Choice eventing support in edit screen for `PaymentSheet` & `CustomerSheet`

* Add source to `HidePaymentOptionBrands` events & fix nits.
  • Loading branch information
samer-stripe authored Jan 15, 2024
1 parent 7417a06 commit 0b54f11
Show file tree
Hide file tree
Showing 22 changed files with 1,446 additions and 57 deletions.
2 changes: 2 additions & 0 deletions paymentsheet/detekt-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@
<ID>NestedBlockDepth:SupportedPaymentMethodTest.kt$SupportedPaymentMethodTest$private fun generatePaymentIntentScenarios(): List&lt;PaymentIntentTestInput></ID>
<ID>ReturnCount:AddressUtils.kt$internal fun CharSequence.levenshtein(other: CharSequence): Int</ID>
<ID>ThrowsCount:PaymentSheetConfigurationKtx.kt$internal fun PaymentSheet.Configuration.validate()</ID>
<ID>TooManyFunctions:CustomerSheetEventReporter.kt$CustomerSheetEventReporter</ID>
<ID>TooManyFunctions:DefaultCustomerSheetEventReporter.kt$DefaultCustomerSheetEventReporter : CustomerSheetEventReporter</ID>
<ID>TooManyFunctions:DefaultEventReporter.kt$DefaultEventReporter : EventReporter</ID>
<ID>TooManyFunctions:DefaultFlowController.kt$DefaultFlowController : FlowController</ID>
<ID>TooManyFunctions:EventReporter.kt$EventReporter</ID>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -330,6 +331,10 @@ internal class CustomerSheetViewModel @Inject constructor(
)
} else {
backStack.update {
it.last().eventReporterScreen?.let { screen ->
eventReporter.onScreenHidden(screen)
}

it.dropLast(1)
}
}
Expand Down Expand Up @@ -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
)
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 -> { }
}

Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -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<String, Any?> = 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!"
)
}
}

Expand Down Expand Up @@ -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<String, Any?> = 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<String, Any?> = 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<String, Any?> = 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<String, Any?> = 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"
Expand Down Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package com.stripe.android.customersheet.analytics

import com.stripe.android.model.CardBrand

internal interface CustomerSheetEventReporter {

/**
* [Screen] was presented to user
*/
fun onScreenPresented(screen: Screen)

/**
* [Screen] was hidden to user
*/
fun onScreenHidden(screen: Screen)

/**
* User attempted to confirm their saved payment method selection and succeeded
*/
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 0b54f11

Please sign in to comment.