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 new file mode 100644 index 00000000000..f43808f11f6 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CachedCustomerEphemeralKey.kt @@ -0,0 +1,16 @@ +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 + + return remainingTime <= 5.minutes.inWholeSeconds + } +} 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 new file mode 100644 index 00000000000..dc9bba50214 --- /dev/null +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/CustomerSessionElementsSessionManager.kt @@ -0,0 +1,98 @@ +package com.stripe.android.customersheet.data + +import com.stripe.android.core.injection.IOContext +import com.stripe.android.customersheet.CustomerSheet +import com.stripe.android.customersheet.ExperimentalCustomerSheetApi +import com.stripe.android.model.ElementsSession +import com.stripe.android.paymentsheet.ExperimentalCustomerSessionApi +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.PrefsRepository +import com.stripe.android.paymentsheet.model.SavedSelection +import com.stripe.android.paymentsheet.repositories.ElementsSessionRepository +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.CoroutineContext + +internal interface CustomerSessionElementsSessionManager { + suspend fun fetchCustomerSessionEphemeralKey(): Result + + suspend fun fetchElementsSession(): Result +} + +@OptIn(ExperimentalCustomerSheetApi::class, ExperimentalCustomerSessionApi::class) +@Singleton +internal class DefaultCustomerSessionElementsSessionManager @Inject constructor( + private val elementsSessionRepository: ElementsSessionRepository, + private val prefsRepositoryFactory: @JvmSuppressWildcards (String) -> PrefsRepository, + private val customerSessionProvider: CustomerSheet.CustomerSessionProvider, + private val timeProvider: () -> Long, + @IOContext private val workContext: CoroutineContext, +) : CustomerSessionElementsSessionManager { + @Volatile + private var cachedCustomerEphemeralKey: CachedCustomerEphemeralKey? = null + + private var intentConfiguration: CustomerSheet.IntentConfiguration? = null + + override suspend fun fetchCustomerSessionEphemeralKey(): Result { + return withContext(workContext) { + cachedCustomerEphemeralKey.takeUnless { cachedCustomerEphemeralKey -> + cachedCustomerEphemeralKey == null || cachedCustomerEphemeralKey.shouldRefresh( + timeProvider() + ) + }?.let { + Result.success(it) + } ?: run { + fetchElementsSession().mapCatching { + cachedCustomerEphemeralKey + ?: throw IllegalStateException("Should have been initialized from `elements/session`!") + } + } + } + } + + override suspend fun fetchElementsSession(): Result { + return withContext(workContext) { + runCatching { + val intentConfiguration = intentConfiguration + ?: customerSessionProvider.intentConfiguration() + .onSuccess { intentConfiguration = it } + .getOrThrow() + + val customerSessionClientSecret = customerSessionProvider + .providesCustomerSessionClientSecret() + .getOrThrow() + + val prefsRepository = prefsRepositoryFactory(customerSessionClientSecret.customerId) + + val savedSelection = prefsRepository.getSavedSelection( + isGooglePayAvailable = false, + isLinkAvailable = false, + ) as? SavedSelection.PaymentMethod + + elementsSessionRepository.get( + initializationMode = PaymentSheet.InitializationMode.DeferredIntent( + intentConfiguration = PaymentSheet.IntentConfiguration( + mode = PaymentSheet.IntentConfiguration.Mode.Setup(), + paymentMethodTypes = intentConfiguration.paymentMethodTypes, + ) + ), + defaultPaymentMethodId = savedSelection?.id, + customer = PaymentSheet.CustomerConfiguration.createWithCustomerSession( + id = customerSessionClientSecret.customerId, + clientSecret = customerSessionClientSecret.clientSecret, + ), + externalPaymentMethods = listOf(), + ).onSuccess { elementsSession -> + elementsSession.customer?.session?.run { + cachedCustomerEphemeralKey = CachedCustomerEphemeralKey( + customerId = customerId, + ephemeralKey = apiKey, + expiresAt = apiKeyExpiry, + ) + } + }.getOrThrow() + } + } + } +} 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 a77079c385f..04656cadb13 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 @@ -1,14 +1,49 @@ package com.stripe.android.customersheet.data +import com.stripe.android.core.injection.IOContext +import com.stripe.android.paymentsheet.PrefsRepository import com.stripe.android.paymentsheet.model.SavedSelection +import kotlinx.coroutines.withContext +import java.io.IOException import javax.inject.Inject +import kotlin.coroutines.CoroutineContext -internal class CustomerSessionSavedSelectionDataSource @Inject constructor() : CustomerSheetSavedSelectionDataSource { +internal class CustomerSessionSavedSelectionDataSource @Inject constructor( + private val elementsSessionManager: CustomerSessionElementsSessionManager, + private val prefsRepositoryFactory: @JvmSuppressWildcards (String) -> PrefsRepository, + @IOContext private val workContext: CoroutineContext, +) : CustomerSheetSavedSelectionDataSource { override suspend fun retrieveSavedSelection(): CustomerSheetDataResult { - throw NotImplementedError("Not implemented yet!") + return withContext(workContext) { + createPrefsRepository().mapCatching { prefsRepository -> + prefsRepository.getSavedSelection( + /* + * We don't calculate on `Google Pay` availability in this function. Instead, we check + * within `CustomerSheet` similar to how we check if a saved payment option is still exists + * within the user's payment methods from `retrievePaymentMethods` + */ + isGooglePayAvailable = true, + isLinkAvailable = false, + ) + } + } } override suspend fun setSavedSelection(selection: SavedSelection?): CustomerSheetDataResult { - throw NotImplementedError("Not implemented yet!") + return withContext(workContext) { + createPrefsRepository().mapCatching { prefsRepository -> + val result = prefsRepository.setSavedSelection(selection) + + if (!result) { + throw IOException("Unable to persist payment option $selection") + } + } + } + } + + private suspend fun createPrefsRepository(): CustomerSheetDataResult { + return elementsSessionManager.fetchCustomerSessionEphemeralKey().mapCatching { ephemeralKey -> + prefsRepositoryFactory(ephemeralKey.customerId) + }.toCustomerSheetDataResult() } } diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/injection/CustomerSessionDataSourceModule.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/injection/CustomerSessionDataSourceModule.kt index 51d8b456404..084ad8260a6 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/data/injection/CustomerSessionDataSourceModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/data/injection/CustomerSessionDataSourceModule.kt @@ -1,5 +1,8 @@ package com.stripe.android.customersheet.data.injection +import android.content.Context +import com.stripe.android.core.injection.IOContext +import com.stripe.android.customersheet.data.CustomerSessionElementsSessionManager import com.stripe.android.customersheet.data.CustomerSessionInitializationDataSource import com.stripe.android.customersheet.data.CustomerSessionIntentDataSource import com.stripe.android.customersheet.data.CustomerSessionPaymentMethodDataSource @@ -8,8 +11,13 @@ import com.stripe.android.customersheet.data.CustomerSheetInitializationDataSour import com.stripe.android.customersheet.data.CustomerSheetIntentDataSource import com.stripe.android.customersheet.data.CustomerSheetPaymentMethodDataSource import com.stripe.android.customersheet.data.CustomerSheetSavedSelectionDataSource +import com.stripe.android.customersheet.data.DefaultCustomerSessionElementsSessionManager +import com.stripe.android.paymentsheet.DefaultPrefsRepository +import com.stripe.android.paymentsheet.PrefsRepository import dagger.Binds import dagger.Module +import dagger.Provides +import kotlin.coroutines.CoroutineContext @Module internal interface CustomerSessionDataSourceModule { @@ -32,4 +40,23 @@ internal interface CustomerSessionDataSourceModule { fun bindsCustomerSheetInitializationDataSource( impl: CustomerSessionInitializationDataSource ): CustomerSheetInitializationDataSource + + @Binds + fun bindsCustomerSessionElementsSessionManager( + impl: DefaultCustomerSessionElementsSessionManager + ): CustomerSessionElementsSessionManager + + companion object { + @Provides + fun providePrefsRepositoryFactory( + appContext: Context, + @IOContext workContext: CoroutineContext + ): (String) -> PrefsRepository = { customerId -> + DefaultPrefsRepository( + appContext, + customerId, + workContext + ) + } + } } diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/CustomerSheetDataCommonModule.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/CustomerSheetDataCommonModule.kt index 5ef55d6dd09..5674b63b383 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/CustomerSheetDataCommonModule.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/CustomerSheetDataCommonModule.kt @@ -15,6 +15,7 @@ import com.stripe.android.payments.core.injection.PRODUCT_USAGE import dagger.Binds import dagger.Module import dagger.Provides +import java.util.Calendar import javax.inject.Named import javax.inject.Provider @@ -68,5 +69,10 @@ internal interface CustomerSheetDataCommonModule { publishableKeyProvider = { paymentConfiguration.get().publishableKey }, networkTypeProvider = NetworkTypeDetector(context)::invoke, ) + + @Provides + fun provideTimeProvider(): () -> Long = { + Calendar.getInstance().timeInMillis + } } } diff --git a/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/StripeCustomerAdapterComponent.kt b/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/StripeCustomerAdapterComponent.kt index b41fdc7a834..72780d394f7 100644 --- a/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/StripeCustomerAdapterComponent.kt +++ b/paymentsheet/src/main/java/com/stripe/android/customersheet/injection/StripeCustomerAdapterComponent.kt @@ -19,7 +19,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import java.util.Calendar import javax.inject.Singleton import kotlin.coroutines.CoroutineContext @@ -68,11 +67,6 @@ internal interface StripeCustomerAdapterModule { fun bindsCustomerRepository(repository: CustomerApiRepository): CustomerRepository companion object { - @Provides - fun provideTimeProvider(): () -> Long = { - Calendar.getInstance().timeInMillis - } - @Provides fun providePrefsRepositoryFactory( appContext: Context, diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSourceTest.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSourceTest.kt new file mode 100644 index 00000000000..ad24deac3ea --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSourceTest.kt @@ -0,0 +1,112 @@ +package com.stripe.android.customersheet.data + +import com.google.common.truth.Truth.assertThat +import com.stripe.android.isInstanceOf +import com.stripe.android.paymentsheet.FakePrefsRepository +import com.stripe.android.paymentsheet.PrefsRepository +import com.stripe.android.paymentsheet.model.SavedSelection +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.coroutines.coroutineContext + +class CustomerSessionSavedSelectionDataSourceTest { + @Test + fun `on fetch saved selection, should get selection from prefs repository`() = runTest { + val prefsRepository = FakePrefsRepository().apply { + setSavedSelection(SavedSelection.GooglePay) + } + + val dataSource = createDataSource( + prefsRepository = prefsRepository + ) + + val result = dataSource.retrieveSavedSelection() + + assertThat(result).isInstanceOf>() + + val successResult = result.asSuccess() + + assertThat(successResult.value).isEqualTo(SavedSelection.GooglePay) + } + + @Test + fun `on failed to fetch ephemeral key, should fail to get selection from prefs repository`() = runTest { + val exception = IllegalStateException("Failed to load!") + + val elementsSessionManager = FakeCustomerSessionElementsSessionManager( + ephemeralKey = Result.failure(exception) + ) + val dataSource = createDataSource( + elementsSessionManager = elementsSessionManager, + ) + + val result = dataSource.retrieveSavedSelection() + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + + @Test + fun `on set saved selection, should set selection in prefs repository`() = runTest { + val prefsRepository = FakePrefsRepository() + + val dataSource = createDataSource( + prefsRepository = prefsRepository + ) + + val result = dataSource.setSavedSelection(SavedSelection.PaymentMethod(id = "pm_1")) + + assertThat(result).isInstanceOf>() + + val savedSelection = prefsRepository.getSavedSelection( + isGooglePayAvailable = false, + isLinkAvailable = false + ) + + assertThat(savedSelection).isEqualTo(SavedSelection.PaymentMethod(id = "pm_1")) + } + + @Test + fun `on failed to fetch ephemeral key, should fail to set selection in prefs repository`() = runTest { + val exception = IllegalStateException("Failed to load!") + + val elementsSessionManager = FakeCustomerSessionElementsSessionManager( + ephemeralKey = Result.failure(exception) + ) + val dataSource = createDataSource( + elementsSessionManager = elementsSessionManager, + ) + + val result = dataSource.setSavedSelection(SavedSelection.PaymentMethod(id = "pm_1")) + + assertThat(result).isInstanceOf>() + + val failedResult = result.asFailure() + + assertThat(failedResult.cause).isEqualTo(exception) + } + + private suspend fun createDataSource( + elementsSessionManager: CustomerSessionElementsSessionManager = FakeCustomerSessionElementsSessionManager(), + prefsRepository: PrefsRepository = FakePrefsRepository(), + ): CustomerSheetSavedSelectionDataSource { + return CustomerSessionSavedSelectionDataSource( + elementsSessionManager = elementsSessionManager, + prefsRepositoryFactory = { + prefsRepository + }, + workContext = coroutineContext + ) + } + + private fun CustomerSheetDataResult.asSuccess(): CustomerSheetDataResult.Success { + return this as CustomerSheetDataResult.Success + } + + private fun CustomerSheetDataResult.asFailure(): CustomerSheetDataResult.Failure { + return this as CustomerSheetDataResult.Failure + } +} 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 new file mode 100644 index 00000000000..452b1ba5d3d --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/DefaultCustomerSessionElementsSessionManagerTest.kt @@ -0,0 +1,327 @@ +package com.stripe.android.customersheet.data + +import com.google.common.truth.Truth.assertThat +import com.stripe.android.customersheet.CustomerSheet +import com.stripe.android.customersheet.ExperimentalCustomerSheetApi +import com.stripe.android.customersheet.utils.FakeCustomerSessionProvider +import com.stripe.android.isInstanceOf +import com.stripe.android.model.ElementsSession +import com.stripe.android.paymentsheet.ExperimentalCustomerSessionApi +import com.stripe.android.paymentsheet.FakePrefsRepository +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.model.SavedSelection +import com.stripe.android.paymentsheet.repositories.ElementsSessionRepository +import com.stripe.android.testing.PaymentIntentFactory +import com.stripe.android.utils.FakeElementsSessionRepository +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.coroutines.coroutineContext + +@OptIn(ExperimentalCustomerSheetApi::class, ExperimentalCustomerSessionApi::class) +class DefaultCustomerSessionElementsSessionManagerTest { + @Test + fun `on fetch elements session, should set parameters properly`() = runTest { + val elementsSessionRepository = FakeElementsSessionRepository( + stripeIntent = PaymentIntentFactory.create(), + error = null, + linkSettings = null, + ) + + val manager = createElementsSessionManager( + elementsSessionRepository = elementsSessionRepository, + savedSelection = SavedSelection.PaymentMethod(id = "pm_123"), + intentConfiguration = Result.success( + CustomerSheet.IntentConfiguration.Builder() + .paymentMethodTypes(paymentMethodTypes = listOf("card", "us_bank_account", "sepa_debit")) + .build() + ), + customerSessionClientSecret = Result.success( + CustomerSheet.CustomerSessionClientSecret.create( + customerId = "cus_1", + clientSecret = "cuss_123", + ) + ), + ) + + manager.fetchElementsSession() + + val lastParams = elementsSessionRepository.lastParams + + assertThat(lastParams?.defaultPaymentMethodId).isEqualTo("pm_123") + assertThat(lastParams?.externalPaymentMethods).isEmpty() + + val initializationMode = lastParams?.initializationMode + assertThat(initializationMode).isInstanceOf(PaymentSheet.InitializationMode.DeferredIntent::class.java) + + val intentConfiguration = initializationMode.asDeferred().intentConfiguration + assertThat(intentConfiguration.paymentMethodTypes).containsExactly( + "card", + "us_bank_account", + "sepa_debit" + ) + assertThat(intentConfiguration.mode).isInstanceOf() + + val customer = lastParams?.customer + assertThat(customer?.id).isEqualTo("cus_1") + assertThat(customer?.accessType) + .isEqualTo(PaymentSheet.CustomerAccessType.CustomerSession(customerSessionClientSecret = "cuss_123")) + } + + @Test + fun `on fetch elements session, should fail if failed to fetch customer session client secret`() = runTest { + val exception = IllegalStateException("Failed!") + + val manager = createElementsSessionManager(customerSessionClientSecret = Result.failure(exception)) + + val result = manager.fetchElementsSession() + + assertThat(result).isEqualTo(Result.failure(exception)) + } + + @Test + fun `on fetch elements session, should fail if failed to fetch intent configuration`() = runTest { + val exception = IllegalStateException("Failed!") + + val manager = createElementsSessionManager(intentConfiguration = Result.failure(exception)) + + val result = manager.fetchElementsSession() + + assertThat(result).isEqualTo(Result.failure(exception)) + } + + @Test + fun `on multiple elements session calls, should only fetch intent configuration successfully once`() = runTest { + var amountOfCalls = 0 + + val manager = createElementsSessionManager( + onIntentConfiguration = { + amountOfCalls++ + + Result.success( + CustomerSheet.IntentConfiguration.Builder() + .paymentMethodTypes(listOf("card", "us_bank_account")) + .build() + ) + } + ) + + manager.fetchElementsSession() + manager.fetchElementsSession() + manager.fetchElementsSession() + manager.fetchElementsSession() + manager.fetchElementsSession() + + assertThat(amountOfCalls).isEqualTo(1) + } + + @Test + fun `on fetch ephemeral key, should fetch from elements session`() = runTest { + val manager = createElementsSessionManager( + elementsSessionRepository = FakeElementsSessionRepository( + stripeIntent = PaymentIntentFactory.create(), + error = null, + linkSettings = null, + sessionsCustomer = ElementsSession.Customer( + paymentMethods = listOf(), + defaultPaymentMethod = null, + session = ElementsSession.Customer.Session( + id = "cuss_123", + liveMode = true, + apiKey = "ek_123", + apiKeyExpiry = 999999, + customerId = "cus_1", + components = ElementsSession.Customer.Components( + mobilePaymentElement = ElementsSession.Customer.Components.MobilePaymentElement.Disabled, + customerSheet = ElementsSession.Customer.Components.CustomerSheet.Disabled, + ) + ) + ), + ) + ) + + val result = manager.fetchCustomerSessionEphemeralKey() + + assertThat(result).isEqualTo( + Result.success( + CachedCustomerEphemeralKey( + customerId = "cus_1", + ephemeralKey = "ek_123", + expiresAt = 999999, + ) + ) + ) + } + + @Test + fun `on ephemeral key, should fetch from elements session`() = runTest { + val manager = createElementsSessionManagerWithCustomer( + apiKey = "ek_123", + apiKeyExpiry = 999999, + customerId = "cus_1", + ) + + val result = manager.fetchCustomerSessionEphemeralKey() + + assertThat(result).isEqualTo( + Result.success( + CachedCustomerEphemeralKey( + customerId = "cus_1", + ephemeralKey = "ek_123", + expiresAt = 999999, + ) + ) + ) + } + + @Test + fun `on ephemeral key, should re-use previously fetched key if not expired`() = runTest { + var amountOfCalls = 0 + + val manager = createElementsSessionManagerWithCustomer( + apiKey = "ek_123", + apiKeyExpiry = 999999, + customerId = "cus_1", + 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(1) + assertThat(lastResult).isEqualTo( + Result.success( + CachedCustomerEphemeralKey( + customerId = "cus_1", + ephemeralKey = "ek_123", + expiresAt = 999999, + ) + ) + ) + } + + @Test + fun `on ephemeral key while fetching elements, should wait and use teh`() = runTest { + var amountOfCalls = 0 + + val manager = createElementsSessionManagerWithCustomer( + apiKey = "ek_123", + apiKeyExpiry = 999999, + customerId = "cus_1", + currentTime = 999699000, + onCustomerSessionClientSecret = { + amountOfCalls++ + + Result.success( + CustomerSheet.CustomerSessionClientSecret.create( + customerId = "cus_1", + clientSecret = "cuss_123", + ) + ) + } + ) + + manager.fetchCustomerSessionEphemeralKey() + manager.fetchCustomerSessionEphemeralKey() + manager.fetchCustomerSessionEphemeralKey() + manager.fetchCustomerSessionEphemeralKey() + + assertThat(amountOfCalls).isEqualTo(4) + } + + private suspend fun createElementsSessionManagerWithCustomer( + customerId: String = "cus_1", + apiKey: String = "ek_123", + apiKeyExpiry: Int = 999999, + currentTime: Long = 10, + onCustomerSessionClientSecret: () -> Result = { + Result.success( + CustomerSheet.CustomerSessionClientSecret.create( + customerId = "cus_1", + clientSecret = "cuss_123", + ) + ) + }, + ): CustomerSessionElementsSessionManager { + return createElementsSessionManager( + elementsSessionRepository = FakeElementsSessionRepository( + stripeIntent = PaymentIntentFactory.create(), + error = null, + linkSettings = null, + sessionsCustomer = ElementsSession.Customer( + paymentMethods = listOf(), + defaultPaymentMethod = null, + session = ElementsSession.Customer.Session( + id = "cuss_123", + liveMode = true, + apiKey = apiKey, + apiKeyExpiry = apiKeyExpiry, + customerId = customerId, + components = ElementsSession.Customer.Components( + mobilePaymentElement = ElementsSession.Customer.Components.MobilePaymentElement.Disabled, + customerSheet = ElementsSession.Customer.Components.CustomerSheet.Disabled, + ) + ) + ), + ), + onCustomerSessionClientSecret = onCustomerSessionClientSecret, + timeProvider = { currentTime } + ) + } + + private suspend fun createElementsSessionManager( + elementsSessionRepository: ElementsSessionRepository = + FakeElementsSessionRepository( + stripeIntent = PaymentIntentFactory.create(), + error = null, + linkSettings = null, + ), + intentConfiguration: Result = + Result.success(CustomerSheet.IntentConfiguration.Builder().build()), + onIntentConfiguration: () -> Result = { intentConfiguration }, + customerSessionClientSecret: Result = + Result.success( + CustomerSheet.CustomerSessionClientSecret.create( + customerId = "cus_1", + clientSecret = "cuss_123", + ) + ), + onCustomerSessionClientSecret: () -> Result = { + customerSessionClientSecret + }, + savedSelection: SavedSelection? = null, + timeProvider: () -> Long = { + 10 + } + ): CustomerSessionElementsSessionManager { + return DefaultCustomerSessionElementsSessionManager( + elementsSessionRepository = elementsSessionRepository, + prefsRepositoryFactory = { + FakePrefsRepository().apply { + setSavedSelection(savedSelection = savedSelection) + } + }, + customerSessionProvider = FakeCustomerSessionProvider( + onIntentConfiguration = onIntentConfiguration, + onProvidesCustomerSessionClientSecret = onCustomerSessionClientSecret, + ), + timeProvider = timeProvider, + workContext = coroutineContext, + ) + } + + private fun PaymentSheet.InitializationMode?.asDeferred(): PaymentSheet.InitializationMode.DeferredIntent { + return this as PaymentSheet.InitializationMode.DeferredIntent + } +} 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 new file mode 100644 index 00000000000..234ea24da69 --- /dev/null +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/data/FakeCustomerSessionElementsSessionManager.kt @@ -0,0 +1,21 @@ +package com.stripe.android.customersheet.data + +import com.stripe.android.model.ElementsSession + +internal class FakeCustomerSessionElementsSessionManager( + private val ephemeralKey: Result = Result.success( + CachedCustomerEphemeralKey( + customerId = "cus_1", + ephemeralKey = "ek_123", + expiresAt = 999999, + ) + ), +) : CustomerSessionElementsSessionManager { + override suspend fun fetchCustomerSessionEphemeralKey(): Result { + return ephemeralKey + } + + override suspend fun fetchElementsSession(): Result { + throw NotImplementedError("Not implemented yet!") + } +} diff --git a/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/FakeCustomerSessionProvider.kt b/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/FakeCustomerSessionProvider.kt index 93b65b35f4d..ac9204489b5 100644 --- a/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/FakeCustomerSessionProvider.kt +++ b/paymentsheet/src/test/java/com/stripe/android/customersheet/utils/FakeCustomerSessionProvider.kt @@ -5,12 +5,26 @@ import com.stripe.android.customersheet.ExperimentalCustomerSheetApi import com.stripe.android.paymentsheet.ExperimentalCustomerSessionApi @OptIn(ExperimentalCustomerSheetApi::class, ExperimentalCustomerSessionApi::class) -class FakeCustomerSessionProvider : CustomerSheet.CustomerSessionProvider() { - override suspend fun provideSetupIntentClientSecret(customerId: String): Result { +class FakeCustomerSessionProvider( + private val onIntentConfiguration: () -> Result = { + throw NotImplementedError("Not implemented yet!") + }, + private val onProvideSetupIntentClientSecret: (String) -> Result = { throw NotImplementedError("Not implemented yet!") + }, + private val onProvidesCustomerSessionClientSecret: () -> Result = { + throw NotImplementedError("Not implemented yet!") + } +) : CustomerSheet.CustomerSessionProvider() { + override suspend fun intentConfiguration(): Result { + return onIntentConfiguration() + } + + override suspend fun provideSetupIntentClientSecret(customerId: String): Result { + return onProvideSetupIntentClientSecret(customerId) } override suspend fun providesCustomerSessionClientSecret(): Result { - throw NotImplementedError("Not implemented yet!") + return onProvidesCustomerSessionClientSecret() } }