Skip to content

Commit

Permalink
Fix Link default card change ui states (#10108)
Browse files Browse the repository at this point in the history
  • Loading branch information
toluo-stripe authored Feb 13, 2025
1 parent 6a3e1bd commit f5bec10
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 48 deletions.
6 changes: 4 additions & 2 deletions paymentsheet/api/paymentsheet.api
Original file line number Diff line number Diff line change
Expand Up @@ -522,9 +522,11 @@ public final class com/stripe/android/link/ui/verification/ComposableSingletons$

public final class com/stripe/android/link/ui/wallet/ComposableSingletons$PaymentDetailsKt {
public static final field INSTANCE Lcom/stripe/android/link/ui/wallet/ComposableSingletons$PaymentDetailsKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
public static field lambda-2 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$paymentsheet_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-1$paymentsheet_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$paymentsheet_release ()Lkotlin/jvm/functions/Function2;
}

public final class com/stripe/android/link/ui/wallet/ComposableSingletons$WalletScreenKt {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.link.ui.wallet

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -80,7 +81,7 @@ internal fun PaymentDetailsListItem(
) {
PaymentDetails(paymentDetails = paymentDetails)

if (paymentDetails.isDefault) {
AnimatedVisibility(paymentDetails.isDefault) {
DefaultTag()
}

Expand Down Expand Up @@ -118,7 +119,9 @@ private fun MenuAndLoader(
) {
if (isUpdating) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
modifier = Modifier
.testTag(WALLET_PAYMENT_DETAIL_ITEM_LOADING_INDICATOR)
.size(24.dp),
strokeWidth = 2.dp
)
} else {
Expand Down Expand Up @@ -262,3 +265,4 @@ private fun RowScope.BankAccountInfo(

internal const val WALLET_PAYMENT_DETAIL_ITEM_RADIO_BUTTON = "wallet_payment_detail_item_radio_button"
internal const val WALLET_PAYMENT_DETAIL_ITEM_MENU_BUTTON = "wallet_payment_detail_item_menu_button"
internal const val WALLET_PAYMENT_DETAIL_ITEM_LOADING_INDICATOR = "wallet_payment_detail_item_loading_indicator"
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.stripe.android.link.ui.wallet

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
Expand Down Expand Up @@ -381,6 +382,7 @@ internal fun CollapsedPaymentDetails(
}
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ExpandedPaymentDetails(
uiState: WalletUiState,
Expand Down Expand Up @@ -433,6 +435,7 @@ private fun ExpandedPaymentDetails(
}
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PaymentDetailsList(
uiState: WalletUiState,
Expand All @@ -453,11 +456,12 @@ private fun PaymentDetailsList(
) { item ->
PaymentDetailsListItem(
modifier = Modifier
.animateItemPlacement()
.testTag(WALLET_SCREEN_PAYMENT_METHODS_LIST),
paymentDetails = item,
enabled = isEnabled,
isSelected = uiState.selectedItem?.id == item.id,
isUpdating = false,
isUpdating = uiState.cardBeingUpdated == item.id,
onClick = {
onItemSelected(item)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ internal data class WalletUiState(
val primaryButtonLabel: ResolvableString,
val hasCompleted: Boolean,
val canAddNewPaymentMethod: Boolean,
val cardBeingUpdated: String? = null,
val errorMessage: ResolvableString? = null,
val expiryDateInput: FormFieldEntry = FormFieldEntry(null),
val cvcInput: FormFieldEntry = FormFieldEntry(null),
Expand All @@ -35,16 +36,21 @@ internal data class WalletUiState(
val isMissingCvcInput = cvcInput.isComplete.not()

val disableButton = (isExpired && isMissingExpiryDateInput) ||
(requiresCvcRecollection && isMissingCvcInput)
(requiresCvcRecollection && isMissingCvcInput) || (cardBeingUpdated != null)

return if (hasCompleted) {
PrimaryButtonState.Completed
} else if (isProcessing) {
PrimaryButtonState.Processing
} else if (disableButton) {
PrimaryButtonState.Disabled
} else {
PrimaryButtonState.Enabled
return when {
hasCompleted -> {
PrimaryButtonState.Completed
}
isProcessing -> {
PrimaryButtonState.Processing
}
disableButton -> {
PrimaryButtonState.Disabled
}
else -> {
PrimaryButtonState.Enabled
}
}
}

Expand All @@ -66,7 +72,8 @@ internal data class WalletUiState(
return copy(
paymentDetailsList = response.paymentDetails,
selectedItem = selectedItem,
isProcessing = false
isProcessing = false,
cardBeingUpdated = null
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ internal class WalletViewModel @Inject constructor(
)

init {
loadPaymentDetails()
_uiState.update {
it.setProcessing()
}

viewModelScope.launch {
loadPaymentDetails(selectedItemId = null)
}

viewModelScope.launch {
expiryDateController.formFieldValue.collectLatest { input ->
Expand All @@ -93,28 +99,22 @@ internal class WalletViewModel @Inject constructor(
}
}

private fun loadPaymentDetails(selectedItemId: String? = null) {
_uiState.update {
it.setProcessing()
}

viewModelScope.launch {
linkAccountManager.listPaymentDetails(
paymentMethodTypes = stripeIntent.supportedPaymentMethodTypes(linkAccount)
).fold(
onSuccess = { response ->
_uiState.update {
it.updateWithResponse(response, selectedItemId = selectedItemId)
}
private suspend fun loadPaymentDetails(selectedItemId: String?) {
linkAccountManager.listPaymentDetails(
paymentMethodTypes = stripeIntent.supportedPaymentMethodTypes(linkAccount)
).fold(
onSuccess = { response ->
_uiState.update {
it.updateWithResponse(response, selectedItemId = selectedItemId)
}

if (response.paymentDetails.isEmpty()) {
navigateAndClearStack(LinkScreen.PaymentMethod)
}
},
// If we can't load the payment details there's nothing to see here
onFailure = ::onFatal
)
}
if (response.paymentDetails.isEmpty()) {
navigateAndClearStack(LinkScreen.PaymentMethod)
}
},
// If we can't load the payment details there's nothing to see here
onFailure = ::onFatal
)
}

private fun onFatal(fatalError: Throwable) {
Expand Down Expand Up @@ -255,7 +255,9 @@ internal class WalletViewModel @Inject constructor(

fun onSetDefaultClicked(item: ConsumerPaymentDetails.PaymentDetails) {
_uiState.update {
it.setProcessing()
it.copy(
cardBeingUpdated = item.id,
)
}
viewModelScope.launch {
val updateParams = ConsumerPaymentDetailsUpdateParams(
Expand All @@ -266,7 +268,22 @@ internal class WalletViewModel @Inject constructor(
linkAccountManager.updatePaymentDetails(updateParams)
.fold(
onSuccess = {
loadPaymentDetails()
_uiState.update { state ->
state.copy(
paymentDetailsList = state.paymentDetailsList.map { details ->
when (details) {
is ConsumerPaymentDetails.BankAccount -> {
details.copy(isDefault = item.id == details.id)
}
is ConsumerPaymentDetails.Card -> {
details.copy(isDefault = item.id == details.id)
}
is ConsumerPaymentDetails.Passthrough -> details
}
},
cardBeingUpdated = null
)
}
},
onFailure = { error ->
updateErrorMessageAndStopProcessing(
Expand Down Expand Up @@ -304,7 +321,8 @@ internal class WalletViewModel @Inject constructor(
_uiState.update {
it.copy(
alertMessage = error.stripeErrorMessage(),
isProcessing = false
isProcessing = false,
cardBeingUpdated = null
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,21 @@ import com.stripe.android.link.ui.PrimaryButtonState
import com.stripe.android.link.ui.PrimaryButtonTag
import com.stripe.android.model.CardBrand
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.CvcCheck
import com.stripe.android.testing.CoroutineTestRule
import com.stripe.android.testing.FakeLogger
import com.stripe.android.ui.core.elements.CvcController
import com.stripe.android.uicore.elements.DateConfig
import com.stripe.android.uicore.elements.SimpleTextFieldController
import com.stripe.android.uicore.utils.stateFlowOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.time.Duration.Companion.seconds
import com.stripe.android.link.confirmation.Result as LinkConfirmationResult

@RunWith(AndroidJUnit4::class)
Expand Down Expand Up @@ -480,6 +483,51 @@ internal class WalletScreenTest {
onWalletPaymentMethodMenu().assertIsDisplayed()
}

@Test
fun `pay method row is loading when card is being updated`() = runTest(dispatcher) {
val linkAccountManager = object : FakeLinkAccountManager() {
override suspend fun updatePaymentDetails(
updateParams: ConsumerPaymentDetailsUpdateParams
): Result<ConsumerPaymentDetails> {
delay(1.seconds)
return super.updatePaymentDetails(updateParams)
}
}
val card1 = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(id = "card1", isDefault = false)
val card2 = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD.copy(id = "card2", isDefault = true)
linkAccountManager.listPaymentDetailsResult = Result.success(
ConsumerPaymentDetails(paymentDetails = listOf(card1, card2))
)

val viewModel = createViewModel(linkAccountManager)
composeTestRule.setContent {
WalletScreen(
viewModel = viewModel,
showBottomSheetContent = {},
hideBottomSheetContent = {}
)
}
composeTestRule.waitForIdle()

onCollapsedWalletRow()
.performClick()
composeTestRule.waitForIdle()

viewModel.onSetDefaultClicked(card1)

composeTestRule.waitForIdle()

onWalletPaymentMethodRowLoadingIndicator().assertIsDisplayed()
onWalletPayButton().assertIsNotEnabled()

dispatcher.scheduler.advanceTimeBy(1.1.seconds)

onWalletPaymentMethodRowLoadingIndicator().assertDoesNotExist()
onWalletPayButton()
.assertExists()
.assertIsEnabled()
}

@Test
fun `wallet menu is dismissed on cancel clicked`() = runTest(dispatcher) {
testMenu(
Expand Down Expand Up @@ -654,6 +702,9 @@ internal class WalletScreenTest {
private fun onWalletPaymentMethodRowMenuButton() =
composeTestRule.onAllNodes(hasTestTag(WALLET_PAYMENT_DETAIL_ITEM_MENU_BUTTON), useUnmergedTree = true)

private fun onWalletPaymentMethodRowLoadingIndicator() =
composeTestRule.onNodeWithTag(WALLET_PAYMENT_DETAIL_ITEM_LOADING_INDICATOR, useUnmergedTree = true)

private fun onWalletPaymentMethodMenu() =
composeTestRule.onNodeWithTag(WALLET_SCREEN_MENU_SHEET_TAG, useUnmergedTree = true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ class WalletUiStateTest {
assertThat(state.primaryButtonState).isEqualTo(PrimaryButtonState.Enabled)
}

@Test
fun testDisabledButtonStateWhenCardIsBeingUpdated() {
val state = walletUiState(
selectedItem = TestFactory.CONSUMER_PAYMENT_DETAILS_CARD,
cardBeingUpdated = "id"
)

assertThat(state.primaryButtonState).isEqualTo(PrimaryButtonState.Disabled)
}

private fun walletUiState(
paymentDetailsList: List<ConsumerPaymentDetails.PaymentDetails> =
TestFactory.CONSUMER_PAYMENT_DETAILS.paymentDetails,
Expand All @@ -157,6 +167,7 @@ class WalletUiStateTest {
expiryDateInput: FormFieldEntry = FormFieldEntry(null),
cvcInput: FormFieldEntry = FormFieldEntry(null),
canAddNewPaymentMethod: Boolean = true,
cardBeingUpdated: String? = null
): WalletUiState {
return WalletUiState(
paymentDetailsList = paymentDetailsList,
Expand All @@ -167,6 +178,7 @@ class WalletUiStateTest {
expiryDateInput = expiryDateInput,
cvcInput = cvcInput,
canAddNewPaymentMethod = canAddNewPaymentMethod,
cardBeingUpdated = cardBeingUpdated
)
}
}
Loading

0 comments on commit f5bec10

Please sign in to comment.