Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CBC dropdown to UpdatePaymentMethodUI without full functionality #9686

Merged
merged 15 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved from EditPaymentMethod. It previously took in an EditPaymentMethodViewState and an EditPaymentMethodViewActionHandler as params. I refactored it so that it didn't rely on those specific types, so that we could re-use it in UpdatePaymentMethodUI

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
Loading