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 remove button to update payment method screen #9593

Merged
merged 3 commits into from
Nov 12, 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 @@ -41,15 +41,18 @@ internal fun PrimaryButton(
modifier: Modifier = Modifier,
isLoading: Boolean = false,
displayLockIcon: Boolean = false,
overrideBackgroundColor: Color? = null,
overrideOnBackgroundColor: Color? = null,
overrideBorderColor: Color? = null,
) {
// We need to use PaymentsTheme.primaryButtonStyle instead of MaterialTheme
// because of the rules API for primary button.
val context = LocalContext.current
val background = Color(StripeTheme.primaryButtonStyle.getBackgroundColor(context))
val onBackground = Color(StripeTheme.primaryButtonStyle.getOnBackgroundColor(context))
val background = overrideBackgroundColor ?: Color(StripeTheme.primaryButtonStyle.getBackgroundColor(context))
val onBackground = overrideOnBackgroundColor ?: Color(StripeTheme.primaryButtonStyle.getOnBackgroundColor(context))
val borderStroke = BorderStroke(
StripeTheme.primaryButtonStyle.shape.borderStrokeWidth.dp,
Color(StripeTheme.primaryButtonStyle.getBorderStrokeColor(context))
overrideBorderColor ?: Color(StripeTheme.primaryButtonStyle.getBorderStrokeColor(context))
)
val shape = RoundedCornerShape(
StripeTheme.primaryButtonStyle.shape.cornerRadius.dp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,8 +241,11 @@ internal class SavedPaymentMethodMutator(
PaymentSheetScreen.UpdatePaymentMethod(
DefaultUpdatePaymentMethodInteractor(
isLiveMode = isLiveModeProvider(),
canRemove = canRemove.value,
displayableSavedPaymentMethod,
card = it,
onRemovePaymentMethod = ::removePaymentMethod,
navigateBack = { navigationHandler.pop() },
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,31 @@ import com.stripe.android.paymentsheet.DisplayableSavedPaymentMethod

internal interface UpdatePaymentMethodInteractor {
val isLiveMode: Boolean
val canRemove: Boolean
val displayableSavedPaymentMethod: DisplayableSavedPaymentMethod
val card: PaymentMethod.Card

fun handleViewAction(viewAction: ViewAction)

sealed class ViewAction {
data object RemovePaymentMethod : ViewAction()
}
}

internal class DefaultUpdatePaymentMethodInteractor(
override val isLiveMode: Boolean,
override val canRemove: Boolean,
override val displayableSavedPaymentMethod: DisplayableSavedPaymentMethod,
override val card: PaymentMethod.Card,
) : UpdatePaymentMethodInteractor
val onRemovePaymentMethod: (PaymentMethod) -> Unit,
val navigateBack: () -> Unit,
) : UpdatePaymentMethodInteractor {
override fun handleViewAction(viewAction: UpdatePaymentMethodInteractor.ViewAction) {
when (viewAction) {
UpdatePaymentMethodInteractor.ViewAction.RemovePaymentMethod -> {
onRemovePaymentMethod(displayableSavedPaymentMethod.paymentMethod)
navigateBack()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import android.content.res.Resources
import androidx.compose.foundation.Image
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.material.Card
Expand All @@ -18,6 +20,7 @@ import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
Expand All @@ -38,6 +41,7 @@ import com.stripe.android.uicore.elements.TextFieldColors
import com.stripe.android.uicore.getBorderStroke
import com.stripe.android.uicore.stripeColors
import com.stripe.android.uicore.stripeShapes
import com.stripe.android.common.ui.PrimaryButton as PrimaryButton

@Composable
internal fun UpdatePaymentMethodUI(interactor: UpdatePaymentMethodInteractor, modifier: Modifier) {
Expand Down Expand Up @@ -95,6 +99,39 @@ internal fun UpdatePaymentMethodUI(interactor: UpdatePaymentMethodInteractor, mo
fontWeight = FontWeight.Normal,
modifier = Modifier.padding(top = 12.dp)
)

if (interactor.canRemove) {
DeletePaymentMethodUi(interactor)
}
}
}

@Composable
private fun DeletePaymentMethodUi(interactor: UpdatePaymentMethodInteractor) {
val openRemoveDialog = rememberSaveable { mutableStateOf(false) }

Spacer(modifier = Modifier.requiredHeight(32.dp))

PrimaryButton(
label = stringResource(id = R.string.stripe_remove),
isLoading = false,
isEnabled = true,
onButtonClick = {
openRemoveDialog.value = true
},
overrideBackgroundColor = MaterialTheme.colors.background,
overrideBorderColor = MaterialTheme.colors.error,
overrideOnBackgroundColor = MaterialTheme.colors.error,
modifier = Modifier.testTag(UPDATE_PM_REMOVE_BUTTON_TEST_TAG)
)

if (openRemoveDialog.value) {
RemovePaymentMethodDialogUI(paymentMethod = interactor.displayableSavedPaymentMethod, onConfirmListener = {
openRemoveDialog.value = false
interactor.handleViewAction(UpdatePaymentMethodInteractor.ViewAction.RemovePaymentMethod)
}, onDismissListener = {
openRemoveDialog.value = false
})
}
}

Expand Down Expand Up @@ -241,8 +278,11 @@ private fun PreviewUpdatePaymentMethodUI() {
UpdatePaymentMethodUI(
interactor = DefaultUpdatePaymentMethodInteractor(
isLiveMode = false,
canRemove = true,
displayableSavedPaymentMethod = exampleCard,
card = exampleCard.paymentMethod.card!!,
onRemovePaymentMethod = {},
navigateBack = {},
),
modifier = Modifier
)
Expand All @@ -256,3 +296,4 @@ private const val YEAR_2100 = 2100

internal const val UPDATE_PM_EXPIRY_FIELD_TEST_TAG = "update_payment_method_expiry_date"
internal const val UPDATE_PM_CVC_FIELD_TEST_TAG = "update_payment_method_cvc"
internal const val UPDATE_PM_REMOVE_BUTTON_TEST_TAG = "update_payment_method_remove_button"
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.stripe.android.paymentsheet.ui

import com.google.common.truth.Truth.assertThat
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodFixtures
import com.stripe.android.paymentsheet.DisplayableSavedPaymentMethod
import kotlinx.coroutines.test.runTest
import org.junit.Test

class DefaultUpdatePaymentMethodInteractorTest {

@Test
fun removeViewAction_removesPmAndNavigatesBack() {
val paymentMethod = PaymentMethodFixtures.displayableCard()

var removedPm: PaymentMethod? = null
fun onRemovePaymentMethod(paymentMethod: PaymentMethod) {
removedPm = paymentMethod
}

var navigatedBack = false
fun navigateBack() {
navigatedBack = true
}

runScenario(
canRemove = true,
displayableSavedPaymentMethod = paymentMethod,
onRemovePaymentMethod = ::onRemovePaymentMethod,
navigateBack = ::navigateBack,
) {
interactor.handleViewAction(UpdatePaymentMethodInteractor.ViewAction.RemovePaymentMethod)

assertThat(removedPm).isEqualTo(paymentMethod.paymentMethod)
assertThat(navigatedBack).isTrue()
}
}

private val notImplemented: () -> Nothing = { throw AssertionError("Not implemented") }

private fun runScenario(
canRemove: Boolean = false,
displayableSavedPaymentMethod: DisplayableSavedPaymentMethod = PaymentMethodFixtures.displayableCard(),
onRemovePaymentMethod: (PaymentMethod) -> Unit = { notImplemented() },
navigateBack: () -> Unit = { notImplemented() },
testBlock: suspend TestParams.() -> Unit
) {
val interactor = DefaultUpdatePaymentMethodInteractor(
isLiveMode = false,
canRemove = canRemove,
displayableSavedPaymentMethod = displayableSavedPaymentMethod,
card = displayableSavedPaymentMethod.paymentMethod.card!!,
onRemovePaymentMethod = onRemovePaymentMethod,
navigateBack = navigateBack,
)

TestParams(interactor).apply { runTest { testBlock() } }
}

private data class TestParams(
val interactor: UpdatePaymentMethodInteractor,
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.paymentsheet.ui

import androidx.compose.runtime.Composable
import com.stripe.android.lpmfoundations.paymentmethod.PaymentMethodMetadataFactory
import com.stripe.android.model.PaymentMethodFixtures.toDisplayableSavedPaymentMethod
import com.stripe.android.paymentsheet.viewmodels.FakeBaseSheetViewModel
Expand All @@ -14,18 +15,35 @@ class PaymentSheetScreenUpdatePaymentMethodScreenshotTest {

@Test
fun updatePaymentMethodScreen_forCard() {
paparazziRule.snapshot {
PaymentSheetScreenOnUpdatePaymentMethod(canRemove = false)
}
}

@Test
fun updatePaymentMethodScreen_forCard_withRemoveButton() {
paparazziRule.snapshot {
PaymentSheetScreenOnUpdatePaymentMethod(canRemove = true)
}
}

@Composable
fun PaymentSheetScreenOnUpdatePaymentMethod(
canRemove: Boolean,
) {
val paymentMethod = PaymentMethodFactory.visaCard().toDisplayableSavedPaymentMethod()
val interactor = DefaultUpdatePaymentMethodInteractor(
isLiveMode = true,
displayableSavedPaymentMethod = paymentMethod,
card = paymentMethod.paymentMethod.card!!,
onRemovePaymentMethod = {},
navigateBack = {},
canRemove = canRemove,
)
val screen = com.stripe.android.paymentsheet.navigation.PaymentSheetScreen.UpdatePaymentMethod(interactor)
val metadata = PaymentMethodMetadataFactory.create()
val viewModel = FakeBaseSheetViewModel.create(metadata, screen)

paparazziRule.snapshot {
PaymentSheetScreen(viewModel = viewModel, type = PaymentSheetFlowType.Complete)
}
PaymentSheetScreen(viewModel = viewModel, type = PaymentSheetFlowType.Complete)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import com.google.common.truth.Truth.assertThat
import com.stripe.android.model.CardBrand
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodFixtures
import com.stripe.android.model.PaymentMethodFixtures.toDisplayableSavedPaymentMethod
import com.stripe.android.paymentsheet.DisplayableSavedPaymentMethod
import com.stripe.android.paymentsheet.ViewActionRecorder
import com.stripe.android.ui.core.elements.TEST_TAG_DIALOG_CONFIRM_BUTTON
import com.stripe.android.ui.core.elements.TEST_TAG_SIMPLE_DIALOG
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -129,6 +135,35 @@ class UpdatePaymentMethodUITest {
}
}

@Test
fun canRemoveIsFalse_removeButtonHidden() = runScenario(
canRemove = false,
) {
composeRule.onNodeWithTag(UPDATE_PM_REMOVE_BUTTON_TEST_TAG).assertDoesNotExist()
}

@Test
fun canRemoveIsTrue_removeButtonHidden() = runScenario(
canRemove = true,
) {
composeRule.onNodeWithTag(UPDATE_PM_REMOVE_BUTTON_TEST_TAG).assertExists()
}

@Test
fun clickingRemoveButton_displaysDialog_deletesOnConfirm() = runScenario(canRemove = true) {
composeRule.onNodeWithTag(UPDATE_PM_REMOVE_BUTTON_TEST_TAG).assertExists()
composeRule.onNodeWithTag(UPDATE_PM_REMOVE_BUTTON_TEST_TAG).performClick()

composeRule.onNodeWithTag(TEST_TAG_DIALOG_CONFIRM_BUTTON).assertExists()
composeRule.onNodeWithTag(TEST_TAG_DIALOG_CONFIRM_BUTTON).performClick()

viewActionRecorder.consume(
UpdatePaymentMethodInteractor.ViewAction.RemovePaymentMethod
)
assertThat(viewActionRecorder.viewActions).isEmpty()
composeRule.onNodeWithTag(TEST_TAG_SIMPLE_DIALOG).assertDoesNotExist()
}

private fun assertExpiryDateEquals(text: String) {
composeRule.onNodeWithTag(UPDATE_PM_EXPIRY_FIELD_TEST_TAG).assertTextContains(
text
Expand All @@ -141,20 +176,39 @@ class UpdatePaymentMethodUITest {
)
}

private class FakeUpdatePaymentMethodInteractor(
override val displayableSavedPaymentMethod: DisplayableSavedPaymentMethod,
override val canRemove: Boolean,
val viewActionRecorder: ViewActionRecorder<UpdatePaymentMethodInteractor.ViewAction>,
) : UpdatePaymentMethodInteractor {
override val isLiveMode: Boolean = false
override val card: PaymentMethod.Card = displayableSavedPaymentMethod.paymentMethod.card!!

override fun handleViewAction(viewAction: UpdatePaymentMethodInteractor.ViewAction) {
viewActionRecorder.record(viewAction)
}
}

private fun runScenario(
displayableSavedPaymentMethod: DisplayableSavedPaymentMethod,
testBlock: () -> Unit,
displayableSavedPaymentMethod: DisplayableSavedPaymentMethod = PaymentMethodFixtures.displayableCard(),
canRemove: Boolean = true,
testBlock: Scenario.() -> Unit,
) {
val interactor = DefaultUpdatePaymentMethodInteractor(
isLiveMode = false,
val viewActionRecorder = ViewActionRecorder<UpdatePaymentMethodInteractor.ViewAction>()
val interactor = FakeUpdatePaymentMethodInteractor(
displayableSavedPaymentMethod = displayableSavedPaymentMethod,
card = displayableSavedPaymentMethod.paymentMethod.card!!,
canRemove = canRemove,
viewActionRecorder = viewActionRecorder,
)

composeRule.setContent {
UpdatePaymentMethodUI(interactor = interactor, modifier = Modifier)
}

testBlock()
Scenario(viewActionRecorder).apply(testBlock)
}

private data class Scenario(
val viewActionRecorder: ViewActionRecorder<UpdatePaymentMethodInteractor.ViewAction>,
)
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading