Skip to content

Report event when card input requires more than 16 digits #10897

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.stripe.android.networking

import androidx.annotation.Keep
import androidx.annotation.RestrictTo
import com.stripe.android.core.networking.AnalyticsEvent

internal enum class PaymentAnalyticsEvent(val code: String) : AnalyticsEvent {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
enum class PaymentAnalyticsEvent(val code: String) : AnalyticsEvent {
// Token
TokenCreate("token_creation"),

Expand Down Expand Up @@ -104,7 +106,10 @@ internal enum class PaymentAnalyticsEvent(val code: String) : AnalyticsEvent {

CardMetadataLoadedTooSlow("card_metadata_loaded_too_slow"),
CardMetadataLoadFailure("card_metadata_load_failure"),
CardMetadataMissingRange("card_metadata_missing_range");
CardMetadataMissingRange("card_metadata_missing_range"),
CardMetadataExpectedExtraDigitsButUserEntered16ThenSwitchedFields(
"card_metadata.expected_extra_digits_but_user_entered_16_then_switched_fields"
);

@Keep
override fun toString(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.model.AccountRange
import com.stripe.android.model.CardBrand
import com.stripe.android.networking.PaymentAnalyticsEvent
import com.stripe.android.stripecardscan.cardscan.CardScanSheetResult
import com.stripe.android.ui.core.R
import com.stripe.android.ui.core.elements.events.LocalAnalyticsEventReporter
import com.stripe.android.ui.core.elements.events.LocalCardBrandDisallowedReporter
import com.stripe.android.ui.core.elements.events.LocalCardNumberCompletedEventReporter
import com.stripe.android.uicore.elements.FieldError
Expand All @@ -45,6 +47,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.drop
import kotlin.coroutines.CoroutineContext
import com.stripe.android.R as PaymentsCoreR
Expand Down Expand Up @@ -368,9 +371,13 @@ internal class DefaultCardNumberController(
) {
val reporter = LocalCardNumberCompletedEventReporter.current
val disallowedBrandReporter = LocalCardBrandDisallowedReporter.current
val analyticsEventReporter = LocalAnalyticsEventReporter.current

// Remember the last state indicating whether it was a disallowed card brand error
var lastLoggedCardBrand by rememberSaveable { mutableStateOf<CardBrand?>(null) }
var hasReportedIncompleteCardNumberRequiringMoreThan16Digits by rememberSaveable {
mutableStateOf(false)
}

LaunchedEffect(Unit) {
// Drop the set empty value & initial value
Expand All @@ -395,6 +402,27 @@ internal class DefaultCardNumberController(
}
}

LaunchedEffect(Unit) {
combine(
fieldState.drop(1),
fieldValue,
_hasFocus,
) { state, fieldValue, hasFocus ->
state is TextFieldStateConstants.Error.Incomplete &&
!hasFocus &&
!hasReportedIncompleteCardNumberRequiringMoreThan16Digits &&
fieldValue.length == CARD_NUMBER_16_DIGITS
}.collectLatest {
if (it) {
analyticsEventReporter.onAnalyticsEvent(
PaymentAnalyticsEvent.CardMetadataExpectedExtraDigitsButUserEntered16ThenSwitchedFields
)

hasReportedIncompleteCardNumberRequiringMoreThan16Digits = true
}
}
}

super.ComposeUI(
enabled,
field,
Expand All @@ -408,5 +436,6 @@ internal class DefaultCardNumberController(

private companion object {
const val STATIC_ICON_COUNT = 3
const val CARD_NUMBER_16_DIGITS = 16
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.stripe.android.ui.core.elements.events

import androidx.annotation.RestrictTo
import androidx.compose.runtime.staticCompositionLocalOf
import com.stripe.android.core.networking.AnalyticsEvent
import com.stripe.android.uicore.BuildConfig

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
fun interface AnalyticsEventReporter {
fun onAnalyticsEvent(event: AnalyticsEvent)
}

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
val LocalAnalyticsEventReporter =
staticCompositionLocalOf<AnalyticsEventReporter> {
EmptyAnalyticsEventReporter
}

private object EmptyAnalyticsEventReporter : AnalyticsEventReporter {
override fun onAnalyticsEvent(event: AnalyticsEvent) {
if (BuildConfig.DEBUG) {
error(
"AnalyticsEventReporter.${event.eventName} was not reported"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package com.stripe.android.ui.core.elements

import androidx.compose.material.TextField
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.LayoutDirection
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
Expand All @@ -17,8 +22,11 @@ import com.stripe.android.cards.StaticCardAccountRangeSource
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.model.AccountRange
import com.stripe.android.model.CardBrand
import com.stripe.android.networking.PaymentAnalyticsEvent
import com.stripe.android.ui.core.elements.events.AnalyticsEventReporter
import com.stripe.android.ui.core.elements.events.CardBrandDisallowedReporter
import com.stripe.android.ui.core.elements.events.CardNumberCompletedEventReporter
import com.stripe.android.ui.core.elements.events.LocalAnalyticsEventReporter
import com.stripe.android.ui.core.elements.events.LocalCardBrandDisallowedReporter
import com.stripe.android.ui.core.elements.events.LocalCardNumberCompletedEventReporter
import com.stripe.android.uicore.elements.IdentifierSpec
Expand All @@ -39,6 +47,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verifyNoInteractions
import org.robolectric.RobolectricTestRunner
Expand Down Expand Up @@ -507,6 +516,64 @@ internal class CardNumberControllerTest {
}
}

@Test
fun `on card number incomplete requiring more than 16 digits, should report event`() {
val eventReporter: AnalyticsEventReporter = mock()

val cardNumberController = createController()

composeTestRule.setContent {
CompositionLocalProvider(
LocalAnalyticsEventReporter provides eventReporter
) {
cardNumberController.ComposeUI(
enabled = true,
field = SimpleTextElement(
identifier = IdentifierSpec.Name,
controller = SimpleTextFieldController(
textFieldConfig = SimpleTextFieldConfig()
),
),
modifier = Modifier.testTag(TEST_TAG),
hiddenIdentifiers = emptySet(),
lastTextFieldIdentifier = null,
nextFocusDirection = FocusDirection.Next,
previousFocusDirection = FocusDirection.Next
)
TextField(
modifier = Modifier.testTag("SECOND_TEXT_FIELD"),
value = TextFieldValue(),
onValueChange = {},
)
}
}

composeTestRule.onNode(hasTestTag(TEST_TAG)).performClick()
composeTestRule.waitForIdle()

composeTestRule.onNode(hasTestTag(TEST_TAG)).performTextInput("621682805")
composeTestRule.waitForIdle()

composeTestRule.onNode(hasTestTag(TEST_TAG)).performTextInput("764")
composeTestRule.waitForIdle()

composeTestRule.onNode(hasTestTag(TEST_TAG)).performTextInput("2532")
composeTestRule.waitForIdle()

verify(eventReporter, never())
.onAnalyticsEvent(
PaymentAnalyticsEvent.CardMetadataExpectedExtraDigitsButUserEntered16ThenSwitchedFields
)

composeTestRule.onNode(hasTestTag("SECOND_TEXT_FIELD")).performClick()
composeTestRule.waitForIdle()

verify(eventReporter, times(1))
.onAnalyticsEvent(
PaymentAnalyticsEvent.CardMetadataExpectedExtraDigitsButUserEntered16ThenSwitchedFields
)
}

@Test
fun `on initial number completed, should not report event`() = runTest {
val eventReporter: CardNumberCompletedEventReporter = mock()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.customersheet

import com.stripe.android.core.networking.AnalyticsEvent
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.lpmfoundations.luxe.SupportedPaymentMethod
import com.stripe.android.model.CardBrand
Expand All @@ -13,6 +14,7 @@ internal sealed class CustomerSheetViewAction {
object OnBackPressed : CustomerSheetViewAction()
object OnEditPressed : CustomerSheetViewAction()
object OnCardNumberInputCompleted : CustomerSheetViewAction()
class OnAnalyticsEvent(val event: AnalyticsEvent) : CustomerSheetViewAction()
object OnAddCardPressed : CustomerSheetViewAction()
object OnPrimaryButtonPressed : CustomerSheetViewAction()
object OnCancelClose : CustomerSheetViewAction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.stripe.android.core.Logger
import com.stripe.android.core.exception.StripeException
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.injection.IS_LIVE_MODE
import com.stripe.android.core.networking.AnalyticsEvent
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.orEmpty
Expand Down Expand Up @@ -271,6 +272,7 @@ internal class CustomerSheetViewModel(
is CustomerSheetViewAction.OnAddCardPressed -> onAddCardPressed()
is CustomerSheetViewAction.OnCardNumberInputCompleted -> onCardNumberInputCompleted()
is CustomerSheetViewAction.OnDisallowedCardBrandEntered -> onDisallowedCardBrandEntered(viewAction.brand)
is CustomerSheetViewAction.OnAnalyticsEvent -> onAnalyticsEvent(viewAction.event)
is CustomerSheetViewAction.OnBackPressed -> onBackPressed()
is CustomerSheetViewAction.OnEditPressed -> onEditPressed()
is CustomerSheetViewAction.OnModifyItem -> onModifyItem(viewAction.paymentMethod)
Expand Down Expand Up @@ -917,6 +919,10 @@ internal class CustomerSheetViewModel(
eventReporter.onCardNumberCompleted()
}

private fun onAnalyticsEvent(event: AnalyticsEvent) {
eventReporter.onAnalyticsEvent(event)
}

private fun onDisallowedCardBrandEntered(brand: CardBrand) {
eventReporter.onDisallowedCardBrandEntered(brand)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.customersheet.analytics

import com.stripe.android.core.networking.AnalyticsEvent
import com.stripe.android.customersheet.CustomerSheet
import com.stripe.android.customersheet.CustomerSheetIntegration
import com.stripe.android.model.CardBrand
Expand Down Expand Up @@ -129,6 +130,8 @@ internal interface CustomerSheetEventReporter {

fun onDisallowedCardBrandEntered(brand: CardBrand)

fun onAnalyticsEvent(event: AnalyticsEvent)

enum class Screen(val value: String) {
AddPaymentMethod("add_payment_method"),
SelectPaymentMethod("select_payment_method"),
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.injection.IOContext
import com.stripe.android.core.networking.AnalyticsEvent
import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.core.networking.AnalyticsRequestFactory
import com.stripe.android.customersheet.CustomerSheet
Expand Down Expand Up @@ -215,6 +216,17 @@ internal class DefaultCustomerSheetEventReporter @Inject constructor(
)
}

override fun onAnalyticsEvent(event: AnalyticsEvent) {
CoroutineScope(workContext).launch {
analyticsRequestExecutor.executeAsync(
analyticsRequestFactory.createRequest(
event = event,
additionalParams = emptyMap(),
)
)
}
}

private fun fireEvent(event: CustomerSheetEvent) {
CoroutineScope(workContext).launch {
analyticsRequestExecutor.executeAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
import com.stripe.android.common.ui.BottomSheetLoadingIndicator
import com.stripe.android.common.ui.BottomSheetScaffold
import com.stripe.android.common.ui.PrimaryButton
import com.stripe.android.core.networking.AnalyticsEvent
import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.customersheet.CustomerSheetViewAction
import com.stripe.android.customersheet.CustomerSheetViewModel
Expand All @@ -35,8 +36,10 @@ import com.stripe.android.paymentsheet.utils.PaymentSheetContentPadding
import com.stripe.android.ui.core.elements.H4Text
import com.stripe.android.ui.core.elements.Mandate
import com.stripe.android.ui.core.elements.SimpleDialogElementUI
import com.stripe.android.ui.core.elements.events.AnalyticsEventReporter
import com.stripe.android.ui.core.elements.events.CardBrandDisallowedReporter
import com.stripe.android.ui.core.elements.events.CardNumberCompletedEventReporter
import com.stripe.android.ui.core.elements.events.LocalAnalyticsEventReporter
import com.stripe.android.ui.core.elements.events.LocalCardBrandDisallowedReporter
import com.stripe.android.ui.core.elements.events.LocalCardNumberCompletedEventReporter
import com.stripe.android.uicore.strings.resolve
Expand Down Expand Up @@ -231,11 +234,15 @@ internal fun AddPaymentMethod(
DefaultCardBrandDisallowedReporter(viewActionHandler)
}

val analyticsEventReporter = remember(viewActionHandler) {
DefaultAnalyticsEventReporter(viewActionHandler)
}

if (displayForm) {
CompositionLocalProvider(
LocalCardNumberCompletedEventReporter provides eventReporter,
LocalCardBrandDisallowedReporter provides disallowedReporter

LocalCardBrandDisallowedReporter provides disallowedReporter,
LocalAnalyticsEventReporter provides analyticsEventReporter,
) {
PaymentElement(
enabled = viewState.enabled,
Expand Down Expand Up @@ -334,6 +341,14 @@ const val CUSTOMER_SHEET_CONFIRM_BUTTON_TEST_TAG = "CustomerSheetConfirmButton"
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
const val CUSTOMER_SHEET_SAVE_BUTTON_TEST_TAG = "CustomerSheetSaveButton"

private class DefaultAnalyticsEventReporter(
private val viewActionHandler: (event: CustomerSheetViewAction) -> Unit
) : AnalyticsEventReporter {
override fun onAnalyticsEvent(event: AnalyticsEvent) {
viewActionHandler.invoke(CustomerSheetViewAction.OnAnalyticsEvent(event))
}
}

private class DefaultCardNumberCompletedEventReporter(
private val viewActionHandler: (CustomerSheetViewAction) -> Unit
) : CardNumberCompletedEventReporter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import com.stripe.android.common.analytics.experiment.LoggableExperiment
import com.stripe.android.common.model.CommonConfiguration
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.networking.AnalyticsEvent
import com.stripe.android.core.networking.AnalyticsRequestExecutor
import com.stripe.android.core.networking.AnalyticsRequestV2Executor
import com.stripe.android.core.networking.AnalyticsRequestV2Factory
Expand Down Expand Up @@ -548,6 +549,17 @@ internal class DefaultEventReporter @Inject internal constructor(
fireEvent(analyticsEvent)
}

override fun onAnalyticsEvent(event: AnalyticsEvent) {
CoroutineScope(workContext).launch {
analyticsRequestExecutor.executeAsync(
paymentAnalyticsRequestFactory.createRequest(
event = event,
additionalParams = emptyMap(),
)
)
}
}

private fun fireEvent(event: PaymentSheetEvent) {
CoroutineScope(workContext).launch {
analyticsRequestExecutor.executeAsync(
Expand Down
Loading
Loading