Skip to content

Commit

Permalink
Add CBC dropdown to UpdatePaymentMethodUI without full functionality (#…
Browse files Browse the repository at this point in the history
…9686)

* Refactor view state out of cbc dropdown

* Rename Dropdown -> CardBrandDropdown

* Refactor view action handler out of CardBrandDropdown

* Move CardBrandChoice type out of EditPaymentMethodViewState

* Add cardBrandFilter to getAvailableNetworks function

* Update choice extension functions to work on cards instead of payment methods

* Move choice extension functions outside of interactor so they can be reused

* Add CBC drop down UI to UpdatePaymentMethodUI

* Fix lint

* Add UI test

* Fix compilation issues

* Add screenshot test with CBC dropdown

* Revert unneeded change

* Fix lint issue
  • Loading branch information
amk-stripe authored Nov 22, 2024
1 parent 5c60031 commit f6720ce
Show file tree
Hide file tree
Showing 23 changed files with 261 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ internal class CustomerSheetViewModel(
isLiveMode = isLiveModeProvider(),
canRemove = customerState.canRemove,
displayableSavedPaymentMethod = paymentMethod,
cardBrandFilter = PaymentSheetCardBrandFilter(customerState.configuration.cardBrandAcceptance),
removeExecutor = ::removeExecutor,
),
isLiveMode = isLiveModeProvider(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ internal class SavedPaymentMethodMutator(
isLiveMode = isLiveModeProvider(),
canRemove = canRemove.value,
displayableSavedPaymentMethod,
cardBrandFilter = cardBrandFilter,
removeExecutor = ::removePaymentMethodInEditScreen,
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stripe.android.paymentsheet.ui

import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.model.CardBrand
import com.stripe.android.uicore.elements.SingleChoiceDropdownItem

internal data class CardBrandChoice(
val brand: CardBrand
) : SingleChoiceDropdownItem {
override val icon: Int
get() = brand.icon

override val label: ResolvableString
get() = brand.displayName.resolvableString
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.stripe.android.paymentsheet.ui

import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.uicore.R
import com.stripe.android.uicore.elements.DROPDOWN_MENU_CLICKABLE_TEST_TAG
import com.stripe.android.uicore.elements.SingleChoiceDropdown
import com.stripe.android.uicore.stripeColors

@Composable
internal fun CardBrandDropdown(
selectedBrand: CardBrandChoice,
availableBrands: List<CardBrandChoice>,
onBrandOptionsShown: () -> Unit,
onBrandChoiceChanged: (CardBrandChoice) -> Unit,
onBrandChoiceOptionsDismissed: () -> Unit,
) {
var expanded by remember {
mutableStateOf(false)
}

Box(
modifier = Modifier
.clickable {
if (!expanded) {
expanded = true

onBrandOptionsShown()
}
}
.semantics {
this.contentDescription = selectedBrand.brand.displayName
}
.testTag(DROPDOWN_MENU_CLICKABLE_TEST_TAG)
) {
Row(
modifier = Modifier.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(id = selectedBrand.icon),
contentDescription = null
)

Icon(
painter = painterResource(
id = R.drawable.stripe_ic_chevron_down
),
contentDescription = null
)
}

SingleChoiceDropdown(
expanded = expanded,
title = com.stripe.android.R.string.stripe_card_brand_choice_selection_header.resolvableString,
currentChoice = selectedBrand,
choices = availableBrands,
headerTextColor = MaterialTheme.stripeColors.subtitle,
optionTextColor = MaterialTheme.stripeColors.onComponent,
onChoiceSelected = { item ->
expanded = false

onBrandChoiceChanged(item)
},
onDismiss = {
expanded = false

onBrandChoiceOptionsDismissed()
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,22 @@
package com.stripe.android.paymentsheet.ui

import androidx.annotation.RestrictTo
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.stripe.android.common.ui.PrimaryButton
Expand All @@ -44,16 +31,12 @@ import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction.OnRemovePr
import com.stripe.android.paymentsheet.ui.EditPaymentMethodViewAction.OnUpdatePressed
import com.stripe.android.ui.core.elements.SimpleDialogElementUI
import com.stripe.android.uicore.StripeTheme
import com.stripe.android.uicore.elements.DROPDOWN_MENU_CLICKABLE_TEST_TAG
import com.stripe.android.uicore.elements.SectionCard
import com.stripe.android.uicore.elements.SingleChoiceDropdown
import com.stripe.android.uicore.elements.TextFieldColors
import com.stripe.android.uicore.strings.resolve
import com.stripe.android.uicore.stripeColors
import com.stripe.android.uicore.utils.collectAsState
import com.stripe.android.R as PaymentsCoreR
import com.stripe.android.R as StripeR
import com.stripe.android.uicore.R as UiCoreR

@Composable
internal fun EditPaymentMethod(
Expand Down Expand Up @@ -99,7 +82,21 @@ internal fun EditPaymentMethodUi(
)
},
trailingIcon = {
Dropdown(viewState, viewActionHandler)
CardBrandDropdown(
selectedBrand = viewState.selectedBrand,
availableBrands = viewState.availableBrands,
onBrandOptionsShown = {
viewActionHandler.invoke(EditPaymentMethodViewAction.OnBrandChoiceOptionsShown)
},
onBrandChoiceChanged = {
viewActionHandler.invoke(
EditPaymentMethodViewAction.OnBrandChoiceChanged(it)
)
},
onBrandChoiceOptionsDismissed = {
viewActionHandler.invoke(EditPaymentMethodViewAction.OnBrandChoiceOptionsDismissed)
}
)
},
onValueChange = {}
)
Expand Down Expand Up @@ -173,70 +170,6 @@ private fun Label(
)
}

@Composable
private fun Dropdown(
viewState: EditPaymentMethodViewState,
viewActionHandler: (action: EditPaymentMethodViewAction) -> Unit
) {
var expanded by remember {
mutableStateOf(false)
}

Box(
modifier = Modifier
.clickable {
if (!expanded) {
expanded = true

viewActionHandler.invoke(EditPaymentMethodViewAction.OnBrandChoiceOptionsShown)
}
}
.semantics {
this.contentDescription = viewState.selectedBrand.brand.displayName
}
.testTag(DROPDOWN_MENU_CLICKABLE_TEST_TAG)
) {
Row(
modifier = Modifier.padding(10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Image(
painter = painterResource(id = viewState.selectedBrand.icon),
contentDescription = null
)

Icon(
painter = painterResource(
id = UiCoreR.drawable.stripe_ic_chevron_down
),
contentDescription = null
)
}

SingleChoiceDropdown(
expanded = expanded,
title = PaymentsCoreR.string.stripe_card_brand_choice_selection_header.resolvableString,
currentChoice = viewState.selectedBrand,
choices = viewState.availableBrands,
headerTextColor = MaterialTheme.stripeColors.subtitle,
optionTextColor = MaterialTheme.stripeColors.onComponent,
onChoiceSelected = { item ->
expanded = false

viewActionHandler.invoke(
EditPaymentMethodViewAction.OnBrandChoiceChanged(item)
)
},
onDismiss = {
expanded = false

viewActionHandler.invoke(EditPaymentMethodViewAction.OnBrandChoiceOptionsDismissed)
}
)
}
}

@Composable
@Preview(showBackground = true)
private fun EditPaymentMethodPreview() {
Expand All @@ -246,15 +179,15 @@ private fun EditPaymentMethodPreview() {
status = EditPaymentMethodViewState.Status.Idle,
last4 = "4242",
displayName = "Card".resolvableString,
selectedBrand = EditPaymentMethodViewState.CardBrandChoice(
selectedBrand = CardBrandChoice(
brand = CardBrand.CartesBancaires
),
canUpdate = true,
availableBrands = listOf(
EditPaymentMethodViewState.CardBrandChoice(
CardBrandChoice(
brand = CardBrand.Visa
),
EditPaymentMethodViewState.CardBrandChoice(
CardBrandChoice(
brand = CardBrand.CartesBancaires
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ internal sealed interface EditPaymentMethodViewAction {
object OnBrandChoiceOptionsDismissed : EditPaymentMethodViewAction

data class OnBrandChoiceChanged(
val choice: EditPaymentMethodViewState.CardBrandChoice
val choice: CardBrandChoice
) : EditPaymentMethodViewAction

object OnRemovePressed : EditPaymentMethodViewAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ internal class DefaultEditPaymentMethodViewInteractor(
private val cardBrandFilter: CardBrandFilter,
workContext: CoroutineContext = Dispatchers.Default,
) : ModifiableEditPaymentMethodViewInteractor {
private val choice = MutableStateFlow(initialPaymentMethod.getPreferredChoice())
private val choice = MutableStateFlow(initialPaymentMethod.getCard().getPreferredChoice())
private val status = MutableStateFlow(EditPaymentMethodViewState.Status.Idle)
private val paymentMethod = MutableStateFlow(initialPaymentMethod)
private val confirmRemoval = MutableStateFlow(false)
Expand All @@ -79,8 +79,8 @@ internal class DefaultEditPaymentMethodViewInteractor(
confirmRemoval,
error,
) { paymentMethod, choice, status, confirmDeletion, error ->
val savedChoice = paymentMethod.getPreferredChoice()
val availableChoices = paymentMethod.getAvailableNetworks().filter { cardBrandFilter.isAccepted(it.brand) }
val savedChoice = paymentMethod.getCard().getPreferredChoice()
val availableChoices = paymentMethod.getCard().getAvailableNetworks(cardBrandFilter)

EditPaymentMethodViewState(
last4 = paymentMethod.getLast4(),
Expand Down Expand Up @@ -134,7 +134,7 @@ internal class DefaultEditPaymentMethodViewInteractor(
val currentPaymentMethod = paymentMethod.value
val currentChoice = choice.value

if (currentPaymentMethod.getPreferredChoice() != currentChoice) {
if (currentPaymentMethod.getCard().getPreferredChoice() != currentChoice) {
coroutineScope.launch {
error.emit(null)
status.emit(EditPaymentMethodViewState.Status.Updating)
Expand All @@ -160,7 +160,7 @@ internal class DefaultEditPaymentMethodViewInteractor(
eventHandler(EditPaymentMethodViewInteractor.Event.HideBrands(brand = null))
}

private fun onBrandChoiceChanged(choice: EditPaymentMethodViewState.CardBrandChoice) {
private fun onBrandChoiceChanged(choice: CardBrandChoice) {
this.choice.value = choice

eventHandler(EditPaymentMethodViewInteractor.Event.HideBrands(brand = choice.brand))
Expand All @@ -175,26 +175,10 @@ internal class DefaultEditPaymentMethodViewInteractor(
?: throw IllegalStateException("Card payment method must contain last 4 digits")
}

private fun PaymentMethod.getPreferredChoice(): EditPaymentMethodViewState.CardBrandChoice {
return CardBrand.fromCode(getCard().displayBrand).toChoice()
}

private fun PaymentMethod.getAvailableNetworks(): List<EditPaymentMethodViewState.CardBrandChoice> {
return getCard().networks?.available?.let { brandCodes ->
brandCodes.map { code ->
CardBrand.fromCode(code).toChoice()
}
} ?: listOf()
}

private fun PaymentMethod.getCard(): PaymentMethod.Card {
return card ?: throw IllegalStateException("Payment method must be a card in order to be edited")
}

private fun CardBrand.toChoice(): EditPaymentMethodViewState.CardBrandChoice {
return EditPaymentMethodViewState.CardBrandChoice(brand = this)
}

object Factory : ModifiableEditPaymentMethodViewInteractor.Factory {
override fun create(
initialPaymentMethod: PaymentMethod,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.stripe.android.paymentsheet.ui

import com.stripe.android.core.strings.ResolvableString
import com.stripe.android.core.strings.resolvableString
import com.stripe.android.model.CardBrand
import com.stripe.android.uicore.elements.SingleChoiceDropdownItem

internal data class EditPaymentMethodViewState(
val status: Status,
Expand All @@ -21,14 +18,4 @@ internal data class EditPaymentMethodViewState(
Updating,
Removing
}

data class CardBrandChoice(
val brand: CardBrand
) : SingleChoiceDropdownItem {
override val icon: Int
get() = brand.icon

override val label: ResolvableString
get() = brand.displayName.resolvableString
}
}
Loading

0 comments on commit f6720ce

Please sign in to comment.