Skip to content

Commit

Permalink
Move CustomerState logic to factory methods. (#8769)
Browse files Browse the repository at this point in the history
* Move `CustomerState` logic to factory methods.

* Use `ElementsSession.Customer` in factory method and handle `null` customer in `PaymentSheetLoader`
  • Loading branch information
samer-stripe authored Jul 12, 2024
1 parent fa8a3a9 commit b7cbd4d
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<PaymentMethod>,
): 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,
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PaymentMethod>,
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
)
),
)
}
}

0 comments on commit b7cbd4d

Please sign in to comment.