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 1d7202b4da2..2f864a7c45a 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 @@ -199,9 +199,7 @@ internal class DefaultEmbeddedContentHelper @Inject constructor( uiContext = uiContext, customerRepository = customerRepository, selection = selectionHolder.selection, - clearSelection = { - setSelection(null) - }, + setSelection = ::setSelection, customerStateHolder = customerStateHolder, prePaymentMethodRemoveActions = {}, postPaymentMethodRemoveActions = {}, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/manage/ManageSavedPaymentMethodMutatorFactory.kt b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/manage/ManageSavedPaymentMethodMutatorFactory.kt index 63b3437a01d..e11dd43e349 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/manage/ManageSavedPaymentMethodMutatorFactory.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentelement/embedded/manage/ManageSavedPaymentMethodMutatorFactory.kt @@ -41,9 +41,7 @@ internal class ManageSavedPaymentMethodMutatorFactory @Inject constructor( uiContext = uiContext, customerRepository = customerRepository, selection = selectionHolder.selection, - clearSelection = { - selectionHolder.set(null) - }, + setSelection = selectionHolder::set, customerStateHolder = customerStateHolder, prePaymentMethodRemoveActions = { if (customerStateHolder.paymentMethods.value.size > 1) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/CustomerStateHolder.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/CustomerStateHolder.kt index d96f7c16cf9..2a0dff9a3a2 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/CustomerStateHolder.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/CustomerStateHolder.kt @@ -54,7 +54,6 @@ internal class CustomerStateHolder( val newCustomer = customer.value?.copy(defaultPaymentMethodId = paymentMethod?.id) savedStateHandle[SAVED_CUSTOMER] = newCustomer - updateMostRecentlySelectedSavedPaymentMethod(paymentMethod = paymentMethod) } fun updateMostRecentlySelectedSavedPaymentMethod(paymentMethod: PaymentMethod?) { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt index 1f862368fd9..aefcffce477 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt @@ -38,7 +38,7 @@ internal class SavedPaymentMethodMutator( private val uiContext: CoroutineContext, private val customerRepository: CustomerRepository, private val selection: StateFlow, - private val clearSelection: () -> Unit, + private val setSelection: (PaymentSelection?) -> Unit, private val customerStateHolder: CustomerStateHolder, // Actions that should be taken after removing a payment method has succeeded but before we've fully updated our // state to reflect that. For example, in our manage payment method screen, we want to navigate back to the @@ -158,7 +158,7 @@ internal class SavedPaymentMethodMutator( if (didRemoveSelectedItem) { // Remove the current selection. The new selection will be set when we're computing // the next PaymentOptionsState. - clearSelection() + setSelection(null) } return customerRepository.detachPaymentMethod( @@ -184,7 +184,7 @@ internal class SavedPaymentMethodMutator( ) if ((selection.value as? PaymentSelection.Saved)?.paymentMethod?.id == paymentMethodId) { - clearSelection() + setSelection(null) } withContext(uiContext) { @@ -222,6 +222,7 @@ internal class SavedPaymentMethodMutator( paymentMethodId = paymentMethod.id, ).onSuccess { customerStateHolder.setDefaultPaymentMethod(paymentMethod = paymentMethod) + setSelection(PaymentSelection.Saved(paymentMethod = paymentMethod)) }.map {} } @@ -398,8 +399,8 @@ internal class SavedPaymentMethodMutator( uiContext = Dispatchers.Main, customerRepository = viewModel.customerRepository, selection = viewModel.selection, + setSelection = viewModel::updateSelection, customerStateHolder = viewModel.customerStateHolder, - clearSelection = { viewModel.updateSelection(null) }, prePaymentMethodRemoveActions = { navigateBackOnPaymentMethodRemoved(viewModel) }, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt index 3ab133d5d77..8cc1c1ac26e 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt @@ -9,15 +9,19 @@ import androidx.compose.ui.test.hasAnyAncestor import androidx.compose.ui.test.hasAnyDescendant import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.isEnabled import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.performClick import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso import com.stripe.android.model.CardBrand import com.stripe.android.model.PaymentMethod +import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.networktesting.NetworkRule import com.stripe.android.networktesting.RequestMatchers +import com.stripe.android.networktesting.testBodyFromFile import com.stripe.android.paymentsheet.state.PaymentElementLoader import com.stripe.android.paymentsheet.ui.PAYMENT_SHEET_EDIT_BUTTON_TEST_TAG import com.stripe.android.paymentsheet.ui.SAVED_PAYMENT_OPTION_TAB_LAYOUT_TEST_TAG @@ -43,6 +47,10 @@ internal class CustomerSessionPaymentSheetActivityTest { private val composeTestRule = createAndroidComposeRule() private val networkRule = NetworkRule() + private val verticalModePage = VerticalModePage(composeTestRule) + private val managePage = ManagePage(composeTestRule) + private val editPage = EditPage(composeTestRule) + @get:Rule val ruleChain: RuleChain = RuleChain .outerRule(composeTestRule) @@ -292,11 +300,60 @@ internal class CustomerSessionPaymentSheetActivityTest { composeTestRule.onUpdateScreenRemoveButton().assertDoesNotExist() } + @Test + fun `Setting default card selects that card in vertical mode`() { + val cards = PaymentMethodFixtures.createCards(2) + runTest( + cards = cards, + setAsDefaultFeatureEnabled = true, + paymentMethodLayout = PaymentSheet.PaymentMethodLayout.Vertical, + ) { + val originallySelectedPaymentMethodId = cards[0].id!! + val newDefaultPaymentMethodId = cards[1].id!! + verticalModePage.assertHasSavedPaymentMethods() + verticalModePage.assertHasSelectedSavedPaymentMethod(originallySelectedPaymentMethodId) + verticalModePage.assertPrimaryButton(isEnabled()) + + verticalModePage.clickViewMore() + managePage.waitUntilVisible() + verticalModePage.assertHasSelectedSavedPaymentMethod(originallySelectedPaymentMethodId) + managePage.clickEdit() + managePage.clickEdit(newDefaultPaymentMethodId) + setDefaultPaymentMethod() + + managePage.waitUntilVisible() + managePage.clickDone() + verticalModePage.assertHasSelectedSavedPaymentMethod(newDefaultPaymentMethodId) + Espresso.pressBack() + + verticalModePage.waitUntilVisible() + verticalModePage.assertHasSelectedSavedPaymentMethod(newDefaultPaymentMethodId) + verticalModePage.assertPrimaryButton(isEnabled()) + } + } + + private fun setDefaultPaymentMethod() { + editPage.waitUntilVisible() + editPage.clickSetAsDefaultCheckbox() + + networkRule.enqueue( + RequestMatchers.host("api.stripe.com"), + RequestMatchers.method("POST"), + RequestMatchers.path("/v1/elements/customers/cus_12345/set_default_payment_method"), + ) { response -> + response.testBodyFromFile("elements-sessions-customers-set-default-pm.json") + } + + editPage.update() + } + private fun runTest( cards: List, - isPaymentMethodRemoveEnabled: Boolean, - canRemoveLastPaymentMethodConfig: Boolean, - canRemoveLastPaymentMethodServer: Boolean, + isPaymentMethodRemoveEnabled: Boolean = true, + canRemoveLastPaymentMethodConfig: Boolean = true, + canRemoveLastPaymentMethodServer: Boolean = true, + setAsDefaultFeatureEnabled: Boolean = false, + paymentMethodLayout: PaymentSheet.PaymentMethodLayout = PaymentSheet.PaymentMethodLayout.Horizontal, test: (PaymentSheetActivity) -> Unit, ) { networkRule.enqueue( @@ -309,6 +366,7 @@ internal class CustomerSessionPaymentSheetActivityTest { cards = cards, isPaymentMethodRemoveEnabled = isPaymentMethodRemoveEnabled, canRemoveLastPaymentMethod = canRemoveLastPaymentMethodServer, + setAsDefaultFeatureEnabled = setAsDefaultFeatureEnabled, ) ) } @@ -330,18 +388,20 @@ internal class CustomerSessionPaymentSheetActivityTest { ), allowsRemovalOfLastSavedPaymentMethod = canRemoveLastPaymentMethodConfig, preferredNetworks = listOf(CardBrand.CartesBancaires, CardBrand.Visa), - paymentMethodLayout = PaymentSheet.PaymentMethodLayout.Horizontal, + paymentMethodLayout = paymentMethodLayout, ), statusBarColor = PaymentSheetFixtures.STATUS_BAR_COLOR, ) ) ).use { scenario -> scenario.onActivity { activity -> - composeTestRule.waitUntil(timeoutMillis = 2_000) { - composeTestRule - .onAllNodes(hasTestTag(SAVED_PAYMENT_OPTION_TAB_LAYOUT_TEST_TAG)) - .fetchSemanticsNodes() - .isNotEmpty() + if (paymentMethodLayout == PaymentSheet.PaymentMethodLayout.Horizontal) { + composeTestRule.waitUntil(timeoutMillis = 2_000) { + composeTestRule + .onAllNodes(hasTestTag(SAVED_PAYMENT_OPTION_TAB_LAYOUT_TEST_TAG)) + .fetchSemanticsNodes() + .isNotEmpty() + } } test(activity) @@ -390,6 +450,7 @@ internal class CustomerSessionPaymentSheetActivityTest { cards: List, isPaymentMethodRemoveEnabled: Boolean, canRemoveLastPaymentMethod: Boolean, + setAsDefaultFeatureEnabled: Boolean, ): String { val cardsArray = JSONArray() @@ -399,17 +460,9 @@ internal class CustomerSessionPaymentSheetActivityTest { val cardsStringified = cardsArray.toString(2) - val isPaymentMethodRemoveStringified = if (isPaymentMethodRemoveEnabled) { - "enabled" - } else { - "disabled" - } - - val canRemoveLastPaymentMethodStringified = if (canRemoveLastPaymentMethod) { - "enabled" - } else { - "disabled" - } + val isPaymentMethodRemoveStringified = isPaymentMethodRemoveEnabled.toFeatureState() + val canRemoveLastPaymentMethodStringified = canRemoveLastPaymentMethod.toFeatureState() + val setAsDefaultFeatureEnabledStringified = setAsDefaultFeatureEnabled.toFeatureState() return """ { @@ -442,7 +495,8 @@ internal class CustomerSessionPaymentSheetActivityTest { "payment_method_save": "enabled", "payment_method_remove": "$isPaymentMethodRemoveStringified", "payment_method_remove_last": "$canRemoveLastPaymentMethodStringified", - "payment_method_save_allow_redisplay_override": null + "payment_method_save_allow_redisplay_override": null, + "payment_method_set_as_default": $setAsDefaultFeatureEnabledStringified } }, "customer_sheet": { @@ -516,5 +570,13 @@ internal class CustomerSessionPaymentSheetActivityTest { } """.trimIndent() } + + private fun Boolean.toFeatureState(): String { + return if (this) { + "enabled" + } else { + "disabled" + } + } } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/EditPage.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/EditPage.kt index 2ad215de230..71aca2c9121 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/EditPage.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/EditPage.kt @@ -12,6 +12,7 @@ import com.stripe.android.paymentsheet.ui.REMOVE_BUTTON_LOADING import com.stripe.android.paymentsheet.ui.UPDATE_PM_REMOVE_BUTTON_TEST_TAG import com.stripe.android.paymentsheet.ui.UPDATE_PM_SAVE_BUTTON_TEST_TAG import com.stripe.android.paymentsheet.ui.UPDATE_PM_SCREEN_TEST_TAG +import com.stripe.android.paymentsheet.ui.UPDATE_PM_SET_AS_DEFAULT_CHECKBOX_TEST_TAG import com.stripe.android.ui.core.elements.TEST_TAG_DIALOG_CONFIRM_BUTTON import com.stripe.android.uicore.elements.DROPDOWN_MENU_CLICKABLE_TEST_TAG import com.stripe.android.uicore.elements.TEST_TAG_DROP_DOWN_CHOICE @@ -110,4 +111,10 @@ internal class EditPage( .isEmpty() } } + + fun clickSetAsDefaultCheckbox() { + composeTestRule.onNodeWithTag( + UPDATE_PM_SET_AS_DEFAULT_CHECKBOX_TEST_TAG + ).performClick() + } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt index a7f7d8c081e..0cf5f0e75f6 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt @@ -517,6 +517,31 @@ class SavedPaymentMethodMutatorTest { } } + @Test + fun `setDefaultPaymentMethod updates selection on success`() { + val paymentMethods = PaymentMethodFixtures.createCards(3) + + runScenario( + customerRepository = FakeCustomerRepository( + onSetDefaultPaymentMethod = { Result.success(mock()) } + ) + ) { + customerStateHolder.setCustomerState( + createCustomerState( + paymentMethods = paymentMethods, + defaultPaymentMethodId = paymentMethods.first().id, + ) + ) + + val newDefaultPaymentMethod = paymentMethods[1] + savedPaymentMethodMutator.setDefaultPaymentMethod(newDefaultPaymentMethod) + + selectionSource.test { + assertThat(awaitItem()).isEqualTo(PaymentSelection.Saved(newDefaultPaymentMethod)) + } + } + } + private fun removeDuplicatesTest(shouldRemoveDuplicates: Boolean) { val repository = FakeCustomerRepository() @@ -591,7 +616,7 @@ class SavedPaymentMethodMutatorTest { uiContext = coroutineContext, customerRepository = customerRepository, selection = selection, - clearSelection = { selection.value = null }, + setSelection = { selection.value = it }, customerStateHolder = customerStateHolder, prePaymentMethodRemoveActions = { prePaymentMethodRemovedTurbine.add(Unit) }, postPaymentMethodRemoveActions = { postPaymentMethodRemovedTurbine.add(Unit) }, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/VerticalModePage.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/VerticalModePage.kt index 56c213065a3..255b9ab6e9d 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/VerticalModePage.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/VerticalModePage.kt @@ -70,6 +70,9 @@ internal class VerticalModePage( } fun assertHasSavedPaymentMethods() { + composeTestRule.waitUntil { + composeTestRule.onNodeWithTag(TEST_TAG_SAVED_TEXT).isDisplayed() + } composeTestRule.onNodeWithTag(TEST_TAG_SAVED_TEXT).assertExists() } diff --git a/paymentsheet/src/test/resources/elements-sessions-customers-set-default-pm.json b/paymentsheet/src/test/resources/elements-sessions-customers-set-default-pm.json new file mode 100644 index 00000000000..b0832bd32b5 --- /dev/null +++ b/paymentsheet/src/test/resources/elements-sessions-customers-set-default-pm.json @@ -0,0 +1,10 @@ +{ + "id": "cus_Rkavqb9GNRJ2Ko", + "object": "customer", + "created": 1739227546, + "default_source": null, + "description": null, + "email": null, + "livemode": false, + "shipping": null +} \ No newline at end of file