Skip to content

Commit

Permalink
Add CustomerSheetDataSource & CustomerAdapterDataSource.
Browse files Browse the repository at this point in the history
  • Loading branch information
samer-stripe committed Sep 16, 2024
1 parent 76ddab6 commit d97d4a4
Show file tree
Hide file tree
Showing 13 changed files with 377 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import com.stripe.android.ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi
import com.stripe.android.common.configuration.ConfigurationDefaults
import com.stripe.android.customersheet.CustomerAdapter.PaymentOption.Companion.toPaymentOption
import com.stripe.android.customersheet.util.CustomerSheetHacks
import com.stripe.android.model.CardBrand
import com.stripe.android.paymentsheet.PaymentSheet
Expand Down Expand Up @@ -130,18 +131,18 @@ class CustomerSheet internal constructor(
)

return coroutineScope {
val adapter = CustomerSheetHacks.adapter.await()

val selectedPaymentOptionDeferred = async {
adapter.retrieveSelectedPaymentOption()
val savedSelectionDeferred = async {
viewModel.customerSheetDataSource.retrieveSavedSelection().toResult()
}
val paymentMethodsDeferred = async {
adapter.retrievePaymentMethods()
viewModel.customerSheetDataSource.retrievePaymentMethods().toResult()
}
val selectedPaymentOption = selectedPaymentOptionDeferred.await()
val savedSelection = savedSelectionDeferred.await()
val paymentMethods = paymentMethodsDeferred.await()

val selection = selectedPaymentOption.mapCatching { paymentOption ->
val selection = savedSelection.map { selection ->
selection?.toPaymentOption()
}.mapCatching { paymentOption ->
paymentOption?.toPaymentSelection {
paymentMethods.getOrNull()?.find {
it.id == paymentOption.id
Expand All @@ -153,7 +154,7 @@ class CustomerSheet internal constructor(
onSuccess = {
CustomerSheetResult.Selected(it)
},
onFailure = { cause, _ ->
onFailure = { cause ->
CustomerSheetResult.Failed(cause)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package com.stripe.android.customersheet

import android.app.Application
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.CreationExtras
import com.stripe.android.core.utils.requireApplication
import com.stripe.android.customersheet.data.CustomerSheetDataSource
import com.stripe.android.customersheet.injection.DaggerCustomerSheetDataSourceComponent
import com.stripe.android.customersheet.util.CustomerSheetHacks

@OptIn(ExperimentalCustomerSheetApi::class)
internal class CustomerSheetConfigViewModel(
application: Application,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
var configureRequest: CustomerSheetConfigureRequest?
Expand All @@ -15,10 +22,17 @@ internal class CustomerSheetConfigViewModel(
}
get() = savedStateHandle[CUSTOMER_SHEET_CONFIGURE_REQUEST_KEY]

val customerSheetDataSource: CustomerSheetDataSource = DaggerCustomerSheetDataSourceComponent.builder()
.context(application)
.customerAdapterProvider(CustomerSheetHacks.adapter)
.build()
.customerSheetDataSource

object Factory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
@Suppress("UNCHECKED_CAST")
return CustomerSheetConfigViewModel(
application = extras.requireApplication(),
savedStateHandle = extras.createSavedStateHandle(),
) as T
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.stripe.android.core.exception.StripeException
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.injection.IS_LIVE_MODE
import com.stripe.android.customersheet.util.CustomerSheetHacks
import com.stripe.android.customersheet.util.awaitAsResult
import com.stripe.android.customersheet.util.sortPaymentMethods
import com.stripe.android.googlepaylauncher.GooglePayEnvironment
import com.stripe.android.googlepaylauncher.GooglePayRepository
Expand All @@ -22,11 +23,9 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

@OptIn(ExperimentalCustomerSheetApi::class)
Expand Down Expand Up @@ -238,15 +237,3 @@ private data class ElementsSessionWithMetadata(
val elementsSession: ElementsSession,
val metadata: PaymentMethodMetadata,
)

private suspend fun <T> Deferred<T>.awaitAsResult(
timeout: Duration,
error: () -> String,
): Result<T> {
val result = withTimeoutOrNull(timeout) { await() }
return if (result != null) {
Result.success(result)
} else {
Result.failure(IllegalStateException(error()))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.stripe.android.customersheet.data

import com.stripe.android.core.exception.StripeException
import com.stripe.android.customersheet.CustomerAdapter
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
import com.stripe.android.customersheet.map
import com.stripe.android.customersheet.util.awaitAsResult
import com.stripe.android.payments.core.analytics.ErrorReporter
import kotlinx.coroutines.Deferred
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

@OptIn(ExperimentalCustomerSheetApi::class)
internal class CustomerAdapterDataSource @Inject constructor(
private val customerAdapterProvider: Deferred<CustomerAdapter>,
private val errorReporter: ErrorReporter,
) : CustomerSheetDataSource {
override suspend fun retrievePaymentMethods() = withAdapter { adapter ->
adapter.retrievePaymentMethods().toCustomerSheetDataResult()
}

override suspend fun retrieveSavedSelection() = withAdapter { adapter ->
adapter.retrieveSelectedPaymentOption().map { result ->
result?.toSavedSelection()
}.toCustomerSheetDataResult()
}

private suspend fun <T> withAdapter(
task: suspend (CustomerAdapter) -> CustomerSheetDataResult<T>
): CustomerSheetDataResult<T> {
val adapter = retrieveCustomerAdapter().getOrElse {
return CustomerSheetDataResult.Failure(cause = it, displayMessage = null)
}

return task(adapter)
}

private suspend fun retrieveCustomerAdapter(): Result<CustomerAdapter> {
return customerAdapterProvider.awaitAsResult(
timeout = 5.seconds,
error = {
"Couldn't find an instance of CustomerAdapter. " +
"Are you instantiating CustomerSheet unconditionally in your app?"
},
).onFailure {
errorReporter.report(
errorEvent = ErrorReporter.ExpectedErrorEvent.CUSTOMER_SHEET_ADAPTER_NOT_FOUND,
stripeException = StripeException.create(it)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.stripe.android.customersheet.data

internal sealed interface CustomerSheetDataResult<T> {
data class Success<T>(val value: T) : CustomerSheetDataResult<T>

data class Failure<T>(
val cause: Throwable,
val displayMessage: String? = null
) : CustomerSheetDataResult<T>

fun toResult(): Result<T> {
return when (this) {
is Success -> Result.success(value)
is Failure -> Result.failure(cause)
}
}

companion object {
fun <T> success(value: T): Success<T> {
return Success(value)
}

fun <T> failure(cause: Throwable, displayMessage: String?): Failure<T> {
return Failure(cause = cause, displayMessage = displayMessage)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.stripe.android.customersheet.data

import com.stripe.android.customersheet.CustomerAdapter
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi

@OptIn(ExperimentalCustomerSheetApi::class)
internal fun <T> CustomerAdapter.Result<T>.toCustomerSheetDataResult(): CustomerSheetDataResult<T> {
return when (this) {
is CustomerAdapter.Result.Success -> CustomerSheetDataResult.success(value)
is CustomerAdapter.Result.Failure -> CustomerSheetDataResult.failure(
cause = cause,
displayMessage = displayMessage,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.stripe.android.customersheet.data

import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.model.SavedSelection

/**
* [CustomerSheetDataSource] defines a set of operations used by [com.stripe.android.customersheet.CustomerSheet] to
* retrieve the data necessary to perform its actions.
*/
internal interface CustomerSheetDataSource {
suspend fun retrievePaymentMethods(): CustomerSheetDataResult<List<PaymentMethod>>

suspend fun retrieveSavedSelection(): CustomerSheetDataResult<SavedSelection?>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.stripe.android.customersheet.injection

import android.content.Context
import com.stripe.android.core.injection.CoreCommonModule
import com.stripe.android.core.injection.CoroutineContextModule
import com.stripe.android.customersheet.CustomerAdapter
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
import com.stripe.android.customersheet.data.CustomerSheetDataSource
import com.stripe.android.payments.core.injection.StripeRepositoryModule
import dagger.BindsInstance
import dagger.Component
import kotlinx.coroutines.Deferred
import javax.inject.Singleton

@Singleton
@Component(
modules = [
CustomerDataCommonModule::class,
CustomerSheetDataSourceModule::class,
StripeRepositoryModule::class,
CoroutineContextModule::class,
CoreCommonModule::class,
]
)
@OptIn(ExperimentalCustomerSheetApi::class)
internal interface CustomerSheetDataSourceComponent {
val customerSheetDataSource: CustomerSheetDataSource

@Component.Builder
interface Builder {
@BindsInstance
fun context(context: Context): Builder

@BindsInstance
fun customerAdapterProvider(provider: Deferred<CustomerAdapter>): Builder

fun build(): CustomerSheetDataSourceComponent
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.stripe.android.customersheet.injection

import com.stripe.android.customersheet.CustomerAdapter
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
import com.stripe.android.customersheet.data.CustomerAdapterDataSource
import com.stripe.android.customersheet.data.CustomerSheetDataSource
import com.stripe.android.payments.core.analytics.ErrorReporter
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.Deferred

@OptIn(ExperimentalCustomerSheetApi::class)
@Module
internal class CustomerSheetDataSourceModule {
@Provides
fun providesCustomerSheetDataSource(
customerAdapterProvider: Deferred<CustomerAdapter>,
errorReporter: ErrorReporter,
): CustomerSheetDataSource {
return CustomerAdapterDataSource(
customerAdapterProvider = customerAdapterProvider,
errorReporter = errorReporter,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import kotlin.coroutines.CoroutineContext
@Singleton
@Component(
modules = [
StripeCustomerAdapterModule::class,
CustomerDataCommonModule::class,
StripeRepositoryModule::class,
CoroutineContextModule::class,
CoreCommonModule::class,
Expand Down Expand Up @@ -75,7 +75,7 @@ internal interface StripeCustomerAdapterComponent {

@Module
@OptIn(ExperimentalCustomerSheetApi::class)
internal interface StripeCustomerAdapterModule {
internal interface CustomerDataCommonModule {
@Binds
fun bindsCustomerRepository(repository: CustomerApiRepository): CustomerRepository

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package com.stripe.android.customersheet.util

import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.model.PaymentSelection
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration

internal fun sortPaymentMethods(
paymentMethods: List<PaymentMethod>,
Expand All @@ -23,3 +26,15 @@ internal fun sortPaymentMethods(
}
} ?: paymentMethods
}

internal suspend fun <T> Deferred<T>.awaitAsResult(
timeout: Duration,
error: () -> String,
): Result<T> {
val result = withTimeoutOrNull(timeout) { await() }
return if (result != null) {
Result.success(result)
} else {
Result.failure(IllegalStateException(error()))
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package com.stripe.android.customersheet

import android.app.Application
import androidx.lifecycle.SavedStateHandle
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import kotlin.test.Test

@OptIn(ExperimentalCustomerSheetApi::class)
@RunWith(RobolectricTestRunner::class)
class CustomerSheetConfigViewModelTest {
private val application = ApplicationProvider.getApplicationContext<Application>()

@Test
fun `On set configure request, should be able to retrieve the same request`() {
val viewModel = CustomerSheetConfigViewModel(
application = application,
savedStateHandle = SavedStateHandle(),
)

Expand All @@ -29,6 +37,7 @@ class CustomerSheetConfigViewModelTest {
fun `On init with 'SavedStateHandle', should be able to retrieve the saved request`() {
val handle = SavedStateHandle()
val viewModel = CustomerSheetConfigViewModel(
application = application,
savedStateHandle = handle,
)

Expand All @@ -43,6 +52,7 @@ class CustomerSheetConfigViewModelTest {
viewModel.configureRequest = request

val recreatedViewModel = CustomerSheetConfigViewModel(
application = application,
savedStateHandle = handle
)

Expand Down
Loading

0 comments on commit d97d4a4

Please sign in to comment.