From b7cbd4d98e4584e0d22b7a001fd095998fedc816 Mon Sep 17 00:00:00 2001 From: Samer Alabi <141707240+samer-stripe@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:38:40 -0400 Subject: [PATCH] Move `CustomerState` logic to factory methods. (#8769) * Move `CustomerState` logic to factory methods. * Use `ElementsSession.Customer` in factory method and handle `null` customer in `PaymentSheetLoader` --- .../paymentsheet/state/CustomerState.kt | 67 ++++++++ .../paymentsheet/state/PaymentSheetLoader.kt | 101 ++++--------- .../paymentsheet/state/CustomerStateTest.kt | 143 ++++++++++++++++++ 3 files changed, 241 insertions(+), 70 deletions(-) create mode 100644 paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/CustomerStateTest.kt 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 0cc8910f1c7..2af99ad834e 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,7 +1,9 @@ package com.stripe.android.paymentsheet.state import android.os.Parcelable +import com.stripe.android.model.ElementsSession import com.stripe.android.model.PaymentMethod +import com.stripe.android.paymentsheet.PaymentSheet import kotlinx.parcelize.Parcelize @Parcelize @@ -16,4 +18,69 @@ internal data class CustomerState( val canRemovePaymentMethods: Boolean, val canRemoveDuplicates: Boolean, ) : Parcelable + + internal companion object { + /** + * Creates a [CustomerState] instance using an [ElementsSession.Customer] response. + * + * @param customer elements session customer data + * + * @return [CustomerState] instance using [ElementsSession.Customer] data + */ + internal fun createForCustomerSession( + customer: ElementsSession.Customer + ): CustomerState { + val canRemovePaymentMethods = when ( + val paymentSheetComponent = customer.session.components.paymentSheet + ) { + is ElementsSession.Customer.Components.PaymentSheet.Enabled -> + paymentSheetComponent.isPaymentMethodRemoveEnabled + is ElementsSession.Customer.Components.PaymentSheet.Disabled -> false + } + + return CustomerState( + id = customer.session.customerId, + ephemeralKeySecret = customer.session.apiKey, + paymentMethods = customer.paymentMethods, + permissions = Permissions( + canRemovePaymentMethods = canRemovePaymentMethods, + // Should always remove duplicates when using `customer_session` + canRemoveDuplicates = true, + ) + ) + } + + /** + * Creates a [CustomerState] instance with un-scoped legacy ephemeral key information. + * + * @param customerId identifier for a customer + * @param accessType legacy ephemeral key secret access type + * @param paymentMethods list of payment methods belonging to the customer + * + * @return [CustomerState] instance with legacy ephemeral key secrets + */ + internal fun createForLegacyEphemeralKey( + customerId: String, + accessType: PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey, + paymentMethods: List, + ): CustomerState { + return CustomerState( + id = customerId, + ephemeralKeySecret = accessType.ephemeralKeySecret, + paymentMethods = paymentMethods, + permissions = Permissions( + /* + * Un-scoped legacy ephemeral keys have full permissions to remove/save/modify. This should + * always be set to true. + */ + canRemovePaymentMethods = true, + /* + * Removing duplicates is not applicable here since we don't filter out duplicates for for + * un-scoped ephemeral keys. + */ + canRemoveDuplicates = false, + ) + ) + } + } } diff --git a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentSheetLoader.kt b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentSheetLoader.kt index e13b19c7639..f1f2a3bc17f 100644 --- a/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentSheetLoader.kt +++ b/paymentsheet/src/main/java/com/stripe/android/paymentsheet/state/PaymentSheetLoader.kt @@ -315,11 +315,37 @@ internal class DefaultPaymentSheetLoader @Inject constructor( ): CustomerState? { val customerConfig = config.customer - val customerState = when (customerConfig?.accessType) { - is PaymentSheet.CustomerAccessType.CustomerSession -> - elementsSession.toCustomerState() - is PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey -> - customerConfig.toCustomerState(metadata) + val customerState = when (val accessType = customerConfig?.accessType) { + is PaymentSheet.CustomerAccessType.CustomerSession -> { + elementsSession.customer?.let { customer -> + CustomerState.createForCustomerSession(customer) + } ?: run { + val exception = IllegalStateException( + "Excepted 'customer' attribute as part of 'elements_session' response!" + ) + + errorReporter.report( + ErrorReporter.UnexpectedErrorEvent.PAYMENT_SHEET_LOADER_ELEMENTS_SESSION_CUSTOMER_NOT_FOUND, + StripeException.create(exception) + ) + + if (!elementsSession.stripeIntent.isLiveMode) { + throw exception + } + + null + } + } + is PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey -> { + CustomerState.createForLegacyEphemeralKey( + customerId = customerConfig.id, + accessType = accessType, + paymentMethods = retrieveCustomerPaymentMethods( + metadata = metadata, + customerConfig = customerConfig, + ) + ) + } else -> null } @@ -536,71 +562,6 @@ internal class DefaultPaymentSheetLoader @Inject constructor( } } - private fun ElementsSession.toCustomerState(): CustomerState? { - return customer?.let { customer -> - val canRemovePaymentMethods = when ( - val paymentSheetComponent = customer.session.components.paymentSheet - ) { - is ElementsSession.Customer.Components.PaymentSheet.Enabled -> - paymentSheetComponent.isPaymentMethodRemoveEnabled - is ElementsSession.Customer.Components.PaymentSheet.Disabled -> false - } - - CustomerState( - id = customer.session.customerId, - ephemeralKeySecret = customer.session.apiKey, - paymentMethods = customer.paymentMethods, - permissions = CustomerState.Permissions( - canRemovePaymentMethods = canRemovePaymentMethods, - // Should always remove duplicates when using `customer_session` - canRemoveDuplicates = true, - ) - ) - } ?: run { - val exception = IllegalStateException( - "Excepted 'customer' attribute as part of 'elements_session' response!" - ) - - errorReporter.report( - ErrorReporter - .UnexpectedErrorEvent - .PAYMENT_SHEET_LOADER_ELEMENTS_SESSION_CUSTOMER_NOT_FOUND, - StripeException.create(exception) - ) - - if (!stripeIntent.isLiveMode) { - throw exception - } - - null - } - } - - private suspend fun PaymentSheet.CustomerConfiguration.toCustomerState( - metadata: PaymentMethodMetadata, - ): CustomerState { - return CustomerState( - id = id, - ephemeralKeySecret = ephemeralKeySecret, - paymentMethods = retrieveCustomerPaymentMethods( - metadata = metadata, - customerConfig = this, - ), - permissions = CustomerState.Permissions( - /* - * Un-scoped legacy ephemeral keys have full permissions to remove/save/modify. This should always be - * set to true. - */ - canRemovePaymentMethods = true, - /* - * Removing duplicates is not applicable here since we don't filter out duplicates for for - * un-scoped ephemeral keys. - */ - canRemoveDuplicates = false, - ) - ) - } - /* * This is a helper for catching thrown exceptions from 'async' calls within a suspendable function. * 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 new file mode 100644 index 00000000000..f86573cee3e --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/paymentsheet/state/CustomerStateTest.kt @@ -0,0 +1,143 @@ +package com.stripe.android.paymentsheet.state + +import com.google.common.truth.Truth.assertThat +import com.stripe.android.model.ElementsSession +import com.stripe.android.model.PaymentMethod +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.testing.PaymentMethodFactory +import org.junit.Test + +class CustomerStateTest { + @Test + fun `Should create 'CustomerState' for customer session properly with permissions disabled`() { + val paymentMethods = PaymentMethodFactory.cards(4) + val customer = createElementsSessionCustomer( + customerId = "cus_1", + ephemeralKeySecret = "ek_1", + paymentMethods = paymentMethods, + paymentSheetComponent = ElementsSession.Customer.Components.PaymentSheet.Disabled + ) + + val customerState = CustomerState.createForCustomerSession(customer) + + assertThat(customerState).isEqualTo( + CustomerState( + id = "cus_1", + ephemeralKeySecret = "ek_1", + paymentMethods = paymentMethods, + permissions = CustomerState.Permissions( + canRemovePaymentMethods = false, + // Always true for `customer_session` + canRemoveDuplicates = true, + ), + ) + ) + } + + @Test + fun `Should create 'CustomerState' for customer session properly with remove permissions enabled`() { + val paymentMethods = PaymentMethodFactory.cards(4) + val customer = createElementsSessionCustomer( + customerId = "cus_1", + ephemeralKeySecret = "ek_1", + paymentMethods = paymentMethods, + paymentSheetComponent = ElementsSession.Customer.Components.PaymentSheet.Enabled( + isPaymentMethodSaveEnabled = false, + isPaymentMethodRemoveEnabled = true, + ), + ) + + val customerState = CustomerState.createForCustomerSession(customer) + + assertThat(customerState).isEqualTo( + CustomerState( + id = "cus_1", + ephemeralKeySecret = "ek_1", + paymentMethods = paymentMethods, + permissions = CustomerState.Permissions( + canRemovePaymentMethods = true, + // Always true for `customer_session` + canRemoveDuplicates = true, + ), + ) + ) + } + + @Test + fun `Should create 'CustomerState' for customer session properly with remove permissions disabled`() { + val paymentMethods = PaymentMethodFactory.cards(3) + val customer = createElementsSessionCustomer( + customerId = "cus_3", + ephemeralKeySecret = "ek_3", + paymentMethods = paymentMethods, + paymentSheetComponent = ElementsSession.Customer.Components.PaymentSheet.Enabled( + isPaymentMethodSaveEnabled = false, + isPaymentMethodRemoveEnabled = false, + ), + ) + + val customerState = CustomerState.createForCustomerSession(customer) + + assertThat(customerState).isEqualTo( + CustomerState( + id = "cus_3", + ephemeralKeySecret = "ek_3", + paymentMethods = paymentMethods, + permissions = CustomerState.Permissions( + canRemovePaymentMethods = false, + // Always true for `customer_session` + canRemoveDuplicates = true, + ), + ) + ) + } + + @Test + fun `Should create 'CustomerState' for legacy ephemeral keys properly`() { + val paymentMethods = PaymentMethodFactory.cards(7) + val customerState = CustomerState.createForLegacyEphemeralKey( + customerId = "cus_1", + accessType = PaymentSheet.CustomerAccessType.LegacyCustomerEphemeralKey( + ephemeralKeySecret = "ek_1", + ), + paymentMethods = paymentMethods, + ) + + assertThat(customerState).isEqualTo( + CustomerState( + id = "cus_1", + ephemeralKeySecret = "ek_1", + paymentMethods = paymentMethods, + permissions = CustomerState.Permissions( + // Always true for legacy ephemeral keys since un-scoped + canRemovePaymentMethods = true, + // Always 'false' for legacy ephemeral keys + canRemoveDuplicates = false, + ), + ) + ) + } + + private fun createElementsSessionCustomer( + customerId: String, + ephemeralKeySecret: String, + paymentMethods: List, + paymentSheetComponent: ElementsSession.Customer.Components.PaymentSheet + ): ElementsSession.Customer { + return ElementsSession.Customer( + paymentMethods = paymentMethods, + defaultPaymentMethod = null, + session = ElementsSession.Customer.Session( + id = "cuss_1", + customerId = customerId, + apiKey = ephemeralKeySecret, + apiKeyExpiry = 999999999, + liveMode = false, + components = ElementsSession.Customer.Components( + customerSheet = ElementsSession.Customer.Components.CustomerSheet.Disabled, + paymentSheet = paymentSheetComponent + ) + ), + ) + } +}