diff --git a/payments-core/detekt-baseline.xml b/payments-core/detekt-baseline.xml index 4f9819afc73..d89c370befd 100644 --- a/payments-core/detekt-baseline.xml +++ b/payments-core/detekt-baseline.xml @@ -34,7 +34,7 @@ LongMethod:CardFormViewTest.kt$CardFormViewTest$@Test fun testCardValidCallback() LongMethod:CardInputWidget.kt$CardInputWidget$private fun initView(attrs: AttributeSet?) LongMethod:CollectBankAccountViewModel.kt$CollectBankAccountViewModel$private suspend fun createFinancialConnectionsSession() - LongMethod:ElementsSessionFixtures.kt$ElementsSessionFixtures$fun createPaymentIntentWithCustomerSession( allowRedisplay: String? = "limited" ): JSONObject + LongMethod:ElementsSessionFixtures.kt$ElementsSessionFixtures$fun createPaymentIntentWithCustomerSession( allowRedisplay: String? = "limited", paymentMethodRemoveFeature: String? = "enabled", paymentMethodRemoveLastFeature: String? = "enabled", ): JSONObject LongMethod:ElementsSessionJsonParserTest.kt$ElementsSessionJsonParserTest$@Test fun `ElementsSession has expected customer session information in the response`() LongMethod:GooglePayJsonFactoryTest.kt$GooglePayJsonFactoryTest$@Test fun testCreatePaymentMethodRequestJson() LongMethod:PaymentAuthConfigTest.kt$PaymentAuthConfigTest$@Test fun testUiCustomizationWrapper() diff --git a/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt b/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt index 943770fa452..ec929159017 100644 --- a/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt +++ b/payments-core/src/main/java/com/stripe/android/model/ElementsSession.kt @@ -90,6 +90,7 @@ data class ElementsSession( data class Enabled( val isPaymentMethodSaveEnabled: Boolean, val isPaymentMethodRemoveEnabled: Boolean, + val canRemoveLastPaymentMethod: Boolean, val allowRedisplayOverride: PaymentMethod.AllowRedisplay? ) : MobilePaymentElement } diff --git a/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt b/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt index 86da90f689e..def7634258b 100644 --- a/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt +++ b/payments-core/src/main/java/com/stripe/android/model/parsers/ElementsSessionJsonParser.kt @@ -243,6 +243,7 @@ internal class ElementsSessionJsonParser( val paymentMethodSaveFeature = paymentSheetFeatures.optString(FIELD_PAYMENT_METHOD_SAVE) val paymentMethodRemoveFeature = paymentSheetFeatures.optString(FIELD_PAYMENT_METHOD_REMOVE) + val paymentMethodRemoveLastFeature = paymentSheetFeatures.optString(FIELD_PAYMENT_METHOD_REMOVE_LAST) val allowRedisplayOverrideValue = paymentSheetFeatures .optString(FIELD_PAYMENT_METHOD_ALLOW_REDISPLAY_OVERRIDE) @@ -253,6 +254,7 @@ internal class ElementsSessionJsonParser( ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = paymentMethodSaveFeature == VALUE_ENABLED, isPaymentMethodRemoveEnabled = paymentMethodRemoveFeature == VALUE_ENABLED, + canRemoveLastPaymentMethod = paymentMethodRemoveLastFeature == VALUE_ENABLED, allowRedisplayOverride = allowRedisplayOverride, ) } else { @@ -349,6 +351,7 @@ internal class ElementsSessionJsonParser( private const val FIELD_PAYMENT_METHOD_REMOVE = "payment_method_remove" private const val FIELD_PAYMENT_METHOD_ALLOW_REDISPLAY_OVERRIDE = "payment_method_save_allow_redisplay_override" + private const val FIELD_PAYMENT_METHOD_REMOVE_LAST = "payment_method_remove_last" private const val VALUE_ENABLED = FIELD_ENABLED const val FIELD_GOOGLE_PAY_PREFERENCE = "google_pay_preference" diff --git a/payments-core/src/test/java/com/stripe/android/model/ElementsSessionFixtures.kt b/payments-core/src/test/java/com/stripe/android/model/ElementsSessionFixtures.kt index a9831c0a6f5..d92b81cf8d5 100644 --- a/payments-core/src/test/java/com/stripe/android/model/ElementsSessionFixtures.kt +++ b/payments-core/src/test/java/com/stripe/android/model/ElementsSessionFixtures.kt @@ -240,7 +240,9 @@ internal object ElementsSessionFixtures { ) fun createPaymentIntentWithCustomerSession( - allowRedisplay: String? = "limited" + allowRedisplay: String? = "limited", + paymentMethodRemoveFeature: String? = "enabled", + paymentMethodRemoveLastFeature: String? = "enabled", ): JSONObject { return JSONObject( """ @@ -322,8 +324,9 @@ internal object ElementsSessionFixtures { "mobile_payment_element": { "enabled": true, "features": { - "payment_method_remove": "enabled", + "payment_method_remove": ${paymentMethodRemoveFeature ?: "enabled"}, "payment_method_save": "disabled", + "payment_method_remove_last": ${paymentMethodRemoveLastFeature ?: "enabled"}, "payment_method_save_allow_redisplay_override": ${allowRedisplay?.let { "\"$it\""} ?: "null"} } }, diff --git a/payments-core/src/test/java/com/stripe/android/model/parsers/ElementsSessionJsonParserTest.kt b/payments-core/src/test/java/com/stripe/android/model/parsers/ElementsSessionJsonParserTest.kt index b1400e11f0a..ff82c92e12a 100644 --- a/payments-core/src/test/java/com/stripe/android/model/parsers/ElementsSessionJsonParserTest.kt +++ b/payments-core/src/test/java/com/stripe/android/model/parsers/ElementsSessionJsonParserTest.kt @@ -541,6 +541,7 @@ class ElementsSessionJsonParserTest { mobilePaymentElement = ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = false, isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethod = true, allowRedisplayOverride = PaymentMethod.AllowRedisplay.LIMITED, ), customerSheet = ElementsSession.Customer.Components.CustomerSheet.Disabled @@ -611,6 +612,54 @@ class ElementsSessionJsonParserTest { ) } + @Test + fun `when 'payment_method_remove' is 'enabled' for MPE, 'canRemovePaymentMethods' should be true`() { + mobilePaymentElementPermissionsTest( + paymentMethodRemoveFeatureValue = "enabled", + canRemovePaymentMethods = true, + ) + } + + @Test + fun `when 'payment_method_remove' is 'disabled' for MPE, 'canRemovePaymentMethods' should be false`() { + mobilePaymentElementPermissionsTest( + paymentMethodRemoveFeatureValue = "disabled", + canRemovePaymentMethods = false, + ) + } + + @Test + fun `when 'payment_method_remove' is unknown value for MPE, 'canRemovePaymentMethods' should be false`() { + mobilePaymentElementPermissionsTest( + paymentMethodRemoveFeatureValue = "something", + canRemovePaymentMethods = false, + ) + } + + @Test + fun `when 'payment_method_remove_last' is 'enabled' for MPE, 'canRemoveLastPaymentMethod' should be true`() { + mobilePaymentElementPermissionsTest( + paymentMethodRemoveLastFeatureValue = "enabled", + canRemoveLastPaymentMethod = true, + ) + } + + @Test + fun `when 'payment_method_remove_last' is 'disabled' for MPE, 'canRemoveLastPaymentMethod' should be false`() { + mobilePaymentElementPermissionsTest( + paymentMethodRemoveLastFeatureValue = "disabled", + canRemoveLastPaymentMethod = false, + ) + } + + @Test + fun `when 'payment_method_remove_last' is unknown value for MPE, 'canRemoveLastPaymentMethod' should be false`() { + mobilePaymentElementPermissionsTest( + paymentMethodRemoveLastFeatureValue = "something", + canRemoveLastPaymentMethod = false, + ) + } + @Test fun `ElementsSession has expected customer session information with customer sheet component in the response`() { val parser = ElementsSessionJsonParser( @@ -636,7 +685,7 @@ class ElementsSessionJsonParserTest { components = ElementsSession.Customer.Components( mobilePaymentElement = ElementsSession.Customer.Components.MobilePaymentElement.Disabled, customerSheet = ElementsSession.Customer.Components.CustomerSheet.Enabled( - isPaymentMethodRemoveEnabled = true + isPaymentMethodRemoveEnabled = true, ), ) ), @@ -776,4 +825,38 @@ class ElementsSessionJsonParserTest { assertThat(enabledComponent?.allowRedisplayOverride).isEqualTo(allowRedisplay) } + + private fun mobilePaymentElementPermissionsTest( + paymentMethodRemoveFeatureValue: String? = "enabled", + paymentMethodRemoveLastFeatureValue: String? = "enabled", + canRemovePaymentMethods: Boolean = true, + canRemoveLastPaymentMethod: Boolean = true, + ) { + val parser = ElementsSessionJsonParser( + ElementsSessionParams.PaymentIntentType( + clientSecret = "secret", + customerSessionClientSecret = "customer_session_client_secret", + externalPaymentMethods = emptyList(), + ), + isLiveMode = false, + ) + + val intent = createPaymentIntentWithCustomerSession( + paymentMethodRemoveFeature = paymentMethodRemoveFeatureValue, + paymentMethodRemoveLastFeature = paymentMethodRemoveLastFeatureValue, + ) + + val elementsSession = parser.parse(intent) + + val mobilePaymentElementComponent = elementsSession?.customer?.session?.components?.mobilePaymentElement + + assertThat(mobilePaymentElementComponent) + .isInstanceOf(ElementsSession.Customer.Components.MobilePaymentElement.Enabled::class.java) + + val enabledComponent = mobilePaymentElementComponent as? + ElementsSession.Customer.Components.MobilePaymentElement.Enabled + + assertThat(enabledComponent?.isPaymentMethodRemoveEnabled).isEqualTo(canRemovePaymentMethods) + assertThat(enabledComponent?.canRemoveLastPaymentMethod).isEqualTo(canRemoveLastPaymentMethod) + } } diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/model/PlaygroundCheckoutModel.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/model/PlaygroundCheckoutModel.kt index 3c539c9f9ed..fc9f3d7d468 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/model/PlaygroundCheckoutModel.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/model/PlaygroundCheckoutModel.kt @@ -38,6 +38,8 @@ class CheckoutRequest private constructor( val paymentMethodSaveFeature: FeatureState?, @SerialName("customer_session_payment_method_remove") val paymentMethodRemoveFeature: FeatureState?, + @SerialName("customer_session_payment_method_remove_last") + val paymentMethodRemoveLastFeature: FeatureState?, @SerialName("customer_session_payment_method_redisplay") val paymentMethodRedisplayFeature: FeatureState?, @SerialName("customer_session_payment_method_allow_redisplay_filters") @@ -69,6 +71,7 @@ class CheckoutRequest private constructor( private var requireCvcRecollection: Boolean? = null private var paymentMethodSaveFeature: FeatureState = FeatureState.Enabled private var paymentMethodRemoveFeature: FeatureState = FeatureState.Enabled + private var paymentMethodRemoveLastFeature: FeatureState = FeatureState.Enabled private var paymentMethodRedisplayFeature: FeatureState = FeatureState.Enabled private var paymentMethodRedisplayFilters: List = listOf( AllowRedisplayFilter.Unspecified, @@ -129,6 +132,10 @@ class CheckoutRequest private constructor( this.paymentMethodRemoveFeature = state } + fun paymentMethodRemoveLastFeature(state: FeatureState) { + this.paymentMethodRemoveLastFeature = state + } + fun paymentMethodRedisplayFeature(state: FeatureState) { this.paymentMethodRedisplayFeature = state } @@ -162,6 +169,7 @@ class CheckoutRequest private constructor( customerSessionComponentName = "mobile_payment_element", paymentMethodSaveFeature = paymentMethodSaveFeature, paymentMethodRemoveFeature = paymentMethodRemoveFeature, + paymentMethodRemoveLastFeature = paymentMethodRemoveLastFeature, paymentMethodRedisplayFeature = paymentMethodRedisplayFeature, paymentMethodRedisplayFilters = paymentMethodRedisplayFilters, paymentMethodOverrideRedisplay = paymentMethodOverrideRedisplay, diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/CustomerSessionRemoveLastSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/CustomerSessionRemoveLastSettingsDefinition.kt new file mode 100644 index 00000000000..d88f1921c06 --- /dev/null +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/CustomerSessionRemoveLastSettingsDefinition.kt @@ -0,0 +1,25 @@ +package com.stripe.android.paymentsheet.example.playground.settings + +import com.stripe.android.paymentsheet.example.playground.model.CheckoutRequest +import com.stripe.android.paymentsheet.example.playground.model.FeatureState + +internal object CustomerSessionRemoveLastSettingsDefinition : BooleanSettingsDefinition( + defaultValue = true, + displayName = "Customer Session Remove Last Payment Method", + key = "customer_session_payment_method_remove" +) { + override fun createOptions( + configurationData: PlaygroundConfigurationData + ) = listOf( + PlaygroundSettingDefinition.Displayable.Option("Enabled", true), + PlaygroundSettingDefinition.Displayable.Option("Disabled", false), + ) + + override fun configure(value: Boolean, checkoutRequestBuilder: CheckoutRequest.Builder) { + if (value) { + checkoutRequestBuilder.paymentMethodRemoveLastFeature(FeatureState.Enabled) + } else { + checkoutRequestBuilder.paymentMethodRemoveLastFeature(FeatureState.Disabled) + } + } +} diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt index df3b9c9749a..f398642e150 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt @@ -418,6 +418,7 @@ internal class PlaygroundSettings private constructor( CustomerSessionSettingsDefinition, CustomerSessionSaveSettingsDefinition, CustomerSessionRemoveSettingsDefinition, + CustomerSessionRemoveLastSettingsDefinition, CustomerSessionRedisplaySettingsDefinition, CustomerSessionRedisplayFiltersSettingsDefinition, CustomerSessionOverrideRedisplaySettingsDefinition, diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt index a8576417109..1d93ecb5d45 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutator.kt @@ -34,7 +34,6 @@ internal class SavedPaymentMethodMutator( private val coroutineScope: CoroutineScope, private val workContext: CoroutineContext, private val customerRepository: CustomerRepository, - private val allowsRemovalOfLastSavedPaymentMethod: Boolean, private val selection: StateFlow, val providePaymentMethodName: (PaymentMethodCode?) -> ResolvableString, private val clearSelection: () -> Unit, @@ -62,10 +61,11 @@ internal class SavedPaymentMethodMutator( val canRemove: StateFlow = customerStateHolder.customer.mapAsStateFlow { customerState -> customerState?.run { val hasRemovePermissions = customerState.permissions.canRemovePaymentMethods + val hasRemoveLastPaymentMethodPermissions = customerState.permissions.canRemoveLastPaymentMethod when (paymentMethods.size) { 0 -> false - 1 -> allowsRemovalOfLastSavedPaymentMethod && hasRemovePermissions + 1 -> hasRemoveLastPaymentMethodPermissions && hasRemovePermissions else -> hasRemovePermissions } } ?: false @@ -390,7 +390,6 @@ internal class SavedPaymentMethodMutator( coroutineScope = viewModel.viewModelScope, workContext = viewModel.workContext, customerRepository = viewModel.customerRepository, - allowsRemovalOfLastSavedPaymentMethod = viewModel.config.allowsRemovalOfLastSavedPaymentMethod, selection = viewModel.selection, providePaymentMethodName = { code -> code?.let { diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/CustomerState.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/CustomerState.kt index 1ea59674b87..4d5b29ef296 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/CustomerState.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/CustomerState.kt @@ -1,6 +1,7 @@ package com.stripe.android.paymentsheet.state import android.os.Parcelable +import com.stripe.android.common.model.CommonConfiguration import com.stripe.android.model.ElementsSession import com.stripe.android.model.PaymentMethod import com.stripe.android.paymentsheet.PaymentSheet @@ -16,6 +17,7 @@ internal data class CustomerState( @Parcelize data class Permissions( val canRemovePaymentMethods: Boolean, + val canRemoveLastPaymentMethod: Boolean, val canRemoveDuplicates: Boolean, ) : Parcelable @@ -28,17 +30,26 @@ internal data class CustomerState( * @return [CustomerState] instance using [ElementsSession.Customer] data */ internal fun createForCustomerSession( + configuration: CommonConfiguration, customer: ElementsSession.Customer, supportedSavedPaymentMethodTypes: List, ): CustomerState { - val canRemovePaymentMethods = when ( - val mobilePaymentElementComponent = customer.session.components.mobilePaymentElement - ) { - is ElementsSession.Customer.Components.MobilePaymentElement.Enabled -> + val mobilePaymentElementComponent = customer.session.components.mobilePaymentElement + + val canRemovePaymentMethods = when (mobilePaymentElementComponent) { + is ElementsSession.Customer.Components.MobilePaymentElement.Enabled -> { mobilePaymentElementComponent.isPaymentMethodRemoveEnabled + } is ElementsSession.Customer.Components.MobilePaymentElement.Disabled -> false } + val canRemoveLastPaymentMethod = when { + !configuration.allowsRemovalOfLastSavedPaymentMethod -> false + mobilePaymentElementComponent is ElementsSession.Customer.Components.MobilePaymentElement.Enabled -> + mobilePaymentElementComponent.canRemoveLastPaymentMethod + else -> false + } + return CustomerState( id = customer.session.customerId, ephemeralKeySecret = customer.session.apiKey, @@ -47,6 +58,7 @@ internal data class CustomerState( }, permissions = Permissions( canRemovePaymentMethods = canRemovePaymentMethods, + canRemoveLastPaymentMethod = canRemoveLastPaymentMethod, // Should always remove duplicates when using `customer_session` canRemoveDuplicates = true, ) @@ -63,6 +75,7 @@ internal data class CustomerState( * @return [CustomerState] instance with legacy ephemeral key secrets */ internal fun createForLegacyEphemeralKey( + configuration: CommonConfiguration, customerId: String, accessType: PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey, paymentMethods: List, @@ -77,6 +90,13 @@ internal data class CustomerState( * always be set to true. */ canRemovePaymentMethods = true, + /* + * Un-scoped legacy ephemeral keys normally have full permissions to remove the last payment + * method, however we do have client-side configuration option to configure this ability. This + * should eventually be removed in favor of the server-side option available with customer + * sessions. + */ + canRemoveLastPaymentMethod = configuration.allowsRemovalOfLastSavedPaymentMethod, /* * Removing duplicates is not applicable here since we don't filter out duplicates for for * un-scoped ephemeral keys. diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt index 15597ff3fbd..4ff4cda8b52 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentElementLoader.kt @@ -189,6 +189,7 @@ internal class DefaultPaymentElementLoader @Inject constructor( customerInfo = customerInfo, metadata = metadata.await(), savedSelection = savedSelection, + configuration = configuration, cardBrandFilter = PaymentSheetCardBrandFilter(configuration.cardBrandAcceptance) ) } @@ -313,6 +314,7 @@ internal class DefaultPaymentElementLoader @Inject constructor( } private suspend fun createCustomerState( + configuration: CommonConfiguration, customerInfo: CustomerInfo?, metadata: PaymentMethodMetadata, savedSelection: Deferred, @@ -322,6 +324,7 @@ internal class DefaultPaymentElementLoader @Inject constructor( is CustomerInfo.CustomerSession -> { CustomerState.createForCustomerSession( customer = customerInfo.elementsSessionCustomer, + configuration = configuration, supportedSavedPaymentMethodTypes = metadata.supportedSavedPaymentMethodTypes() ) } @@ -329,6 +332,7 @@ internal class DefaultPaymentElementLoader @Inject constructor( CustomerState.createForLegacyEphemeralKey( customerId = customerInfo.id, accessType = customerInfo.accessType, + configuration = configuration, paymentMethods = retrieveCustomerPaymentMethods( metadata = metadata, customerConfig = customerInfo.customerConfig, diff --git a/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt b/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt index 32b4490135f..ad813b2c75a 100644 --- a/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/lpmfoundations/paymentmethod/PaymentMethodMetadataTest.kt @@ -867,6 +867,7 @@ internal class PaymentMethodMetadataTest { mobilePaymentElementComponent = ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = true, isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethod = true, allowRedisplayOverride = null, ) ) @@ -880,6 +881,7 @@ internal class PaymentMethodMetadataTest { mobilePaymentElementComponent = ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = false, isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethod = true, allowRedisplayOverride = null, ), ) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt index f3ee2fdb662..46524fc5940 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerSessionPaymentSheetActivityTest.kt @@ -58,7 +58,8 @@ internal class CustomerSessionPaymentSheetActivityTest { PaymentMethodFactory.card(last4 = "5544", addCbcNetworks = true), ), isPaymentMethodRemoveEnabled = true, - allowsRemovalOfLastSavedPaymentMethod = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = true, ) { composeTestRule.onEditButton().performClick() @@ -67,19 +68,46 @@ internal class CustomerSessionPaymentSheetActivityTest { } @Test - fun `When single PM with remove permissions and can remove last PM, should be enabled when editing`() = + fun `When single PM with remove permissions and can remove last from sources, should be enabled when editing`() = runTest( cards = listOf( PaymentMethodFactory.card(last4 = "4242"), ), isPaymentMethodRemoveEnabled = true, - allowsRemovalOfLastSavedPaymentMethod = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = true, ) { composeTestRule.onEditButton().performClick() composeTestRule.onSavedPaymentMethod(last4 = "4242").assertIsEnabled() } + @Test + fun `When single PM with remove permissions and cannot remove last PM from server, edit should not be shown`() = + runTest( + cards = listOf( + PaymentMethodFactory.card(last4 = "4242"), + ), + isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = false, + ) { + composeTestRule.onEditButton().assertDoesNotExist() + } + + @Test + fun `When single PM with remove permissions & cannot remove last PM from config, edit should not be shown`() = + runTest( + cards = listOf( + PaymentMethodFactory.card(last4 = "4242"), + ), + isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethodConfig = false, + canRemoveLastPaymentMethodServer = true, + ) { + composeTestRule.onEditButton().assertDoesNotExist() + } + @Test fun `When single PM with remove permissions but cannot remove last PM, edit button should not be displayed`() = runTest( @@ -87,7 +115,8 @@ internal class CustomerSessionPaymentSheetActivityTest { PaymentMethodFactory.card(last4 = "4242"), ), isPaymentMethodRemoveEnabled = true, - allowsRemovalOfLastSavedPaymentMethod = false, + canRemoveLastPaymentMethodConfig = false, + canRemoveLastPaymentMethodServer = false, ) { composeTestRule.onEditButton().assertDoesNotExist() } @@ -100,7 +129,8 @@ internal class CustomerSessionPaymentSheetActivityTest { PaymentMethodFactory.card(last4 = "5544"), ), isPaymentMethodRemoveEnabled = false, - allowsRemovalOfLastSavedPaymentMethod = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = false, ) { composeTestRule.onEditButton().assertDoesNotExist() } @@ -113,7 +143,8 @@ internal class CustomerSessionPaymentSheetActivityTest { PaymentMethodFactory.card(last4 = "5544"), ), isPaymentMethodRemoveEnabled = false, - allowsRemovalOfLastSavedPaymentMethod = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = false, ) { composeTestRule.onEditButton().performClick() @@ -133,7 +164,8 @@ internal class CustomerSessionPaymentSheetActivityTest { PaymentMethodFactory.card(last4 = "5544"), ), isPaymentMethodRemoveEnabled = true, - allowsRemovalOfLastSavedPaymentMethod = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = false, ) { composeTestRule.onEditButton().performClick() @@ -150,13 +182,58 @@ internal class CustomerSessionPaymentSheetActivityTest { } @Test - fun `When single CBC card, has remove permissions, and can remove last PM, should be able to remove and edit`() = + fun `When single CBC card, has remove permissions, and cannot remove last PM from server, can only edit`() = runTest( cards = listOf( PaymentMethodFactory.card(last4 = "4242", addCbcNetworks = true), ), isPaymentMethodRemoveEnabled = true, - allowsRemovalOfLastSavedPaymentMethod = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = false, + ) { + composeTestRule.onEditButton().performClick() + + val cbcCard = composeTestRule.onSavedPaymentMethod(last4 = "4242") + + cbcCard.assertIsEnabled() + cbcCard.assertHasModifyBadge() + + composeTestRule.onModifyBadgeFor(last4 = "4242").performClick() + + composeTestRule.onEditScreenRemoveButton().assertDoesNotExist() + } + + @Test + fun `When single CBC card, has remove permissions, and cannot remove last PM from config, can only edit`() = + runTest( + cards = listOf( + PaymentMethodFactory.card(last4 = "4242", addCbcNetworks = true), + ), + isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethodConfig = false, + canRemoveLastPaymentMethodServer = true, + ) { + composeTestRule.onEditButton().performClick() + + val cbcCard = composeTestRule.onSavedPaymentMethod(last4 = "4242") + + cbcCard.assertIsEnabled() + cbcCard.assertHasModifyBadge() + + composeTestRule.onModifyBadgeFor(last4 = "4242").performClick() + + composeTestRule.onEditScreenRemoveButton().assertDoesNotExist() + } + + @Test + fun `When single CBC card, has remove permissions, and can remove last PM from all sources, can remove and edit`() = + runTest( + cards = listOf( + PaymentMethodFactory.card(last4 = "4242", addCbcNetworks = true), + ), + isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = true, ) { composeTestRule.onEditButton().performClick() @@ -177,7 +254,8 @@ internal class CustomerSessionPaymentSheetActivityTest { PaymentMethodFactory.card(last4 = "4242", addCbcNetworks = true), ), isPaymentMethodRemoveEnabled = false, - allowsRemovalOfLastSavedPaymentMethod = true, + canRemoveLastPaymentMethodConfig = true, + canRemoveLastPaymentMethodServer = false, ) { composeTestRule.onEditButton().performClick() @@ -198,7 +276,8 @@ internal class CustomerSessionPaymentSheetActivityTest { PaymentMethodFactory.card(last4 = "4242", addCbcNetworks = true), ), isPaymentMethodRemoveEnabled = true, - allowsRemovalOfLastSavedPaymentMethod = false, + canRemoveLastPaymentMethodConfig = false, + canRemoveLastPaymentMethodServer = false, ) { composeTestRule.onEditButton().performClick() @@ -215,7 +294,8 @@ internal class CustomerSessionPaymentSheetActivityTest { private fun runTest( cards: List, isPaymentMethodRemoveEnabled: Boolean, - allowsRemovalOfLastSavedPaymentMethod: Boolean, + canRemoveLastPaymentMethodConfig: Boolean, + canRemoveLastPaymentMethodServer: Boolean, test: (PaymentSheetActivity) -> Unit, ) { networkRule.enqueue( @@ -223,7 +303,13 @@ internal class CustomerSessionPaymentSheetActivityTest { RequestMatchers.method("GET"), RequestMatchers.path("/v1/elements/sessions"), ) { response -> - response.setBody(createElementsSessionResponse(cards, isPaymentMethodRemoveEnabled)) + response.setBody( + createElementsSessionResponse( + cards = cards, + isPaymentMethodRemoveEnabled = isPaymentMethodRemoveEnabled, + canRemoveLastPaymentMethod = canRemoveLastPaymentMethodServer, + ) + ) } val countDownLatch = CountDownLatch(1) @@ -241,7 +327,7 @@ internal class CustomerSessionPaymentSheetActivityTest { id = "cus_1", clientSecret = "cuss_1", ), - allowsRemovalOfLastSavedPaymentMethod = allowsRemovalOfLastSavedPaymentMethod, + allowsRemovalOfLastSavedPaymentMethod = canRemoveLastPaymentMethodConfig, preferredNetworks = listOf(CardBrand.CartesBancaires, CardBrand.Visa), paymentMethodLayout = PaymentSheet.PaymentMethodLayout.Horizontal, ), @@ -302,6 +388,7 @@ internal class CustomerSessionPaymentSheetActivityTest { fun createElementsSessionResponse( cards: List, isPaymentMethodRemoveEnabled: Boolean, + canRemoveLastPaymentMethod: Boolean, ): String { val cardsArray = JSONArray() @@ -317,6 +404,12 @@ internal class CustomerSessionPaymentSheetActivityTest { "disabled" } + val canRemoveLastPaymentMethodStringified = if (canRemoveLastPaymentMethod) { + "enabled" + } else { + "disabled" + } + return """ { "business_name": "Mobile Example Account", @@ -347,6 +440,7 @@ internal class CustomerSessionPaymentSheetActivityTest { "features": { "payment_method_save": "enabled", "payment_method_remove": "$isPaymentMethodRemoveStringified", + "payment_method_remove_last": "$canRemoveLastPaymentMethodStringified", "payment_method_save_allow_redisplay_override": null } }, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerStateHolderTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerStateHolderTest.kt index 04c8856cc7b..d2d1e685e9f 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerStateHolderTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/CustomerStateHolderTest.kt @@ -3,6 +3,7 @@ package com.stripe.android.paymentsheet import androidx.lifecycle.SavedStateHandle import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import com.stripe.android.common.model.asCommonConfiguration import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.paymentsheet.model.PaymentSelection import com.stripe.android.paymentsheet.state.CustomerState @@ -31,6 +32,7 @@ internal class CustomerStateHolderTest { val savedStateHandle = SavedStateHandle() val customerState = CustomerState.createForLegacyEphemeralKey( customerId = "cus_123", + configuration = PaymentSheetFixtures.CONFIG_CUSTOMER.asCommonConfiguration(), accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), paymentMethods = emptyList() ) @@ -50,6 +52,7 @@ internal class CustomerStateHolderTest { customerStateHolder.setCustomerState( CustomerState.createForLegacyEphemeralKey( customerId = "cus_123", + configuration = PaymentSheetFixtures.CONFIG_CUSTOMER.asCommonConfiguration(), accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), paymentMethods = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD) ) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetFixtures.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetFixtures.kt index 3b187822d06..701622333c3 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetFixtures.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetFixtures.kt @@ -97,6 +97,7 @@ internal object PaymentSheetFixtures { paymentMethods = listOf(), permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = false, ) ) @@ -159,6 +160,7 @@ internal object PaymentSheetFixtures { paymentMethods = paymentMethods, permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = false, ) ), diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt index 89b1507c6df..d5df7baa324 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/PaymentSheetViewModelTest.kt @@ -334,6 +334,7 @@ internal class PaymentSheetViewModelTest { paymentMethods = paymentMethods, permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = false, ), ), diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt index 23224ac0d47..b093a60e9b0 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/SavedPaymentMethodMutatorTest.kt @@ -30,16 +30,14 @@ import org.mockito.Mockito.mock class SavedPaymentMethodMutatorTest { @Test - fun `canRemove is correct when no payment methods for customer`() = runScenario( - allowsRemovalOfLastSavedPaymentMethod = true, - ) { + fun `canRemove is correct when no payment methods for customer`() = runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, paymentMethods = listOf() ) ) @@ -50,19 +48,15 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canRemove is correct when one payment method & allowsRemovalOfLastSavedPaymentMethod is true`() = - runScenario( - allowsRemovalOfLastSavedPaymentMethod = true, - ) { + fun `canRemove is correct when one payment method & can remove last payment method`() = + runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet - .CustomerAccessType - .LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, paymentMethods = PaymentMethodFactory.cards(1), ) ) @@ -74,19 +68,15 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canRemove is correct when one payment method & allowsRemovalOfLastSavedPaymentMethod is false`() = - runScenario( - allowsRemovalOfLastSavedPaymentMethod = false, - ) { + fun `canRemove is correct when one payment method & cannot remove last payment method`() = + runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet - .CustomerAccessType - .LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, paymentMethods = PaymentMethodFactory.cards(1), ) ) @@ -97,19 +87,15 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canRemove is correct when multiple payment methods & allowsRemovalOfLastSavedPaymentMethod is true`() = - runScenario( - allowsRemovalOfLastSavedPaymentMethod = true, - ) { + fun `canRemove is correct when multiple payment methods & can remove last payment method`() = + runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet - .CustomerAccessType - .LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, paymentMethods = PaymentMethodFactory.cards(2), ) ) @@ -119,19 +105,15 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canRemove is correct when multiple payment methods & allowsRemovalOfLastSavedPaymentMethod is false`() = - runScenario( - allowsRemovalOfLastSavedPaymentMethod = false, - ) { + fun `canRemove is correct when multiple payment methods & cannot remove last payment method`() = + runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet - .CustomerAccessType - .LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, paymentMethods = PaymentMethodFactory.cards(2), ) ) @@ -141,10 +123,8 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canRemove is correct when has remove permissions & allowsRemovalOfLastSavedPaymentMethod is true`() = - runScenario( - allowsRemovalOfLastSavedPaymentMethod = true, - ) { + fun `canRemove is correct when has remove permissions & can remove last payment method`() = + runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() @@ -152,6 +132,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = PaymentMethodFactory.cards(1), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -162,10 +143,8 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canRemove is correct when has remove permissions & allowsRemovalOfLastSavedPaymentMethod is false`() = - runScenario( - allowsRemovalOfLastSavedPaymentMethod = false, - ) { + fun `canRemove is correct when has remove permissions & canRemoveLastPaymentMethod is false`() = + runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() @@ -173,6 +152,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = PaymentMethodFactory.cards(1), isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, ) ) @@ -181,10 +161,8 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canRemove is correct when does not remove permissions & allowsRemovalOfLastSavedPaymentMethod is true`() = - runScenario( - allowsRemovalOfLastSavedPaymentMethod = true, - ) { + fun `canRemove is correct when does not remove permissions & canRemoveLastPaymentMethod is true`() = + runScenario { savedPaymentMethodMutator.canRemove.test { assertThat(awaitItem()).isFalse() @@ -192,6 +170,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = PaymentMethodFactory.cards(1), isRemoveEnabled = false, + canRemoveLastPaymentMethod = true, ) ) @@ -200,18 +179,14 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canEdit is correct when no payment methods`() = runScenario( - allowsRemovalOfLastSavedPaymentMethod = true, - ) { + fun `canEdit is correct when no payment methods`() = runScenario { savedPaymentMethodMutator.canEdit.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet - .CustomerAccessType - .LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, paymentMethods = listOf() ) ) @@ -222,16 +197,14 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canEdit is correct when allowsRemovalOfLastSavedPaymentMethod is true`() = runScenario( - allowsRemovalOfLastSavedPaymentMethod = true, - ) { + fun `canEdit is correct when user has permissions to remove last PM`() = runScenario { savedPaymentMethodMutator.canEdit.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, paymentMethods = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD) ) ) @@ -240,16 +213,14 @@ class SavedPaymentMethodMutatorTest { } @Test - fun `canEdit is correct when allowsRemovalOfLastSavedPaymentMethod is false`() = runScenario( - allowsRemovalOfLastSavedPaymentMethod = false, - ) { + fun `canEdit is correct when when user does not have permissions to remove last PM`() = runScenario { savedPaymentMethodMutator.canEdit.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, paymentMethods = listOf( PaymentMethodFixtures.CARD_PAYMENT_METHOD, PaymentMethodFixtures.CARD_WITH_NETWORKS_PAYMENT_METHOD @@ -259,9 +230,9 @@ class SavedPaymentMethodMutatorTest { assertThat(awaitItem()).isTrue() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, paymentMethods = listOf( PaymentMethodFixtures.CARD_WITH_NETWORKS_PAYMENT_METHOD, ) @@ -273,16 +244,15 @@ class SavedPaymentMethodMutatorTest { @Test fun `canEdit is correct CBC is enabled`() = runScenario( - allowsRemovalOfLastSavedPaymentMethod = false, isCbcEligible = { true } ) { savedPaymentMethodMutator.canEdit.test { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, paymentMethods = listOf( PaymentMethodFixtures.CARD_WITH_NETWORKS_PAYMENT_METHOD, ) @@ -294,12 +264,12 @@ class SavedPaymentMethodMutatorTest { assertThat(awaitItem()).isFalse() customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, paymentMethods = listOf( PaymentMethodFixtures.CARD_WITH_NETWORKS_PAYMENT_METHOD - ) + ), ) ) assertThat(awaitItem()).isTrue() @@ -319,12 +289,12 @@ class SavedPaymentMethodMutatorTest { runScenario(customerRepository = customerRepository) { customerStateHolder.setCustomerState( - CustomerState.createForLegacyEphemeralKey( - customerId = "cus_123", - accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey("ek_123"), + createCustomerState( + isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, paymentMethods = listOf( PaymentMethodFixtures.CARD_PAYMENT_METHOD - ) + ), ) ) @@ -383,10 +353,51 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = cards, isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, + ) + ) + + savedPaymentMethodMutator.modifyPaymentMethod(cards[0]) + modifyPaymentMethodTurbine.awaitItem().apply { + assertThat(paymentMethod).isEqualTo(cards[0]) + assertThat(canRemove).isTrue() + } + } + + @Test + fun `modifyPaymentMethod should be called correctly when 1 PM & cannot remove last PM`() = runScenario { + val cards = PaymentMethodFixtures.createCards(1) + + customerStateHolder.setCustomerState( + createCustomerState( + paymentMethods = cards, + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, + ) + ) + + savedPaymentMethodMutator.modifyPaymentMethod(cards[0]) + + modifyPaymentMethodTurbine.awaitItem().apply { + assertThat(paymentMethod).isEqualTo(cards[0]) + assertThat(canRemove).isFalse() + } + } + + @Test + fun `modifyPaymentMethod be called correctly when multiple PMs & cannot remove last PM`() = runScenario { + val cards = PaymentMethodFixtures.createCards(2) + + customerStateHolder.setCustomerState( + createCustomerState( + paymentMethods = cards, + isRemoveEnabled = true, + canRemoveLastPaymentMethod = false, ) ) savedPaymentMethodMutator.modifyPaymentMethod(cards[0]) + modifyPaymentMethodTurbine.awaitItem().apply { assertThat(paymentMethod).isEqualTo(cards[0]) assertThat(canRemove).isTrue() @@ -450,6 +461,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -484,6 +496,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -519,6 +532,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -554,6 +568,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -589,6 +604,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -623,6 +639,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -658,6 +675,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -693,6 +711,7 @@ class SavedPaymentMethodMutatorTest { createCustomerState( paymentMethods = listOf(displayableSavedPaymentMethod.paymentMethod), isRemoveEnabled = true, + canRemoveLastPaymentMethod = true, ) ) @@ -714,6 +733,7 @@ class SavedPaymentMethodMutatorTest { private fun createCustomerState( paymentMethods: List, isRemoveEnabled: Boolean, + canRemoveLastPaymentMethod: Boolean, ): CustomerState { return CustomerState( id = "cus_1", @@ -722,6 +742,7 @@ class SavedPaymentMethodMutatorTest { permissions = CustomerState.Permissions( canRemovePaymentMethods = isRemoveEnabled, canRemoveDuplicates = true, + canRemoveLastPaymentMethod = canRemoveLastPaymentMethod, ) ) } @@ -737,6 +758,7 @@ class SavedPaymentMethodMutatorTest { paymentMethods = listOf(), permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = shouldRemoveDuplicates, ) ) @@ -763,7 +785,6 @@ class SavedPaymentMethodMutatorTest { private fun runScenario( customerRepository: CustomerRepository = FakeCustomerRepository(), - allowsRemovalOfLastSavedPaymentMethod: Boolean = true, isCbcEligible: () -> Boolean = { false }, block: suspend Scenario.() -> Unit ) { @@ -786,7 +807,6 @@ class SavedPaymentMethodMutatorTest { coroutineScope = CoroutineScope(UnconfinedTestDispatcher()), workContext = coroutineContext, customerRepository = customerRepository, - allowsRemovalOfLastSavedPaymentMethod = allowsRemovalOfLastSavedPaymentMethod, selection = selection, providePaymentMethodName = { it?.resolvableString.orEmpty() }, customerStateHolder = customerStateHolder, diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/CustomerStateTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/CustomerStateTest.kt index f9de305c826..58d1abd0603 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/CustomerStateTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/CustomerStateTest.kt @@ -1,10 +1,13 @@ package com.stripe.android.paymentsheet.state import com.google.common.truth.Truth.assertThat +import com.stripe.android.common.model.CommonConfiguration +import com.stripe.android.common.model.asCommonConfiguration import com.stripe.android.model.ElementsSession import com.stripe.android.model.PaymentMethod import com.stripe.android.model.PaymentMethodFixtures import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.PaymentSheetFixtures import com.stripe.android.testing.PaymentMethodFactory import kotlinx.coroutines.test.runTest import org.junit.Test @@ -22,6 +25,7 @@ class CustomerStateTest { val customerState = CustomerState.createForCustomerSession( customer = customer, + configuration = createConfiguration(), supportedSavedPaymentMethodTypes = listOf(PaymentMethod.Type.Card) ) @@ -32,6 +36,7 @@ class CustomerStateTest { paymentMethods = paymentMethods, permissions = CustomerState.Permissions( canRemovePaymentMethods = false, + canRemoveLastPaymentMethod = false, // Always true for `customer_session` canRemoveDuplicates = true, ), @@ -49,12 +54,14 @@ class CustomerStateTest { mobilePaymentElementComponent = ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = false, isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethod = true, allowRedisplayOverride = null, ), ) val customerState = CustomerState.createForCustomerSession( customer = customer, + configuration = createConfiguration(), supportedSavedPaymentMethodTypes = listOf(PaymentMethod.Type.Card) ) @@ -65,6 +72,7 @@ class CustomerStateTest { paymentMethods = paymentMethods, permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, // Always true for `customer_session` canRemoveDuplicates = true, ), @@ -82,12 +90,14 @@ class CustomerStateTest { mobilePaymentElementComponent = ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = false, isPaymentMethodRemoveEnabled = false, + canRemoveLastPaymentMethod = false, allowRedisplayOverride = null, ), ) val customerState = CustomerState.createForCustomerSession( customer = customer, + configuration = createConfiguration(), supportedSavedPaymentMethodTypes = listOf(PaymentMethod.Type.Card) ) @@ -98,6 +108,7 @@ class CustomerStateTest { paymentMethods = paymentMethods, permissions = CustomerState.Permissions( canRemovePaymentMethods = false, + canRemoveLastPaymentMethod = false, // Always true for `customer_session` canRemoveDuplicates = true, ), @@ -113,6 +124,7 @@ class CustomerStateTest { accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey( ephemeralKeySecret = "ek_1", ), + configuration = createConfiguration(), paymentMethods = paymentMethods, ) @@ -124,6 +136,8 @@ class CustomerStateTest { permissions = CustomerState.Permissions( // Always true for legacy ephemeral keys since un-scoped canRemovePaymentMethods = true, + // Always true unless configured client-side + canRemoveLastPaymentMethod = true, // Always 'false' for legacy ephemeral keys canRemoveDuplicates = false, ), @@ -145,22 +159,134 @@ class CustomerStateTest { mobilePaymentElementComponent = ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = false, isPaymentMethodRemoveEnabled = false, + canRemoveLastPaymentMethod = false, allowRedisplayOverride = null, ), ) val customerState = CustomerState.createForCustomerSession( customer = customer, + configuration = createConfiguration(), supportedSavedPaymentMethodTypes = listOf(PaymentMethod.Type.Card) ) assertThat(customerState.paymentMethods).containsExactlyElementsIn(cards) } + @Test + fun `Should set 'canRemoveLastPaymentMethod' to true if config value is true for legacy ephemeral keys`() { + val customerState = CustomerState.createForLegacyEphemeralKey( + customerId = "cus_1", + accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey( + ephemeralKeySecret = "ek_1", + ), + configuration = createConfiguration(allowsRemovalOfLastSavedPaymentMethod = true), + paymentMethods = listOf(), + ) + + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isTrue() + } + + @Test + fun `Should set 'canRemoveLastPaymentMethod' to false if config value is false for legacy ephemeral keys`() { + val customerState = CustomerState.createForLegacyEphemeralKey( + customerId = "cus_1", + accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey( + ephemeralKeySecret = "ek_1", + ), + configuration = createConfiguration(allowsRemovalOfLastSavedPaymentMethod = false), + paymentMethods = listOf(), + ) + + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isFalse() + } + + @Test + fun `Should set 'canRemoveLastPaymentMethod' to false if config value & server value are false`() = + customerSessionPermissionsTest( + canRemoveLastPaymentMethod = false, + canRemoveLastPaymentMethodConfigValue = false, + ) { customerState -> + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isFalse() + } + + @Test + fun `Should set 'canRemoveLastPaymentMethod' to false if config value is false & server MPE is disabled`() = + customerSessionPermissionsTest( + paymentElementDisabled = false, + canRemoveLastPaymentMethodConfigValue = false, + ) { customerState -> + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isFalse() + } + + @Test + fun `Should set 'canRemoveLastPaymentMethod' to false if config value is true but server MPE is disabled`() = + customerSessionPermissionsTest( + paymentElementDisabled = true, + canRemoveLastPaymentMethodConfigValue = true, + ) { customerState -> + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isFalse() + } + + @Test + fun `Should set 'canRemoveLastPaymentMethod' to false if config value is true but server value is false`() = + customerSessionPermissionsTest( + canRemoveLastPaymentMethod = false, + canRemoveLastPaymentMethodConfigValue = true, + ) { customerState -> + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isFalse() + } + + @Test + fun `Should set 'canRemoveLastPaymentMethod' to false if config value is false but server value is true`() = + customerSessionPermissionsTest( + canRemoveLastPaymentMethod = true, + canRemoveLastPaymentMethodConfigValue = false, + ) { customerState -> + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isFalse() + } + + @Test + fun `Should set 'canRemoveLastPaymentMethod' to true if config value & server value are true`() = + customerSessionPermissionsTest( + canRemoveLastPaymentMethod = true, + canRemoveLastPaymentMethodConfigValue = true, + ) { customerState -> + assertThat(customerState.permissions.canRemoveLastPaymentMethod).isTrue() + } + + private fun customerSessionPermissionsTest( + paymentElementDisabled: Boolean = false, + canRemoveLastPaymentMethodConfigValue: Boolean = true, + canRemoveLastPaymentMethod: Boolean = true, + test: (customerState: CustomerState) -> Unit, + ) { + val customerState = CustomerState.createForCustomerSession( + customer = createElementsSessionCustomer( + mobilePaymentElementComponent = if (paymentElementDisabled) { + ElementsSession.Customer.Components.MobilePaymentElement.Disabled + } else { + ElementsSession.Customer.Components.MobilePaymentElement.Enabled( + isPaymentMethodRemoveEnabled = true, + isPaymentMethodSaveEnabled = false, + canRemoveLastPaymentMethod = canRemoveLastPaymentMethod, + allowRedisplayOverride = null, + ) + } + ), + configuration = createConfiguration( + allowsRemovalOfLastSavedPaymentMethod = canRemoveLastPaymentMethodConfigValue + ), + supportedSavedPaymentMethodTypes = listOf(PaymentMethod.Type.Card) + ) + + test(customerState) + } + private fun createElementsSessionCustomer( customerId: String = "cus_1", ephemeralKeySecret: String = "ek_1", - paymentMethods: List, + paymentMethods: List = listOf(), mobilePaymentElementComponent: ElementsSession.Customer.Components.MobilePaymentElement ): ElementsSession.Customer { return ElementsSession.Customer( @@ -179,4 +305,12 @@ class CustomerStateTest { ), ) } + + private fun createConfiguration( + allowsRemovalOfLastSavedPaymentMethod: Boolean = true, + ): CommonConfiguration { + return PaymentSheetFixtures.CONFIG_CUSTOMER.asCommonConfiguration().copy( + allowsRemovalOfLastSavedPaymentMethod = allowsRemovalOfLastSavedPaymentMethod, + ) + } } diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt index deec2e92fa8..80724ba3816 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/DefaultPaymentElementLoaderTest.kt @@ -127,6 +127,7 @@ internal class DefaultPaymentElementLoaderTest { paymentMethods = PAYMENT_METHODS, permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = false, ), ), @@ -1380,6 +1381,7 @@ internal class DefaultPaymentElementLoaderTest { paymentMethods = cards, permissions = CustomerState.Permissions( canRemovePaymentMethods = false, + canRemoveLastPaymentMethod = false, canRemoveDuplicates = true, ), ) @@ -1396,6 +1398,7 @@ internal class DefaultPaymentElementLoaderTest { session = createElementsSessionCustomerSession( ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethod = true, isPaymentMethodSaveEnabled = false, allowRedisplayOverride = null, ) @@ -1421,6 +1424,7 @@ internal class DefaultPaymentElementLoaderTest { assertThat(state.customer?.permissions).isEqualTo( CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = true, ) ) @@ -1437,6 +1441,7 @@ internal class DefaultPaymentElementLoaderTest { ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodRemoveEnabled = false, isPaymentMethodSaveEnabled = false, + canRemoveLastPaymentMethod = true, allowRedisplayOverride = null, ) ), @@ -1461,6 +1466,7 @@ internal class DefaultPaymentElementLoaderTest { assertThat(state.customer?.permissions).isEqualTo( CustomerState.Permissions( canRemovePaymentMethods = false, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = true, ) ) @@ -1477,6 +1483,7 @@ internal class DefaultPaymentElementLoaderTest { ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodRemoveEnabled = false, isPaymentMethodSaveEnabled = false, + canRemoveLastPaymentMethod = true, allowRedisplayOverride = null, ) ), @@ -1501,6 +1508,7 @@ internal class DefaultPaymentElementLoaderTest { assertThat(state.customer?.permissions).isEqualTo( CustomerState.Permissions( canRemovePaymentMethods = false, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = true, ) ) @@ -1528,6 +1536,7 @@ internal class DefaultPaymentElementLoaderTest { assertThat(state.customer?.permissions).isEqualTo( CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = false, ) ) @@ -1651,6 +1660,7 @@ internal class DefaultPaymentElementLoaderTest { paymentMethods = cards, permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = false, ), ) @@ -2107,6 +2117,73 @@ internal class DefaultPaymentElementLoaderTest { ) } + @Test + fun `When using 'LegacyEphemeralKey',last PM permission should be true if config value is true`() = + removeLastPaymentMethodTest( + customer = PaymentSheet.CustomerConfiguration( + id = "cus_1", + ephemeralKeySecret = "ek_123", + ), + canRemoveLastPaymentMethodFromConfig = true, + ) { permissions -> + assertThat(permissions.canRemoveLastPaymentMethod).isTrue() + } + + @OptIn(ExperimentalCustomerSessionApi::class) + @Test + fun `When using 'CustomerSession', last PM permission should be true if server & config value is true`() = + removeLastPaymentMethodTest( + customer = PaymentSheet.CustomerConfiguration.createWithCustomerSession( + id = "cus_1", + clientSecret = "cuss_123", + ), + canRemoveLastPaymentMethodFromServer = true, + canRemoveLastPaymentMethodFromConfig = true, + ) { permissions -> + assertThat(permissions.canRemoveLastPaymentMethod).isTrue() + } + + private fun removeLastPaymentMethodTest( + customer: PaymentSheet.CustomerConfiguration, + shouldDisableMobilePaymentElement: Boolean = false, + canRemoveLastPaymentMethodFromServer: Boolean = true, + canRemoveLastPaymentMethodFromConfig: Boolean = true, + test: (CustomerState.Permissions) -> Unit, + ) = runTest { + val loader = createPaymentElementLoader( + customer = ElementsSession.Customer( + paymentMethods = PaymentMethodFactory.cards(4), + session = createElementsSessionCustomerSession( + if (shouldDisableMobilePaymentElement) { + ElementsSession.Customer.Components.MobilePaymentElement.Disabled + } else { + ElementsSession.Customer.Components.MobilePaymentElement.Enabled( + isPaymentMethodRemoveEnabled = false, + isPaymentMethodSaveEnabled = false, + canRemoveLastPaymentMethod = canRemoveLastPaymentMethodFromServer, + allowRedisplayOverride = null, + ) + } + ), + defaultPaymentMethod = null, + ) + ) + + val state = loader.load( + initializationMode = PaymentElementLoader.InitializationMode.PaymentIntent( + clientSecret = "client_secret" + ), + paymentSheetConfiguration = PaymentSheet.Configuration( + merchantDisplayName = "Merchant, Inc.", + customer = customer, + allowsRemovalOfLastSavedPaymentMethod = canRemoveLastPaymentMethodFromConfig + ), + initializedViaCompose = false, + ).getOrThrow() + + test(requireNotNull(state.customer).permissions) + } + private suspend fun testExternalPaymentMethods( requestedExternalPaymentMethods: List, externalPaymentMethodData: String?, @@ -2200,6 +2277,7 @@ internal class DefaultPaymentElementLoaderTest { ElementsSession.Customer.Components.MobilePaymentElement.Enabled( isPaymentMethodSaveEnabled = it, isPaymentMethodRemoveEnabled = true, + canRemoveLastPaymentMethod = true, allowRedisplayOverride = null, ) } ?: ElementsSession.Customer.Components.MobilePaymentElement.Disabled diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeInitialScreenFactoryTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeInitialScreenFactoryTest.kt index 9fbc72c5762..35d8a56bc81 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeInitialScreenFactoryTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/verticalmode/VerticalModeInitialScreenFactoryTest.kt @@ -74,6 +74,7 @@ class VerticalModeInitialScreenFactoryTest { paymentMethods = listOf(PaymentMethodFixtures.CARD_PAYMENT_METHOD), permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = true, ) ) diff --git a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/PaymentOptionsItemsMapperTest.kt b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/PaymentOptionsItemsMapperTest.kt index 9826fe251d4..3883b836a17 100644 --- a/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/PaymentOptionsItemsMapperTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/viewmodels/PaymentOptionsItemsMapperTest.kt @@ -91,6 +91,7 @@ class PaymentOptionsItemsMapperTest { paymentMethods = paymentMethods, permissions = CustomerState.Permissions( canRemovePaymentMethods = true, + canRemoveLastPaymentMethod = true, canRemoveDuplicates = false, ) )