Skip to content

Commit

Permalink
Actually update default payment method when set as default checkbox c…
Browse files Browse the repository at this point in the history
…hecked on manage PM screen (#10249)

* Add setDefaultPaymentMethodExecutor to DefaultUpdatePaymentMethodInteractor

* Use setDefaultPaymentMethodExecutor in DefaultUpdatePaymentMethodInteractor

* Implement setDefaultPaymentMethodExecutors
  • Loading branch information
amk-stripe authored Feb 21, 2025
1 parent 6e469d3 commit 69caba2
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 16 deletions.
4 changes: 4 additions & 0 deletions paymentsheet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
<string name="stripe_paymentsheet_card_details_cannot_be_changed">Card details cannot be changed.</string>
<!-- Text displayed below a credit card entry form when the card will be saved to make future payments. -->
<string name="stripe_paymentsheet_card_mandate">By providing your card information, you allow %s to charge your card for future payments in accordance with their terms.</string>
<!-- Error message displayed when updating a card fails. -->
<string name="stripe_paymentsheet_card_updates_failed_error_message">Card was not updated. Please try again.</string>
<!-- Title shown above a carousel containing available payment methods -->
<string name="stripe_paymentsheet_choose_payment_method">Choose a payment method</string>
<string name="stripe_paymentsheet_close">Close</string>
Expand Down Expand Up @@ -141,6 +143,8 @@
<string name="stripe_paymentsheet_select_your_payment_method">Select your payment method</string>
<!-- Text displayed on manage SEPA debit screen when SEPA debit details are not editable. -->
<string name="stripe_paymentsheet_sepa_debit_details_cannot_be_changed">SEPA debit details cannot be changed.</string>
<!-- Error message displayed when setting a default payment method fails. -->
<string name="stripe_paymentsheet_set_default_payment_method_failed_error_message">Default payment method was not changed. Please try again.</string>
<!-- Label indicating the total amount that the user is authorizing (e.g. "Total: $25.00") -->
<string name="stripe_paymentsheet_total_amount">Total: %s</string>
<!-- Label of a checkbox that when checked makes a payment method as the default one. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ internal class CustomerSheetViewModel(
workContext = workContext,
// This checkbox is never displayed in CustomerSheet.
shouldShowSetAsDefaultCheckbox = false,
// Should never be called from CustomerSheet, because we don't enable the set as default checkbox.
setDefaultPaymentMethodExecutor = {
Result.failure(
IllegalStateException("Unexpected attempt to update default from CustomerSheet.")
)
},
),
isLiveMode = isLiveModeProvider(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ internal class DefaultEmbeddedContentHelper @Inject constructor(
customerStateHolder = customerStateHolder,
prePaymentMethodRemoveActions = {},
postPaymentMethodRemoveActions = {},
onUpdatePaymentMethod = { _, _, _, _ ->
onUpdatePaymentMethod = { _, _, _, _, _ ->
sheetLauncher?.launchManage(
paymentMethodMetadata = paymentMethodMetadata,
customerState = requireNotNull(customerStateHolder.customer.value),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ internal class DefaultEmbeddedUpdateScreenInteractorFactory @Inject constructor(
},
)
},
setDefaultPaymentMethodExecutor = { method ->
savedPaymentMethodMutatorProvider.get().setDefaultPaymentMethod(method)
},
onBrandChoiceOptionsShown = {
eventReporter.onShowPaymentOptionBrands(
source = EventReporter.CardBrandChoiceEventSource.Edit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal class ManageSavedPaymentMethodMutatorFactory @Inject constructor(
}
},
postPaymentMethodRemoveActions = ::onPaymentMethodRemoved,
onUpdatePaymentMethod = { displayableSavedPaymentMethod, _, _, _ ->
onUpdatePaymentMethod = { displayableSavedPaymentMethod, _, _, _, _ ->
onUpdatePaymentMethod(displayableSavedPaymentMethod)
},
isLinkEnabled = stateFlowOf(false), // Link is never enabled in the manage screen.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ internal class CustomerStateHolder(
updateMostRecentlySelectedSavedPaymentMethod(newSelection)
}

fun setDefaultPaymentMethod(paymentMethod: PaymentMethod?) {
val newCustomer = customer.value?.copy(defaultPaymentMethodId = paymentMethod?.id)

savedStateHandle[SAVED_CUSTOMER] = newCustomer
updateMostRecentlySelectedSavedPaymentMethod(paymentMethod = paymentMethod)
}

fun updateMostRecentlySelectedSavedPaymentMethod(paymentMethod: PaymentMethod?) {
savedStateHandle[SAVED_PM_SELECTION] = paymentMethod
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ internal class SavedPaymentMethodMutator(
canRemove: Boolean,
performRemove: suspend () -> Throwable?,
updateExecutor: suspend (brand: CardBrand) -> Result<PaymentMethod>,
setDefaultPaymentMethodExecutor: suspend (paymentMethod: PaymentMethod) -> Result<Unit>,
) -> Unit,
isLinkEnabled: StateFlow<Boolean?>,
isNotPaymentFlow: Boolean,
Expand Down Expand Up @@ -201,10 +202,29 @@ internal class SavedPaymentMethodMutator(
},
{ cardBrand ->
modifyCardPaymentMethod(paymentMethod, cardBrand)
}
},
::setDefaultPaymentMethod,
)
}

internal suspend fun setDefaultPaymentMethod(paymentMethod: PaymentMethod): Result<Unit> {
val customer = customerStateHolder.customer.value
?: return Result.failure(
IllegalStateException("Unable to set default payment method when customer is null.")
)

return customerRepository.setDefaultPaymentMethod(
customerInfo = CustomerRepository.CustomerInfo(
id = customer.id,
ephemeralKeySecret = customer.ephemeralKeySecret,
customerSessionClientSecret = customer.customerSessionClientSecret,
),
paymentMethodId = paymentMethod.id,
).onSuccess {
customerStateHolder.setDefaultPaymentMethod(paymentMethod = paymentMethod)
}.map {}
}

suspend fun removePaymentMethodInEditScreen(paymentMethod: PaymentMethod): Throwable? {
val paymentMethodId = paymentMethod.id!!
val result = removePaymentMethodInternal(paymentMethodId)
Expand Down Expand Up @@ -323,6 +343,7 @@ internal class SavedPaymentMethodMutator(
canRemove: Boolean,
performRemove: suspend () -> Throwable?,
updateCardBrandExecutor: suspend (brand: CardBrand) -> Result<PaymentMethod>,
setDefaultPaymentMethodExecutor: suspend (paymentMethod: PaymentMethod) -> Result<Unit>,
) {
if (displayableSavedPaymentMethod.savedPaymentMethod != SavedPaymentMethod.Unexpected) {
val isLiveMode = requireNotNull(viewModel.paymentMethodMetadata.value).stripeIntent.isLiveMode
Expand All @@ -339,6 +360,7 @@ internal class SavedPaymentMethodMutator(
updateCardBrandExecutor = { method, brand ->
updateCardBrandExecutor(brand)
},
setDefaultPaymentMethodExecutor = setDefaultPaymentMethodExecutor,
onBrandChoiceOptionsShown = {
viewModel.eventReporter.onShowPaymentOptionBrands(
source = EventReporter.CardBrandChoiceEventSource.Edit,
Expand Down Expand Up @@ -385,13 +407,15 @@ internal class SavedPaymentMethodMutator(
onUpdatePaymentMethod = { displayableSavedPaymentMethod,
canRemove,
performRemove,
updateCardBrandExecutor ->
updateCardBrandExecutor,
setDefaultPaymentMethodExecutor, ->
onUpdatePaymentMethod(
viewModel = viewModel,
displayableSavedPaymentMethod = displayableSavedPaymentMethod,
canRemove = canRemove,
performRemove = performRemove,
updateCardBrandExecutor = updateCardBrandExecutor,
setDefaultPaymentMethodExecutor = setDefaultPaymentMethodExecutor,
)
},
isLinkEnabled = viewModel.linkHandler.isLinkEnabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.paymentsheet.ui

import androidx.annotation.VisibleForTesting
import com.stripe.android.CardBrandFilter
import com.stripe.android.common.exception.stripeErrorMessage
import com.stripe.android.core.strings.ResolvableString
Expand Down Expand Up @@ -76,6 +77,9 @@ internal typealias UpdateCardBrandOperation = suspend (
paymentMethod: PaymentMethod,
brand: CardBrand
) -> Result<PaymentMethod>
internal typealias PaymentMethodSetAsDefaultOperation = suspend (
paymentMethod: PaymentMethod
) -> Result<Unit>

internal class DefaultUpdatePaymentMethodInteractor(
isLiveMode: Boolean,
Expand All @@ -85,6 +89,7 @@ internal class DefaultUpdatePaymentMethodInteractor(
shouldShowSetAsDefaultCheckbox: Boolean,
private val removeExecutor: PaymentMethodRemoveOperation,
private val updateCardBrandExecutor: UpdateCardBrandOperation,
private val setDefaultPaymentMethodExecutor: PaymentMethodSetAsDefaultOperation,
private val onBrandChoiceOptionsShown: (CardBrand) -> Unit,
private val onBrandChoiceOptionsDismissed: (CardBrand) -> Unit,
private val onUpdateSuccess: () -> Unit,
Expand Down Expand Up @@ -171,9 +176,11 @@ internal class DefaultUpdatePaymentMethodInteractor(
status.emit(UpdatePaymentMethodInteractor.Status.Updating)

val updateCardBrandResult = maybeUpdateCardBrand()
val setDefaultPaymentMethodResult = maybeSetDefaultPaymentMethod()

val updateResult = getUpdateResult(
updateCardBrandResult = updateCardBrandResult,
setDefaultPaymentMethodResult = setDefaultPaymentMethodResult,
)

when (updateResult) {
Expand Down Expand Up @@ -201,16 +208,30 @@ internal class DefaultUpdatePaymentMethodInteractor(
}
}

private suspend fun maybeSetDefaultPaymentMethod(): Result<Unit>? {
return if (setAsDefaultCheckboxChecked.value) {
setDefaultPaymentMethodExecutor(displayableSavedPaymentMethod.paymentMethod)
} else {
null
}
}

private fun getUpdateResult(
updateCardBrandResult: Result<PaymentMethod>?,
setDefaultPaymentMethodResult: Result<Unit>?,
): UpdateResult {
return when {
updateCardBrandResult == null -> UpdateResult.NoUpdatesMade
updateCardBrandResult.isFailure -> UpdateResult.Error(
updateCardBrandResult.exceptionOrNull()?.stripeErrorMessage()
)
updateCardBrandResult.isSuccess -> UpdateResult.Success
else -> UpdateResult.NoUpdatesMade
if (updateCardBrandResult == null && setDefaultPaymentMethodResult == null) {
return UpdateResult.NoUpdatesMade
}

return if (updateCardBrandResult?.isFailure == true && setDefaultPaymentMethodResult?.isFailure == true) {
UpdateResult.Error(setDefaultPaymentMethodErrorMessage)
} else if (updateCardBrandResult?.isFailure == true) {
UpdateResult.Error(updateCardBrandResult.exceptionOrNull()?.stripeErrorMessage())
} else if (setDefaultPaymentMethodResult?.isFailure == true) {
UpdateResult.Error(updatesFailedErrorMessage)
} else {
UpdateResult.Success
}
}

Expand Down Expand Up @@ -256,6 +277,17 @@ internal class DefaultUpdatePaymentMethodInteractor(
data object Success : UpdateResult()
data object NoUpdatesMade : UpdateResult()
}

companion object {

@VisibleForTesting
internal val setDefaultPaymentMethodErrorMessage =
R.string.stripe_paymentsheet_set_default_payment_method_failed_error_message.resolvableString

@VisibleForTesting
internal val updatesFailedErrorMessage =
R.string.stripe_paymentsheet_set_default_payment_method_failed_error_message.resolvableString
}
}

internal const val PaymentMethodRemovalDelayMillis = 600L
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ private fun PreviewUpdatePaymentMethodUI() {
displayableSavedPaymentMethod = exampleCard,
removeExecutor = { null },
updateCardBrandExecutor = { paymentMethod, _ -> Result.success(paymentMethod) },
setDefaultPaymentMethodExecutor = { _ -> Result.success(Unit) },
cardBrandFilter = DefaultCardBrandFilter,
onBrandChoiceOptionsShown = {},
onBrandChoiceOptionsDismissed = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ internal class CustomerSheetScreenshotTest {
displayableSavedPaymentMethod = PaymentMethodFixtures.displayableCard(),
removeExecutor = { null },
updateCardBrandExecutor = { paymentMethod, _ -> Result.success(paymentMethod) },
setDefaultPaymentMethodExecutor = { _ -> Result.success(Unit) },
canRemove = canRemove,
isLiveMode = true,
cardBrandFilter = DefaultCardBrandFilter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import com.stripe.android.paymentsheet.state.CustomerState

internal fun createCustomerState(
paymentMethods: List<PaymentMethod>,
isRemoveEnabled: Boolean,
canRemoveLastPaymentMethod: Boolean,
isRemoveEnabled: Boolean = true,
canRemoveLastPaymentMethod: Boolean = true,
defaultPaymentMethodId: String? = null,
): CustomerState {
return CustomerState(
id = "cus_1",
Expand All @@ -18,6 +19,6 @@ internal fun createCustomerState(
canRemoveDuplicates = true,
canRemoveLastPaymentMethod = canRemoveLastPaymentMethod,
),
defaultPaymentMethodId = null,
defaultPaymentMethodId = defaultPaymentMethodId,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,30 @@ class SavedPaymentMethodMutatorTest {
calledUpdate.ensureAllEventsConsumed()
}

@Test
fun `setDefaultPaymentMethod updates default payment method on success`() {
val customerRepository = FakeCustomerRepository(
onSetDefaultPaymentMethod = { Result.success(mock()) }
)
val paymentMethods = PaymentMethodFixtures.createCards(3)

runScenario(customerRepository = customerRepository) {
customerStateHolder.setCustomerState(
createCustomerState(
paymentMethods = paymentMethods,
defaultPaymentMethodId = paymentMethods.first().id,
)
)

val newDefaultPaymentMethod = paymentMethods[1]
savedPaymentMethodMutator.setDefaultPaymentMethod(newDefaultPaymentMethod)

customerStateHolder.customer.test {
assertThat(awaitItem()?.defaultPaymentMethodId).isEqualTo(newDefaultPaymentMethod.id)
}
}
}

private fun removeDuplicatesTest(shouldRemoveDuplicates: Boolean) {
val repository = FakeCustomerRepository()

Expand Down Expand Up @@ -571,9 +595,20 @@ class SavedPaymentMethodMutatorTest {
customerStateHolder = customerStateHolder,
prePaymentMethodRemoveActions = { prePaymentMethodRemovedTurbine.add(Unit) },
postPaymentMethodRemoveActions = { postPaymentMethodRemovedTurbine.add(Unit) },
onUpdatePaymentMethod = { displayableSavedPaymentMethod, canRemove, performRemove, updateExecutor ->
onUpdatePaymentMethod = {
displayableSavedPaymentMethod,
canRemove,
performRemove,
updateExecutor,
setDefaultPaymentMethodExecutor, ->
updatePaymentMethodTurbine.add(
UpdateCall(displayableSavedPaymentMethod, canRemove, performRemove, updateExecutor)
UpdateCall(
displayableSavedPaymentMethod,
canRemove,
performRemove,
updateExecutor,
setDefaultPaymentMethodExecutor
)
)
},
isLinkEnabled = stateFlowOf(false),
Expand Down Expand Up @@ -615,5 +650,6 @@ class SavedPaymentMethodMutatorTest {
val canRemove: Boolean,
val performRemove: suspend () -> Throwable?,
val updateExecutor: suspend (brand: CardBrand) -> Result<PaymentMethod>,
val setSetDefaultPaymentMethodExecutor: suspend (paymentMethod: PaymentMethod) -> Result<Unit>,
)
}
Loading

0 comments on commit 69caba2

Please sign in to comment.