Skip to content

Commit

Permalink
Address PR comments
Browse files Browse the repository at this point in the history
  • Loading branch information
samer-stripe committed Oct 1, 2024
1 parent 2cc22be commit e197a9c
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.stripe.android.common.validation

internal object CustomerSessionClientSecretValidator {
private const val EPHEMERAL_KEY_SECRET_PREFIX = "ek_"
private const val CUSTOMER_SESSION_CLIENT_SECRET_KEY_PREFIX = "cuss_"

sealed interface Result {
data object Valid : Result

sealed interface Error : Result {
data object Empty : Result

data object LegacyEphemeralKey : Result

data object UnknownKey : Result
}
}

fun validate(customerSessionClientSecret: String): Result {
return when {
customerSessionClientSecret.isBlank() ->
Result.Error.Empty
customerSessionClientSecret.startsWith(EPHEMERAL_KEY_SECRET_PREFIX) ->
Result.Error.LegacyEphemeralKey
!customerSessionClientSecret.startsWith(CUSTOMER_SESSION_CLIENT_SECRET_KEY_PREFIX) ->
Result.Error.UnknownKey
else -> Result.Valid
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,13 @@ package com.stripe.android.customersheet.data
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes

internal sealed interface CachedCustomerEphemeralKey {
fun shouldRefresh(currentTimeInMillis: Long): Boolean

data class Available(
val customerId: String,
val ephemeralKey: String,
private val expiresAt: Int,
) : CachedCustomerEphemeralKey {
override fun shouldRefresh(currentTimeInMillis: Long): Boolean {
val remainingTime = expiresAt - currentTimeInMillis.milliseconds.inWholeSeconds
return remainingTime <= 5.minutes.inWholeSeconds
}
}

data object None : CachedCustomerEphemeralKey {
override fun shouldRefresh(currentTimeInMillis: Long): Boolean {
return true
}
internal data class CachedCustomerEphemeralKey(
val customerId: String,
val ephemeralKey: String,
private val expiresAt: Int,
) {
fun shouldRefresh(currentTimeInMillis: Long): Boolean {
val remainingTime = expiresAt - currentTimeInMillis.milliseconds.inWholeSeconds
return remainingTime <= 5.minutes.inWholeSeconds
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.stripe.android.customersheet.data

import com.stripe.android.common.validation.CustomerSessionClientSecretValidator
import com.stripe.android.core.injection.IOContext
import com.stripe.android.customersheet.CustomerSheet
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
Expand All @@ -17,14 +18,15 @@ import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext

internal interface CustomerSessionElementsSessionManager {
suspend fun fetchCustomerSessionEphemeralKey(): Result<CachedCustomerEphemeralKey.Available>
suspend fun fetchCustomerSessionEphemeralKey(): Result<CachedCustomerEphemeralKey>

suspend fun fetchElementsSession(): Result<ElementsSessionWithCustomer>
suspend fun fetchElementsSession(): Result<CustomerSessionElementsSession>
}

internal data class ElementsSessionWithCustomer(
internal data class CustomerSessionElementsSession(
val elementsSession: ElementsSession,
val customer: ElementsSession.Customer,
val ephemeralKey: CachedCustomerEphemeralKey,
)

@OptIn(ExperimentalCustomerSheetApi::class, ExperimentalCustomerSessionApi::class)
Expand All @@ -38,32 +40,22 @@ internal class DefaultCustomerSessionElementsSessionManager @Inject constructor(
@IOContext private val workContext: CoroutineContext,
) : CustomerSessionElementsSessionManager {
@Volatile
private var cachedCustomerEphemeralKey: CachedCustomerEphemeralKey = CachedCustomerEphemeralKey.None
private var cachedCustomerEphemeralKey: CachedCustomerEphemeralKey? = null

private var intentConfiguration: CustomerSheet.IntentConfiguration? = null

override suspend fun fetchCustomerSessionEphemeralKey(): Result<CachedCustomerEphemeralKey.Available> {
override suspend fun fetchCustomerSessionEphemeralKey(): Result<CachedCustomerEphemeralKey> {
return withContext(workContext) {
runCatching {
val ephemeralKey = cachedCustomerEphemeralKey.takeUnless { cachedCustomerEphemeralKey ->
cachedCustomerEphemeralKey.shouldRefresh(timeProvider())
} ?: run {
fetchElementsSession().getOrThrow()

cachedCustomerEphemeralKey
}

when (ephemeralKey) {
is CachedCustomerEphemeralKey.Available -> ephemeralKey
is CachedCustomerEphemeralKey.None -> throw IllegalStateException(
"No ephemeral key available!"
)
}
cachedCustomerEphemeralKey.takeUnless { cachedCustomerEphemeralKey ->
cachedCustomerEphemeralKey == null ||
cachedCustomerEphemeralKey.shouldRefresh(timeProvider())
} ?: fetchElementsSession().getOrThrow().ephemeralKey
}
}
}

override suspend fun fetchElementsSession(): Result<ElementsSessionWithCustomer> {
override suspend fun fetchElementsSession(): Result<CustomerSessionElementsSession> {
return withContext(workContext) {
runCatching {
val intentConfiguration = intentConfiguration
Expand Down Expand Up @@ -110,46 +102,45 @@ internal class DefaultCustomerSessionElementsSessionManager @Inject constructor(
)
}

ElementsSessionWithCustomer(
elementsSession = elementsSession,
customer = customer
)
}.onSuccess { elementsSessionWithCustomer ->
val customerSession = elementsSessionWithCustomer.customer.session
val customerSession = customer.session

cachedCustomerEphemeralKey = CachedCustomerEphemeralKey.Available(
customerId = customerSession.customerId,
ephemeralKey = customerSession.apiKey,
expiresAt = customerSession.apiKeyExpiry,
CustomerSessionElementsSession(
elementsSession = elementsSession,
customer = customer,
ephemeralKey = CachedCustomerEphemeralKey(
customerId = customerSession.customerId,
ephemeralKey = customerSession.apiKey,
expiresAt = customerSession.apiKeyExpiry,
)
)
}.onSuccess { customerSessionElementsSession ->
cachedCustomerEphemeralKey = customerSessionElementsSession.ephemeralKey
}.getOrThrow()
}
}
}

private fun validateCustomerSessionClientSecret(customerSessionClientSecret: String) {
val error = when {
customerSessionClientSecret.isBlank() -> {
"The 'customerSessionClientSecret' cannot be an empty string."
val result = CustomerSessionClientSecretValidator
.validate(customerSessionClientSecret)

val error = when (result) {
is CustomerSessionClientSecretValidator.Result.Error.Empty -> {
"The provided 'customerSessionClientSecret' cannot be an empty string."
}
customerSessionClientSecret.startsWith(EPHEMERAL_KEY_SECRET_PREFIX) -> {
is CustomerSessionClientSecretValidator.Result.Error.LegacyEphemeralKey -> {
"Provided secret looks like an Ephemeral Key secret, but expecting a CustomerSession client " +
"secret. See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create"
}
!customerSessionClientSecret.startsWith(CUSTOMER_SESSION_CLIENT_SECRET_KEY_PREFIX) -> {
is CustomerSessionClientSecretValidator.Result.Error.UnknownKey -> {
"Provided secret does not look like a CustomerSession client secret. " +
"See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create"
}
else -> null
is CustomerSessionClientSecretValidator.Result.Valid -> null
}

error?.let {
throw IllegalArgumentException(it)
throw IllegalArgumentException(error)
}
}

private companion object {
const val EPHEMERAL_KEY_SECRET_PREFIX = "ek_"
const val CUSTOMER_SESSION_CLIENT_SECRET_KEY_PREFIX = "cuss_"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
import com.stripe.android.common.validation.CustomerSessionClientSecretValidator
import com.stripe.android.model.CardBrand
import com.stripe.android.paymentsheet.addresselement.AddressDetails
import com.stripe.android.uicore.PrimaryButtonColors
Expand All @@ -13,9 +14,6 @@ import com.stripe.android.uicore.StripeTheme
import com.stripe.android.uicore.StripeThemeDefaults
import java.lang.IllegalArgumentException

private const val EPHEMERAL_KEY_SECRET_PREFIX = "ek_"
private const val CUSTOMER_SESSION_CLIENT_SECRET_KEY_PREFIX = "cuss_"

internal fun PaymentSheet.Configuration.validate() {
// These are not localized as they are not intended to be displayed to a user.
when {
Expand Down Expand Up @@ -44,23 +42,29 @@ internal fun PaymentSheet.Configuration.validate() {
}
}
is PaymentSheet.CustomerAccessType.CustomerSession -> {
val customerSessionClientSecret = customerAccessType.customerSessionClientSecret
val result = CustomerSessionClientSecretValidator
.validate(customerAccessType.customerSessionClientSecret)

if (customerSessionClientSecret.isBlank()) {
throw IllegalArgumentException(
"When a CustomerConfiguration is passed to PaymentSheet, " +
"the customerSessionClientSecret cannot be an empty string."
)
} else if (customerSessionClientSecret.startsWith(EPHEMERAL_KEY_SECRET_PREFIX)) {
throw IllegalArgumentException(
"Argument looks like an Ephemeral Key secret, but expecting a CustomerSession client " +
"secret. See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create"
)
} else if (!customerSessionClientSecret.startsWith(CUSTOMER_SESSION_CLIENT_SECRET_KEY_PREFIX)) {
throw IllegalArgumentException(
"Argument does not look like a CustomerSession client secret. " +
"See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create"
)
when (result) {
is CustomerSessionClientSecretValidator.Result.Error.Empty -> {
throw IllegalArgumentException(
"When a CustomerConfiguration is passed to PaymentSheet, " +
"the customerSessionClientSecret cannot be an empty string."
)
}
is CustomerSessionClientSecretValidator.Result.Error.LegacyEphemeralKey -> {
throw IllegalArgumentException(
"Argument looks like an Ephemeral Key secret, but expecting a CustomerSession client " +
"secret. See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create"
)
}
is CustomerSessionClientSecretValidator.Result.Error.UnknownKey -> {
throw IllegalArgumentException(
"Argument does not look like a CustomerSession client secret. " +
"See CustomerSession API: https://docs.stripe.com/api/customer_sessions/create"
)
}
is CustomerSessionClientSecretValidator.Result.Valid -> Unit
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class CustomerSessionPaymentMethodDataSourceTest {
customerRepository = customerRepository,
elementsSessionManager = FakeCustomerSessionElementsSessionManager(
ephemeralKey = Result.success(
CachedCustomerEphemeralKey.Available(
CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 999999,
Expand Down Expand Up @@ -169,7 +169,7 @@ class CustomerSessionPaymentMethodDataSourceTest {
customerRepository = customerRepository,
elementsSessionManager = FakeCustomerSessionElementsSessionManager(
ephemeralKey = Result.success(
CachedCustomerEphemeralKey.Available(
CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 999999,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class DefaultCustomerSessionElementsSessionManagerTest {

assertThat(result).isEqualTo(
Result.success(
CachedCustomerEphemeralKey.Available(
CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 999999,
Expand Down Expand Up @@ -184,7 +184,7 @@ class DefaultCustomerSessionElementsSessionManagerTest {
assertThat(amountOfCalls).isEqualTo(1)
assertThat(lastResult).isEqualTo(
Result.success(
CachedCustomerEphemeralKey.Available(
CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 999999,
Expand Down Expand Up @@ -223,7 +223,7 @@ class DefaultCustomerSessionElementsSessionManagerTest {
assertThat(amountOfCalls).isEqualTo(4)
assertThat(lastResult).isEqualTo(
Result.success(
CachedCustomerEphemeralKey.Available(
CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 200000,
Expand Down Expand Up @@ -258,7 +258,7 @@ class DefaultCustomerSessionElementsSessionManagerTest {

assertThat(ephemeralKey).isEqualTo(
Result.success(
CachedCustomerEphemeralKey.Available(
CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 999999,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.stripe.android.model.PaymentMethod
import com.stripe.android.testing.SetupIntentFactory

internal class FakeCustomerSessionElementsSessionManager(
private val ephemeralKey: Result<CachedCustomerEphemeralKey.Available> = Result.success(
CachedCustomerEphemeralKey.Available(
private val ephemeralKey: Result<CachedCustomerEphemeralKey> = Result.success(
CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 999999,
Expand All @@ -32,8 +32,8 @@ internal class FakeCustomerSessionElementsSessionManager(
defaultPaymentMethod = null,
paymentMethods = paymentMethods,
),
private val elementsSession: Result<ElementsSessionWithCustomer> = Result.success(
ElementsSessionWithCustomer(
private val elementsSession: Result<CustomerSessionElementsSession> = Result.success(
CustomerSessionElementsSession(
elementsSession = ElementsSession(
linkSettings = null,
paymentMethodSpecs = null,
Expand All @@ -46,14 +46,19 @@ internal class FakeCustomerSessionElementsSessionManager(
cardBrandChoice = null,
),
customer = customer,
ephemeralKey = CachedCustomerEphemeralKey(
customerId = "cus_1",
ephemeralKey = "ek_123",
expiresAt = 999999,
),
)
)
) : CustomerSessionElementsSessionManager {
override suspend fun fetchCustomerSessionEphemeralKey(): Result<CachedCustomerEphemeralKey.Available> {
override suspend fun fetchCustomerSessionEphemeralKey(): Result<CachedCustomerEphemeralKey> {
return ephemeralKey
}

override suspend fun fetchElementsSession(): Result<ElementsSessionWithCustomer> {
override suspend fun fetchElementsSession(): Result<CustomerSessionElementsSession> {
return elementsSession
}
}

0 comments on commit e197a9c

Please sign in to comment.