-
Notifications
You must be signed in to change notification settings - Fork 662
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
CustomerSessionSavedSelectionDataSource
and backing `CustomerSe…
…ssionElementsSessionManager`
- Loading branch information
1 parent
9253071
commit 1c675ff
Showing
10 changed files
with
662 additions
and
12 deletions.
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
...ntsheet/src/main/java/com/stripe/android/customersheet/data/CachedCustomerEphemeralKey.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} |
98 changes: 98 additions & 0 deletions
98
.../main/java/com/stripe/android/customersheet/data/CustomerSessionElementsSessionManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CachedCustomerEphemeralKey> | ||
|
||
suspend fun fetchElementsSession(): Result<ElementsSession> | ||
} | ||
|
||
@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<CachedCustomerEphemeralKey> { | ||
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<ElementsSession> { | ||
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() | ||
} | ||
} | ||
} | ||
} |
41 changes: 38 additions & 3 deletions
41
...ain/java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSource.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SavedSelection?> { | ||
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<Unit> { | ||
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<PrefsRepository> { | ||
return elementsSessionManager.fetchCustomerSessionEphemeralKey().mapCatching { ephemeralKey -> | ||
prefsRepositoryFactory(ephemeralKey.customerId) | ||
}.toCustomerSheetDataResult() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
...java/com/stripe/android/customersheet/data/CustomerSessionSavedSelectionDataSourceTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CustomerSheetDataResult.Success<SavedSelection?>>() | ||
|
||
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<CustomerSheetDataResult.Failure<SavedSelection?>>() | ||
|
||
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<CustomerSheetDataResult.Success<Unit>>() | ||
|
||
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<CustomerSheetDataResult.Failure<Unit>>() | ||
|
||
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 <T> CustomerSheetDataResult<T>.asSuccess(): CustomerSheetDataResult.Success<T> { | ||
return this as CustomerSheetDataResult.Success<T> | ||
} | ||
|
||
private fun <T> CustomerSheetDataResult<T>.asFailure(): CustomerSheetDataResult.Failure<T> { | ||
return this as CustomerSheetDataResult.Failure<T> | ||
} | ||
} |
Oops, something went wrong.