diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CachedCustomerEphemeralKey.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CachedCustomerEphemeralKey.kt index f43808f11f6..eac5cb890fe 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CachedCustomerEphemeralKey.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CachedCustomerEphemeralKey.kt @@ -3,14 +3,35 @@ package com.stripe.android.customersheet.data import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.minutes -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 +internal sealed interface CachedCustomerEphemeralKey { + fun shouldRefresh(currentTimeInMillis: Long): Boolean - return remainingTime <= 5.minutes.inWholeSeconds + 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 fun Result.mapWithEphemeralKeyCatching( + mapper: (customerId: String, ephemeralKey: String) -> T +): Result { + return mapCatching { cachedKey -> + when (cachedKey) { + is CachedCustomerEphemeralKey.Available -> mapper(cachedKey.customerId, cachedKey.ephemeralKey) + is CachedCustomerEphemeralKey.None -> throw IllegalStateException("No ephemeral key was available!") + } + } +} \ No newline at end of file diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionElementsSessionManager.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionElementsSessionManager.kt index dc9bba50214..4137b71134b 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionElementsSessionManager.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionElementsSessionManager.kt @@ -30,23 +30,20 @@ internal class DefaultCustomerSessionElementsSessionManager @Inject constructor( @IOContext private val workContext: CoroutineContext, ) : CustomerSessionElementsSessionManager { @Volatile - private var cachedCustomerEphemeralKey: CachedCustomerEphemeralKey? = null + private var cachedCustomerEphemeralKey: CachedCustomerEphemeralKey = CachedCustomerEphemeralKey.None private var intentConfiguration: CustomerSheet.IntentConfiguration? = null override suspend fun fetchCustomerSessionEphemeralKey(): Result { return withContext(workContext) { cachedCustomerEphemeralKey.takeUnless { cachedCustomerEphemeralKey -> - cachedCustomerEphemeralKey == null || cachedCustomerEphemeralKey.shouldRefresh( - timeProvider() - ) + cachedCustomerEphemeralKey.shouldRefresh(timeProvider()) }?.let { Result.success(it) - } ?: run { - fetchElementsSession().mapCatching { - cachedCustomerEphemeralKey - ?: throw IllegalStateException("Should have been initialized from `elements/session`!") - } + } ?: kotlin.runCatching { + fetchElementsSession().getOrThrow() + + cachedCustomerEphemeralKey } } } @@ -85,7 +82,7 @@ internal class DefaultCustomerSessionElementsSessionManager @Inject constructor( externalPaymentMethods = listOf(), ).onSuccess { elementsSession -> elementsSession.customer?.session?.run { - cachedCustomerEphemeralKey = CachedCustomerEphemeralKey( + cachedCustomerEphemeralKey = CachedCustomerEphemeralKey.Available( customerId = customerId, ephemeralKey = apiKey, expiresAt = apiKeyExpiry, diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSource.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSource.kt index 04656cadb13..f379fc524ac 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSource.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSource.kt @@ -42,8 +42,8 @@ internal class CustomerSessionSavedSelectionDataSource @Inject constructor( } private suspend fun createPrefsRepository(): CustomerSheetDataResult { - return elementsSessionManager.fetchCustomerSessionEphemeralKey().mapCatching { ephemeralKey -> - prefsRepositoryFactory(ephemeralKey.customerId) + return elementsSessionManager.fetchCustomerSessionEphemeralKey().mapWithEphemeralKeyCatching { customerId, _ -> + prefsRepositoryFactory(customerId) }.toCustomerSheetDataResult() } } diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/data/DefaultCustomerSessionElementsSessionManagerTest.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/DefaultCustomerSessionElementsSessionManagerTest.kt index 81c3639edbb..d53b5f36ce9 100644 --- a/paymentsheet/src/test/java/com/stripe/android/customersheet/data/DefaultCustomerSessionElementsSessionManagerTest.kt +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/DefaultCustomerSessionElementsSessionManagerTest.kt @@ -126,7 +126,7 @@ class DefaultCustomerSessionElementsSessionManagerTest { assertThat(result).isEqualTo( Result.success( - CachedCustomerEphemeralKey( + CachedCustomerEphemeralKey.Available( customerId = "cus_1", ephemeralKey = "ek_123", expiresAt = 999999, @@ -159,6 +159,7 @@ class DefaultCustomerSessionElementsSessionManagerTest { apiKey = "ek_123", apiKeyExpiry = 999999, customerId = "cus_1", + currentTime = 10, onCustomerSessionClientSecret = { amountOfCalls++ @@ -180,7 +181,81 @@ class DefaultCustomerSessionElementsSessionManagerTest { assertThat(amountOfCalls).isEqualTo(1) assertThat(lastResult).isEqualTo( Result.success( - CachedCustomerEphemeralKey( + CachedCustomerEphemeralKey.Available( + customerId = "cus_1", + ephemeralKey = "ek_123", + expiresAt = 999999, + ) + ) + ) + } + + @Test + fun `on fetch ephemeral key, should re-fetch key if expired`() = runTest { + var amountOfCalls = 0 + + val manager = createElementsSessionManagerWithCustomer( + apiKey = "ek_123", + apiKeyExpiry = 200000, + customerId = "cus_1", + currentTime = 210000000, + onCustomerSessionClientSecret = { + amountOfCalls++ + + Result.success( + CustomerSheet.CustomerSessionClientSecret.create( + customerId = "cus_1", + clientSecret = "cuss_123", + ) + ) + } + ) + + manager.fetchCustomerSessionEphemeralKey() + manager.fetchCustomerSessionEphemeralKey() + manager.fetchCustomerSessionEphemeralKey() + + val lastResult = manager.fetchCustomerSessionEphemeralKey() + + assertThat(amountOfCalls).isEqualTo(4) + assertThat(lastResult).isEqualTo( + Result.success( + CachedCustomerEphemeralKey.Available( + customerId = "cus_1", + ephemeralKey = "ek_123", + expiresAt = 200000, + ) + ) + ) + } + + @Test + fun `on fetch elements session, should set ephemeral key & be re-used when fetching ephemeral key`() = runTest { + var amountOfCalls = 0 + + val manager = createElementsSessionManagerWithCustomer( + apiKeyExpiry = 999999, + currentTime = 10, + onCustomerSessionClientSecret = { + amountOfCalls++ + + Result.success( + CustomerSheet.CustomerSessionClientSecret.create( + customerId = "cus_1", + clientSecret = "cuss_123", + ) + ) + }, + ) + + manager.fetchElementsSession() + val ephemeralKey = manager.fetchCustomerSessionEphemeralKey() + + assertThat(amountOfCalls).isEqualTo(1) + + assertThat(ephemeralKey).isEqualTo( + Result.success( + CachedCustomerEphemeralKey.Available( customerId = "cus_1", ephemeralKey = "ek_123", expiresAt = 999999, diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/data/FakeCustomerSessionElementsSessionManager.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/FakeCustomerSessionElementsSessionManager.kt index 234ea24da69..0a3d9a227f0 100644 --- a/paymentsheet/src/test/java/com/stripe/android/customersheet/data/FakeCustomerSessionElementsSessionManager.kt +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/FakeCustomerSessionElementsSessionManager.kt @@ -4,7 +4,7 @@ import com.stripe.android.model.ElementsSession internal class FakeCustomerSessionElementsSessionManager( private val ephemeralKey: Result = Result.success( - CachedCustomerEphemeralKey( + CachedCustomerEphemeralKey.Available( customerId = "cus_1", ephemeralKey = "ek_123", expiresAt = 999999,