From c9e5bcbbcab7a9139c5835351a6da60262fe3dac Mon Sep 17 00:00:00 2001 From: tianzhao-stripe Date: Tue, 18 Nov 2025 15:43:26 -0800 Subject: [PATCH 1/5] create layoutcoordinateInitialVisibilityTracker --- ...ayoutCoordinateInitialVisibilityTracker.kt | 96 +++++++++++++++++++ .../PaymentMethodInitialVisibilityTracker.kt | 92 +++--------------- .../PaymentMethodVerticalLayoutInteractor.kt | 2 +- 3 files changed, 110 insertions(+), 80 deletions(-) create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt new file mode 100644 index 00000000000..6ceab84fa09 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt @@ -0,0 +1,96 @@ +package com.stripe.android.paymentsheet.verticalmode + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.unit.IntSize + +internal abstract class LayoutCoordinateInitialVisibilityTracker( + var expectedItems: List, + val visibilityThreshold: Int, +){ + + protected data class CoordinateSnapshot( + val positionInWindow: Offset, + val size: IntSize, + val boundsInWindow: Rect + ) + + var hasDispatched = false + protected set + + fun updateVisibility(itemCode: String, coordinates: LayoutCoordinates) { + if (itemCode !in expectedItems || expectedItems.isEmpty()) return + if (hasDispatched) return // Only dispatch once per tracker instance + /** + * Capture only the relevant fields from [LayoutCoordinates] when we know the coordinates are attached. + * Whether previous coordinates is/isn't attached or not is irrelevant, we only need to know + * the previous coordinate's boundsInWindow, positionInWindow, and size. + * + * When implemented, [LayoutCoordinates.isAttached] provides a getter to a var, so we cannot rely on being able + * to call `positionInWindow` and `boundsInWindow` of saved [LayoutCoordinates]. + */ + if (coordinates.isAttached) { + val coordinateSnapshot = CoordinateSnapshot( + positionInWindow = coordinates.positionInWindow(), + size = coordinates.size, + boundsInWindow = coordinates.boundsInWindow(), + ) + + val isVisible = calculateVisibility( + coordinates = coordinateSnapshot, + visibilityThresholdPercentage = visibilityThreshold, + ) + + updateVisibilityHelper(itemCode, coordinateSnapshot, isVisible) + } else { + return + } + } + + fun updateExpectedItems(items: List) { + if (this.expectedItems != items) { + // Reset to initial state with new items + this.expectedItems = items + reset() + } + } + + abstract fun reset() + + protected abstract fun updateVisibilityHelper( + itemCode: String, + coordinateSnapshot: CoordinateSnapshot, + isVisible: Boolean, + ) + + private fun calculateVisibility( + coordinates: CoordinateSnapshot, + visibilityThresholdPercentage: Int, + ): Boolean { + val bounds = coordinates.boundsInWindow + + // Check if completely out of bounds (hidden) + @Suppress("ComplexCondition") + if (bounds.left == 0f && bounds.top == 0f && bounds.right == 0f && bounds.bottom == 0f) { + return false + } + + // Calculate visibility percentage + val widthInBounds = bounds.width + val heightInBounds = bounds.height + val totalArea = coordinates.size.height * coordinates.size.width + val areaInBounds = widthInBounds * heightInBounds + + // 100 refers to percentages + val percentVisible = if (totalArea > 0) { + ((areaInBounds / totalArea) * 100).toInt().coerceIn(0, 100) + } else { + 0 + } + + return percentVisible >= visibilityThresholdPercentage + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt index 0207b047526..df44ae876df 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt @@ -1,11 +1,5 @@ package com.stripe.android.paymentsheet.verticalmode -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.positionInWindow -import androidx.compose.ui.unit.IntSize import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -26,36 +20,21 @@ import kotlin.coroutines.CoroutineContext * Once stable, it dispatches a single analytics event. */ internal class PaymentMethodInitialVisibilityTracker( - private var expectedItems: List = emptyList(), - private val renderedLpmCallback: (List, List) -> Unit, + expectedItems: List, dispatcher: CoroutineContext = Dispatchers.Default, + private val renderedLpmCallback: (List, List) -> Unit, +): LayoutCoordinateInitialVisibilityTracker( + expectedItems = expectedItems, + visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD_PERCENT ) { - private data class CoordinateSnapshot( - val positionInWindow: Offset, - val size: IntSize, - val boundsInWindow: Rect - ) private val visibilityMap = mutableMapOf() private val previousCoordinateSnapshots = mutableMapOf() private val coordinateStabilityMap = mutableMapOf() - private var hasDispatched = false private val coroutineScope = CoroutineScope(dispatcher) private var dispatchEventJob: Job? = null - fun updateExpectedItems(items: List) { - if (this.expectedItems != items) { - // Reset to initial state with new items - this.expectedItems = items - reset() - } - } - - fun getHasDispatched(): Boolean { - return this.hasDispatched - } - /** * When this function is called from onGloballyPositioned * it is guaranteed to be called twice on any given stable coordinate @@ -65,30 +44,11 @@ internal class PaymentMethodInitialVisibilityTracker( * when coordinates have moved * and when the composition is finalized and stable. */ - fun updateVisibility(itemCode: String, coordinates: LayoutCoordinates) { - if (itemCode !in expectedItems || expectedItems.isEmpty()) return - if (hasDispatched) return // Only dispatch once per tracker instance - - /** - * Capture only the relevant fields from [LayoutCoordinates] when we know the coordinates are attached. - * Whether previous coordinates is/isn't attached or not is irrelevant, we only need to know - * the previous coordinate's boundsInWindow, positionInWindow, and size. - * - * When implemented, [LayoutCoordinates.isAttached] provides a getter to a var, so we cannot rely on being able - * to call `positionInWindow` and `boundsInWindow` of saved [LayoutCoordinates]. - */ - val coordinateSnapshot: CoordinateSnapshot - if (coordinates.isAttached) { - coordinateSnapshot = CoordinateSnapshot( - positionInWindow = coordinates.positionInWindow(), - size = coordinates.size, - boundsInWindow = coordinates.boundsInWindow(), - ) - } else { - return - } - - val newVisibility = calculateVisibility(coordinateSnapshot) + override fun updateVisibilityHelper( + itemCode: String, + coordinateSnapshot: CoordinateSnapshot, + isVisible: Boolean, + ) { val previousCoordinatesForItem = previousCoordinateSnapshots[itemCode] // Check if coordinates are stable (haven't changed) @@ -102,8 +62,8 @@ internal class PaymentMethodInitialVisibilityTracker( // Update our tracking this.previousCoordinateSnapshots[itemCode] = coordinateSnapshot - val wasVisibilityStable = visibilityMap[itemCode] == newVisibility - visibilityMap[itemCode] = newVisibility + val wasVisibilityStable = visibilityMap[itemCode] == isVisible + visibilityMap[itemCode] = isVisible if (coordinatesAreStable && wasVisibilityStable) { coordinateStabilityMap[itemCode] = true @@ -116,32 +76,6 @@ internal class PaymentMethodInitialVisibilityTracker( checkStabilityAndDispatch() } - @Suppress("MagicNumber") - private fun calculateVisibility(coordinates: CoordinateSnapshot): Boolean { - val bounds = coordinates.boundsInWindow - - // Check if completely out of bounds (hidden) - @Suppress("ComplexCondition") - if (bounds.left == 0f && bounds.top == 0f && bounds.right == 0f && bounds.bottom == 0f) { - return false - } - - // Calculate visibility percentage - val widthInBounds = bounds.width - val heightInBounds = bounds.height - val totalArea = coordinates.size.height * coordinates.size.width - val areaInBounds = widthInBounds * heightInBounds - - // 100 refers to percentages - val percentVisible = if (totalArea > 0) { - ((areaInBounds / totalArea) * 100).toInt().coerceIn(0, 100) - } else { - 0 - } - - return percentVisible >= DEFAULT_VISIBILITY_THRESHOLD_PERCENT - } - private fun checkStability(): Boolean { if (expectedItems.size != coordinateStabilityMap.size || expectedItems.size != visibilityMap.size) { return false @@ -176,7 +110,7 @@ internal class PaymentMethodInitialVisibilityTracker( } } - fun reset() { + override fun reset() { coordinateStabilityMap.clear() previousCoordinateSnapshots.clear() visibilityMap.clear() diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt index 6b9a7775019..702770ef863 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt @@ -544,7 +544,7 @@ internal class DefaultPaymentMethodVerticalLayoutInteractor( ) private fun updatePaymentMethodVisibility(itemCode: String, layoutCoordinates: LayoutCoordinates) { - if (visibilityTracker.getHasDispatched()) { + if (visibilityTracker.hasDispatched) { return } From 78f0ef7f7fc9827990ef39365c8770f042fdaf85 Mon Sep 17 00:00:00 2001 From: tianzhao-stripe Date: Wed, 19 Nov 2025 11:10:50 -0800 Subject: [PATCH 2/5] make shampoo rule more reliable --- .../com/stripe/android/paymentelement/EmbeddedContentPage.kt | 1 + .../paymentelement/EmbeddedPaymentElementAnalyticsTest.kt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedContentPage.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedContentPage.kt index fc298156e82..35486e54b4d 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedContentPage.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedContentPage.kt @@ -29,6 +29,7 @@ internal class EmbeddedContentPage( } fun clickOnLpm(code: String) { + composeTestRule.waitForIdle() waitUntilVisible() composeTestRule.onNode(hasTestTag("${TEST_TAG_NEW_PAYMENT_METHOD_ROW_BUTTON}_$code")) diff --git a/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedPaymentElementAnalyticsTest.kt b/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedPaymentElementAnalyticsTest.kt index 31535d4ba18..10288e64867 100644 --- a/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedPaymentElementAnalyticsTest.kt +++ b/paymentsheet/src/androidTest/java/com/stripe/android/paymentelement/EmbeddedPaymentElementAnalyticsTest.kt @@ -461,6 +461,9 @@ internal class EmbeddedPaymentElementAnalyticsTest { managePage.waitUntilGone(card1.id) managePage.clickDone() + validateAnalyticsRequest("mc_dismiss") + Espresso.pressBack() + testContext.markTestSucceeded() } From 182a66e221cee56c3b6f4900fce6195f9306f1e7 Mon Sep 17 00:00:00 2001 From: tianzhao-stripe Date: Wed, 19 Nov 2025 11:13:07 -0800 Subject: [PATCH 3/5] PaymentMethodLayoutInitialVisibilityTracker for embedded --- .../embedded/content/EmbeddedContentHelper.kt | 1 + .../PaymentMethodEmbeddedLayoutUI.kt | 21 +-- ...entMethodLayoutInitialVisibilityTracker.kt | 37 ++++++ .../PaymentMethodVerticalLayoutInteractor.kt | 31 +++-- ...ymentMethodVerticalLayoutInteractorTest.kt | 83 +++++++++++- .../FakeLayoutCoordinatesFixtures.kt | 13 ++ ...ethodLayoutInitialVisibilityTrackerTest.kt | 125 ++++++++++++++++++ 7 files changed, 284 insertions(+), 27 deletions(-) create mode 100644 paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedContentHelper.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedContentHelper.kt index a24b7ce87bd..2d189d1f1db 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedContentHelper.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/content/EmbeddedContentHelper.kt @@ -265,6 +265,7 @@ internal class DefaultEmbeddedContentHelper @Inject constructor( }, invokeRowSelectionCallback = ::invokeRowSelectionCallback, displaysMandatesInFormScreen = isImmediateAction && embeddedViewDisplaysMandateText, + shouldTrackIndividualPaymentMethods = false, onInitiallyDisplayedPaymentMethodVisibilitySnapshot = { visiblePaymentMethods, hiddenPaymentMethods -> eventReporter.onInitiallyDisplayedPaymentMethodVisibilitySnapshot( visiblePaymentMethods = visiblePaymentMethods, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodEmbeddedLayoutUI.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodEmbeddedLayoutUI.kt index 00561b9ba63..b919c23ea91 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodEmbeddedLayoutUI.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodEmbeddedLayoutUI.kt @@ -34,6 +34,7 @@ import com.stripe.android.paymentsheet.DisplayableSavedPaymentMethod import com.stripe.android.paymentsheet.PaymentSheet.Appearance.Embedded import com.stripe.android.paymentsheet.PaymentSheet.Appearance.Embedded.RowStyle import com.stripe.android.paymentsheet.R +import com.stripe.android.paymentsheet.verticalmode.PaymentMethodLayoutInitialVisibilityTracker.Companion.EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME import com.stripe.android.ui.core.elements.Mandate import com.stripe.android.uicore.image.StripeImageLoader import com.stripe.android.uicore.strings.resolve @@ -137,7 +138,12 @@ internal fun PaymentMethodEmbeddedLayoutUI( // Cancel tracking and any pending dispatches, when the paymentMethods used change DisposableEffect(paymentMethodCodes) { onDispose { cancelPaymentMethodVisibilityTracking.invoke() } } - Column(modifier = modifier, verticalArrangement = arrangement) { + Column( + modifier = modifier.onGloballyPositioned { + updatePaymentMethodVisibility(EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, it) + }, + verticalArrangement = arrangement + ) { if (appearance.style.topSeparatorEnabled()) OptionalEmbeddedDivider(appearance.style) EmbeddedSavedPaymentMethodRowButton( @@ -149,7 +155,6 @@ internal fun PaymentMethodEmbeddedLayoutUI( onViewMorePaymentMethods = onViewMorePaymentMethods, onManageOneSavedPaymentMethod = onManageOneSavedPaymentMethod, onSelectSavedPaymentMethod = onSelectSavedPaymentMethod, - updatePaymentMethodVisibility = updatePaymentMethodVisibility, appearance = appearance ) @@ -159,7 +164,6 @@ internal fun PaymentMethodEmbeddedLayoutUI( isEnabled = isEnabled, imageLoader = imageLoader, appearance = appearance, - updatePaymentMethodVisibility = updatePaymentMethodVisibility ) if (appearance.style.bottomSeparatorEnabled()) OptionalEmbeddedDivider(appearance.style) @@ -262,7 +266,6 @@ internal fun EmbeddedSavedPaymentMethodRowButton( onViewMorePaymentMethods: () -> Unit, onManageOneSavedPaymentMethod: (DisplayableSavedPaymentMethod) -> Unit, onSelectSavedPaymentMethod: (DisplayableSavedPaymentMethod) -> Unit, - updatePaymentMethodVisibility: (String, LayoutCoordinates) -> Unit = { _, _ -> }, appearance: Embedded, ) { if (displayedSavedPaymentMethod != null) { @@ -278,9 +281,6 @@ internal fun EmbeddedSavedPaymentMethodRowButton( onManageOneSavedPaymentMethod = { onManageOneSavedPaymentMethod(displayedSavedPaymentMethod) }, ) }, - modifier = Modifier.onGloballyPositioned { coordinates -> - updatePaymentMethodVisibility("saved", coordinates) - }, onClick = { onSelectSavedPaymentMethod(displayedSavedPaymentMethod) }, appearance = appearance ) @@ -296,7 +296,6 @@ internal fun EmbeddedNewPaymentMethodRowButtonsLayoutUi( isEnabled: Boolean, imageLoader: StripeImageLoader, appearance: Embedded, - updatePaymentMethodVisibility: (String, LayoutCoordinates) -> Unit = { _, _ -> }, ) { val selectedIndex = remember(selection, paymentMethods) { if (selection is PaymentMethodVerticalLayoutInteractor.Selection.New) { @@ -327,9 +326,6 @@ internal fun EmbeddedNewPaymentMethodRowButtonsLayoutUi( ) } }, - modifier = Modifier.onGloballyPositioned { coordinates -> - updatePaymentMethodVisibility(item.code, coordinates) - }, ) } else { NewPaymentMethodRowButton( @@ -338,9 +334,6 @@ internal fun EmbeddedNewPaymentMethodRowButtonsLayoutUi( displayablePaymentMethod = item, imageLoader = imageLoader, appearance = appearance, - modifier = Modifier.onGloballyPositioned { coordinates -> - updatePaymentMethodVisibility(item.code, coordinates) - }, ) } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt new file mode 100644 index 00000000000..adc56e00767 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt @@ -0,0 +1,37 @@ +package com.stripe.android.paymentsheet.verticalmode + +internal class PaymentMethodLayoutInitialVisibilityTracker( + private val callback: () -> Unit, +): LayoutCoordinateInitialVisibilityTracker( + expectedItems = listOf(EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME), + visibilityThreshold = VISIBILITY_THRESHOLD_PERCENT_FOR_EMBEDDED_LAYOUT_TRACKING +) { + + override fun updateVisibilityHelper( + itemCode: String, + coordinateSnapshot: CoordinateSnapshot, + isVisible: Boolean, + ) { + if(isVisible) { + hasDispatched = true + callback() + } + } + + override fun reset() { + hasDispatched = false + } + + companion object { + const val EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME = "embedded_payment_method_layout" + + /** + * Arbitrary small non zero value to ensure that users see some of + * embedded payment method layout before dispatching event. + * + * It is assumed that users will scroll and see the entire + * embedded payment method layout before completing their purchase + */ + const val VISIBILITY_THRESHOLD_PERCENT_FOR_EMBEDDED_LAYOUT_TRACKING = 25 + } +} diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt index 702770ef863..7f722f3017f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodVerticalLayoutInteractor.kt @@ -109,6 +109,7 @@ internal class DefaultPaymentMethodVerticalLayoutInteractor( private val shouldUpdateVerticalModeSelection: (String?) -> Boolean, private val invokeRowSelectionCallback: (() -> Unit)? = null, private val displaysMandatesInFormScreen: Boolean, + private val shouldTrackIndividualPaymentMethods: Boolean, private val onInitiallyDisplayedPaymentMethodVisibilitySnapshot: (List, List) -> Unit, dispatcher: CoroutineContext = Dispatchers.Default, mainDispatcher: CoroutineContext = Dispatchers.Main.immediate, @@ -179,6 +180,7 @@ internal class DefaultPaymentMethodVerticalLayoutInteractor( !requiresFormScreen }, displaysMandatesInFormScreen = false, + shouldTrackIndividualPaymentMethods = true, onInitiallyDisplayedPaymentMethodVisibilitySnapshot = { visiblePaymentMethods, hiddenPaymentMethods -> viewModel.eventReporter.onInitiallyDisplayedPaymentMethodVisibilitySnapshot( visiblePaymentMethods = visiblePaymentMethods, @@ -535,20 +537,33 @@ internal class DefaultPaymentMethodVerticalLayoutInteractor( } } - private val visibilityTracker = PaymentMethodInitialVisibilityTracker( - expectedItems = expectedItemsForVisibilityTracking.value, - renderedLpmCallback = { visiblePaymentMethods, hiddenPaymentMethods -> - onInitiallyDisplayedPaymentMethodVisibilitySnapshot(visiblePaymentMethods, hiddenPaymentMethods) - }, - dispatcher = dispatcher - ) + private val visibilityTracker = if (shouldTrackIndividualPaymentMethods) { + PaymentMethodInitialVisibilityTracker( + expectedItems = expectedItemsForVisibilityTracking.value, + renderedLpmCallback = { visiblePaymentMethods, hiddenPaymentMethods -> + onInitiallyDisplayedPaymentMethodVisibilitySnapshot(visiblePaymentMethods, hiddenPaymentMethods) + }, + dispatcher = dispatcher + ) + } else { + PaymentMethodLayoutInitialVisibilityTracker( + callback = { + onInitiallyDisplayedPaymentMethodVisibilitySnapshot( + expectedItemsForVisibilityTracking.value, + emptyList() + ) + } + ) + } private fun updatePaymentMethodVisibility(itemCode: String, layoutCoordinates: LayoutCoordinates) { if (visibilityTracker.hasDispatched) { return } - visibilityTracker.updateExpectedItems(expectedItemsForVisibilityTracking.value) + if (shouldTrackIndividualPaymentMethods) { + visibilityTracker.updateExpectedItems(expectedItemsForVisibilityTracking.value) + } visibilityTracker.updateVisibility(itemCode, layoutCoordinates) } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/DefaultPaymentMethodVerticalLayoutInteractorTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/DefaultPaymentMethodVerticalLayoutInteractorTest.kt index ee301fd6594..db26a771a43 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/DefaultPaymentMethodVerticalLayoutInteractorTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/DefaultPaymentMethodVerticalLayoutInteractorTest.kt @@ -27,6 +27,7 @@ import com.stripe.android.paymentsheet.model.PaymentMethodIncentive import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.LinkState import com.stripe.android.paymentsheet.state.WalletsState +import com.stripe.android.paymentsheet.verticalmode.PaymentMethodLayoutInitialVisibilityTracker.Companion.EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME import com.stripe.android.paymentsheet.verticalmode.PaymentMethodVerticalLayoutInteractor.ViewAction import com.stripe.android.testing.PaymentMethodFactory import com.stripe.android.ui.core.cbc.CardBrandChoiceEligibility @@ -1468,13 +1469,14 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { } @Test - fun visibilityTracker_doesNotEmitEventOnInit() = runScenario( + fun paymentMethodVisibilityTracker_doesNotEmitEventOnInit() = runScenario( initialPaymentMethods = PaymentMethodFixtures.createCards(1), paymentMethodMetadata = PaymentMethodMetadataFactory.create( stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD.copy( paymentMethodTypes = listOf("card", "cashapp") ) ), + shouldTrackIndividualPaymentMethods = true, ) { val fakeLayoutCoordinates = FakeLayoutCoordinatesFixtures.FULLY_VISIBLE_COORDINATES @@ -1486,12 +1488,13 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { } @Test - fun visibilityTracker_emitsCorrectlyNoSaved() = runScenario( + fun paymentMethodVisibilityTracker_emitsCorrectlyNoSaved() = runScenario( paymentMethodMetadata = PaymentMethodMetadataFactory.create( stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD.copy( paymentMethodTypes = listOf("card") ) ), + shouldTrackIndividualPaymentMethods = true, ) { val fakeLayoutCoordinates = FakeLayoutCoordinatesFixtures.FULLY_VISIBLE_COORDINATES @@ -1511,7 +1514,7 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { } @Test - fun visibilityTracker_emitsCorrectlyWithSaved() { + fun paymentMethodVisibilityTracker_emitsCorrectlyWithSaved() { runScenario( initialPaymentMethods = PaymentMethodFixtures.createCards(1), paymentMethodMetadata = PaymentMethodMetadataFactory.create( @@ -1519,6 +1522,7 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { paymentMethodTypes = listOf("card", "cashapp") ) ), + shouldTrackIndividualPaymentMethods = true, ) { val visibleCoordinates = FakeLayoutCoordinatesFixtures.FULLY_VISIBLE_COORDINATES @@ -1557,8 +1561,9 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { } @Test - fun visibilityTracker_doesNotEmitWhenCancellingTracking() = runScenario( - initialPaymentMethods = PaymentMethodFixtures.createCards(1) + fun paymentMethodVisibilityTracker_doesNotEmitWhenCancellingTracking() = runScenario( + initialPaymentMethods = PaymentMethodFixtures.createCards(1), + shouldTrackIndividualPaymentMethods = true, ) { val fakeLayoutCoordinates = FakeLayoutCoordinatesFixtures.FULLY_HIDDEN_COORDINATES @@ -1580,6 +1585,72 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { visibilitySnapshotTurbine.expectNoEvents() } + @Test + fun paymentMethodLayoutVisibilityTracker_doesNotEmitEventOnInit() = runScenario( + initialPaymentMethods = PaymentMethodFixtures.createCards(1), + paymentMethodMetadata = PaymentMethodMetadataFactory.create( + stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD.copy( + paymentMethodTypes = listOf("card", "cashapp") + ) + ), + shouldTrackIndividualPaymentMethods = false, + ) { + visibilitySnapshotTurbine.expectNoEvents() + } + + @Test + fun paymentMethodLayoutVisibilityTracker_emitsCorrectlyNoSaved() = runScenario( + paymentMethodMetadata = PaymentMethodMetadataFactory.create( + stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD.copy( + paymentMethodTypes = listOf("card") + ) + ), + shouldTrackIndividualPaymentMethods = false, + ) { + val fakeLayoutCoordinates = FakeLayoutCoordinatesFixtures.FULLY_VISIBLE_COORDINATES + + interactor.handleViewAction( + ViewAction.UpdatePaymentMethodVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + fakeLayoutCoordinates + ) + ) + + testScope.testScheduler.advanceUntilIdle() + + assertThat(visibilitySnapshotTurbine.awaitItem()).isEqualTo( + Pair(listOf("card"), emptyList()) + ) + } + + @Test + fun paymentMethodLayoutVisibilityTracker_emitsCorrectlyWithSaved() { + runScenario( + initialPaymentMethods = PaymentMethodFixtures.createCards(1), + paymentMethodMetadata = PaymentMethodMetadataFactory.create( + stripeIntent = PaymentIntentFixtures.PI_REQUIRES_PAYMENT_METHOD.copy( + paymentMethodTypes = listOf("card", "cashapp") + ) + ), + shouldTrackIndividualPaymentMethods = false, + ) { + val visibleCoordinates = FakeLayoutCoordinatesFixtures.FULLY_VISIBLE_COORDINATES + + interactor.handleViewAction( + ViewAction.UpdatePaymentMethodVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + visibleCoordinates + ) + ) + + testScope.testScheduler.advanceUntilIdle() + + assertThat(visibilitySnapshotTurbine.awaitItem()).isEqualTo( + Pair(listOf("saved", "card", "cashapp"), emptyList()) + ) + } + } + private val notImplemented: () -> Nothing = { throw AssertionError("Not implemented") } private val linkAndGooglePayWalletState = WalletsState( @@ -1631,6 +1702,7 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { invokeRowSelectionCallback: (() -> Unit)? = null, initialWalletsState: WalletsState? = null, displaysMandatesInFormScreen: Boolean = false, + shouldTrackIndividualPaymentMethods: Boolean = true, testBlock: suspend TestParams.() -> Unit ) { val processing: MutableStateFlow = MutableStateFlow(initialProcessing) @@ -1694,6 +1766,7 @@ class DefaultPaymentMethodVerticalLayoutInteractorTest { mainDispatcher = testDispatcher, invokeRowSelectionCallback = invokeRowSelectionCallback, displaysMandatesInFormScreen = displaysMandatesInFormScreen, + shouldTrackIndividualPaymentMethods = shouldTrackIndividualPaymentMethods, onInitiallyDisplayedPaymentMethodVisibilitySnapshot = { visibleItems, hiddenItems -> visibilitySnapshotTurbine.add( Pair(visibleItems, hiddenItems) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt index ed4d4736443..daf58a6ba6b 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt @@ -2,6 +2,7 @@ package com.stripe.android.paymentsheet.verticalmode import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.unit.IntSize internal object FakeLayoutCoordinatesFixtures { @@ -21,4 +22,16 @@ internal object FakeLayoutCoordinatesFixtures { position = Offset(0f, 75f), bounds = Rect(0f, 75f, 100f, 100f), ) + + fun getCoordinatesBasedOnPercentVisible(percentVisible: Float): LayoutCoordinates { + return FakeLayoutCoordinates.create( + size = IntSize(100, 50), + bounds = getBoundsBasedOnPercentVisible(percentVisible) + ) + } + + fun getBoundsBasedOnPercentVisible(percentVisible: Float): Rect { + return Rect(0f, 0f, 100f, percentVisible * 50f) + } + } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt new file mode 100644 index 00000000000..569f32ac70a --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt @@ -0,0 +1,125 @@ +package com.stripe.android.paymentsheet.verticalmode + +import com.stripe.android.paymentsheet.verticalmode.PaymentMethodLayoutInitialVisibilityTracker.Companion.EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.robolectric.RobolectricTestRunner + +@ExperimentalCoroutinesApi +@RunWith(RobolectricTestRunner::class) +class PaymentMethodLayoutInitialVisibilityTrackerTest { + + private val callback: () -> Unit = mock() + + @Test + fun `updateVisibility - ignores items not in expected list`() = runTest { + val tracker = getTracker() + + val coordinates = FakeLayoutCoordinatesFixtures.FULLY_VISIBLE_COORDINATES + + tracker.updateVisibility("unknown_method", coordinates) + verifyNoCallback(callback) + } + + @Test + fun `visible item invokes callback`() = runTest { + val tracker = getTracker() + + val coordinates = FakeLayoutCoordinatesFixtures.FULLY_VISIBLE_COORDINATES + + tracker.updateVisibility(EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, coordinates) + verify(callback).invoke() + } + + + @Test + fun `hidden item does not invoke callback`() = runTest { + val tracker = getTracker() + + val coordinates = FakeLayoutCoordinatesFixtures.FULLY_HIDDEN_COORDINATES + + tracker.updateVisibility(EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, coordinates) + verifyNoCallback(callback) + } + + @Test + fun `visibility calculation - partially visible above threshold invokes callback`() = runTest { + val tracker = getTracker() + + tracker.updateVisibility( + itemCode = EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + coordinates = FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.5F) + ) + + verify(callback).invoke() + } + + @Test + fun `visibility calculation - partially visible below threshold does not invoke callback`() = runTest { + val tracker = getTracker() + + tracker.updateVisibility( + itemCode = EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + coordinates = FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.1F) + ) + + // Should not dispatch because item doesn't meet visibility threshold + verifyNoCallback(callback) + } + + @Test + fun `visibility calculation - simulate scrolling invokes callback`() = runTest { + val tracker = getTracker() + + tracker.updateVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0F) + ) + + tracker.updateVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.06F) + ) + + tracker.updateVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.12F) + ) + + + tracker.updateVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.18F) + ) + + tracker.updateVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.24F) + ) + + // Should not dispatch because item doesn't meet visibility threshold + verifyNoCallback(callback) + + tracker.updateVisibility( + EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, + FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.3F) + ) + + verify(callback).invoke() + } + + private fun getTracker(): PaymentMethodLayoutInitialVisibilityTracker { + return PaymentMethodLayoutInitialVisibilityTracker( + callback = callback + ) + } + + private fun verifyNoCallback(callback: () -> Unit) { + verify(callback, never()).invoke() + } +} From e6533caaa8390366933370d11f4fadc9d2e00106 Mon Sep 17 00:00:00 2001 From: tianzhao-stripe Date: Wed, 19 Nov 2025 11:23:34 -0800 Subject: [PATCH 4/5] lint --- .../verticalmode/LayoutCoordinateInitialVisibilityTracker.kt | 3 ++- .../verticalmode/PaymentMethodInitialVisibilityTracker.kt | 2 +- .../PaymentMethodLayoutInitialVisibilityTracker.kt | 4 ++-- .../verticalmode/FakeLayoutCoordinatesFixtures.kt | 3 +-- .../PaymentMethodLayoutInitialVisibilityTrackerTest.kt | 2 -- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt index 6ceab84fa09..07a987dc51a 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/LayoutCoordinateInitialVisibilityTracker.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.unit.IntSize internal abstract class LayoutCoordinateInitialVisibilityTracker( var expectedItems: List, val visibilityThreshold: Int, -){ +) { protected data class CoordinateSnapshot( val positionInWindow: Offset, @@ -66,6 +66,7 @@ internal abstract class LayoutCoordinateInitialVisibilityTracker( isVisible: Boolean, ) + @Suppress("MagicNumber") private fun calculateVisibility( coordinates: CoordinateSnapshot, visibilityThresholdPercentage: Int, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt index df44ae876df..07fa7d8aa92 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodInitialVisibilityTracker.kt @@ -23,7 +23,7 @@ internal class PaymentMethodInitialVisibilityTracker( expectedItems: List, dispatcher: CoroutineContext = Dispatchers.Default, private val renderedLpmCallback: (List, List) -> Unit, -): LayoutCoordinateInitialVisibilityTracker( +) : LayoutCoordinateInitialVisibilityTracker( expectedItems = expectedItems, visibilityThreshold = DEFAULT_VISIBILITY_THRESHOLD_PERCENT ) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt index adc56e00767..c5678d23306 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTracker.kt @@ -2,7 +2,7 @@ package com.stripe.android.paymentsheet.verticalmode internal class PaymentMethodLayoutInitialVisibilityTracker( private val callback: () -> Unit, -): LayoutCoordinateInitialVisibilityTracker( +) : LayoutCoordinateInitialVisibilityTracker( expectedItems = listOf(EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME), visibilityThreshold = VISIBILITY_THRESHOLD_PERCENT_FOR_EMBEDDED_LAYOUT_TRACKING ) { @@ -12,7 +12,7 @@ internal class PaymentMethodLayoutInitialVisibilityTracker( coordinateSnapshot: CoordinateSnapshot, isVisible: Boolean, ) { - if(isVisible) { + if (isVisible) { hasDispatched = true callback() } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt index daf58a6ba6b..ba0124d1706 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/FakeLayoutCoordinatesFixtures.kt @@ -31,7 +31,6 @@ internal object FakeLayoutCoordinatesFixtures { } fun getBoundsBasedOnPercentVisible(percentVisible: Float): Rect { - return Rect(0f, 0f, 100f, percentVisible * 50f) + return Rect(0f, 0f, 100f, percentVisible * 50f) } - } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt index 569f32ac70a..4c900f0ef46 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutInitialVisibilityTrackerTest.kt @@ -36,7 +36,6 @@ class PaymentMethodLayoutInitialVisibilityTrackerTest { verify(callback).invoke() } - @Test fun `hidden item does not invoke callback`() = runTest { val tracker = getTracker() @@ -91,7 +90,6 @@ class PaymentMethodLayoutInitialVisibilityTrackerTest { FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.12F) ) - tracker.updateVisibility( EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME, FakeLayoutCoordinatesFixtures.getCoordinatesBasedOnPercentVisible(0.18F) From c20a7e1e839792c1c924b7340383294297bdf4be Mon Sep 17 00:00:00 2001 From: tianzhao-stripe Date: Wed, 19 Nov 2025 13:46:20 -0800 Subject: [PATCH 5/5] fix PaymentMethodLayoutUiTest --- .../verticalmode/PaymentMethodLayoutUITest.kt | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutUITest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutUITest.kt index 6184db323c2..8973b47e7fc 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutUITest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/PaymentMethodLayoutUITest.kt @@ -23,6 +23,7 @@ import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.paymentsheet.DisplayableSavedPaymentMethod import com.stripe.android.paymentsheet.PaymentSheet.Appearance.Embedded import com.stripe.android.paymentsheet.ViewActionRecorder +import com.stripe.android.paymentsheet.verticalmode.PaymentMethodLayoutInitialVisibilityTracker.Companion.EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME import com.stripe.android.paymentsheet.verticalmode.PaymentMethodVerticalLayoutInteractor.SavedPaymentMethodAction import com.stripe.android.paymentsheet.verticalmode.PaymentMethodVerticalLayoutInteractor.Selection import com.stripe.android.testing.createComposeCleanupRule @@ -38,7 +39,8 @@ internal class PaymentMethodLayoutUITest( private val paymentMethodsTag: String, private val allPaymentMethodsChildCount: Int, private val layoutUI: - @Composable ColumnScope.(interactor: FakePaymentMethodVerticalLayoutInteractor, modifier: Modifier) -> Unit + @Composable ColumnScope.(interactor: FakePaymentMethodVerticalLayoutInteractor, modifier: Modifier) -> Unit, + private val shouldTrackIndividualPaymentMethod: Boolean, ) { @get:Rule val composeRule = createComposeRule() @@ -251,16 +253,24 @@ internal class PaymentMethodLayoutUITest( } } - initialState.displayedSavedPaymentMethod?.let { - viewActionRecorder.consume { - it is PaymentMethodVerticalLayoutInteractor.ViewAction.UpdatePaymentMethodVisibility && - it.itemCode == "saved" + if (shouldTrackIndividualPaymentMethod) { + initialState.displayedSavedPaymentMethod?.let { + viewActionRecorder.consume { + it is PaymentMethodVerticalLayoutInteractor.ViewAction.UpdatePaymentMethodVisibility && + it.itemCode == "saved" + } } - } - initialState.displayablePaymentMethods.forEach { paymentMethod -> + + initialState.displayablePaymentMethods.forEach { paymentMethod -> + viewActionRecorder.consume { + it is PaymentMethodVerticalLayoutInteractor.ViewAction.UpdatePaymentMethodVisibility && + it.itemCode == paymentMethod.code + } + } + } else { viewActionRecorder.consume { it is PaymentMethodVerticalLayoutInteractor.ViewAction.UpdatePaymentMethodVisibility && - it.itemCode == paymentMethod.code + it.itemCode == EMBEDDED_PAYMENT_METHOD_LAYOUT_NAME } } @@ -280,7 +290,8 @@ internal class PaymentMethodLayoutUITest( allPaymentMethodsChildCount = 3, layoutUI = { interactor, modifier -> PaymentMethodVerticalLayoutUI(interactor, modifier) - } + }, + shouldTrackIndividualPaymentMethod = true, ), parameters( paymentMethodsTag = TEST_TAG_PAYMENT_METHOD_EMBEDDED_LAYOUT, @@ -292,7 +303,8 @@ internal class PaymentMethodLayoutUITest( modifier = modifier, appearance = Embedded(Embedded.RowStyle.FloatingButton.default), ) - } + }, + shouldTrackIndividualPaymentMethod = false, ) ) @@ -302,8 +314,9 @@ internal class PaymentMethodLayoutUITest( layoutUI: @Composable ColumnScope.( interactor: FakePaymentMethodVerticalLayoutInteractor, modifier: Modifier - ) -> Unit - ) = arrayOf(paymentMethodsTag, allPaymentMethodsChildCount, layoutUI) + ) -> Unit, + shouldTrackIndividualPaymentMethod: Boolean, + ) = arrayOf(paymentMethodsTag, allPaymentMethodsChildCount, layoutUI, shouldTrackIndividualPaymentMethod) private fun createState( displayablePaymentMethods: List = emptyList(),