Skip to content

Commit

Permalink
Add initial CustomerSheet data sources & use in CustomerSheet
Browse files Browse the repository at this point in the history
  • Loading branch information
samer-stripe committed Sep 18, 2024
1 parent c8bcb35 commit d31e470
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 7 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,20 @@ class CustomerSheet internal constructor(
)

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

val selectedPaymentOptionDeferred = async {
adapter.retrieveSelectedPaymentOption()
val savedSelectionDeferred = async {
dataSource.retrieveSavedSelection().toResult()
}
val paymentMethodsDeferred = async {
adapter.retrievePaymentMethods()
dataSource.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 +156,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
@@ -0,0 +1,23 @@
package com.stripe.android.customersheet.data

import com.stripe.android.customersheet.CustomerAdapter
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
import com.stripe.android.customersheet.map
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.model.SavedSelection
import javax.inject.Inject

@OptIn(ExperimentalCustomerSheetApi::class)
internal class CustomerAdapterDataSource @Inject constructor(
private val customerAdapter: CustomerAdapter,
) : CustomerSheetCombinedDataSource {
override suspend fun retrievePaymentMethods(): CustomerSheetDataResult<List<PaymentMethod>> {
return customerAdapter.retrievePaymentMethods().toCustomerSheetDataResult()
}

override suspend fun retrieveSavedSelection(): CustomerSheetDataResult<SavedSelection?> {
return customerAdapter.retrieveSelectedPaymentOption().map { result ->
result?.toSavedSelection()
}.toCustomerSheetDataResult()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.stripe.android.customersheet.data

internal interface CustomerSheetCombinedDataSource :
CustomerSheetPaymentMethodDataSource,
CustomerSheetSavedSelectionDataSource
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

/**
* [CustomerSheetPaymentMethodDataSource] defines a set of operations for managing saved payment methods within a
* [com.stripe.android.customersheet.CustomerSheet] context.
*/
internal interface CustomerSheetPaymentMethodDataSource {
/**
* Retrieves a list of payment methods
*/
suspend fun retrievePaymentMethods(): CustomerSheetDataResult<List<PaymentMethod>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.stripe.android.customersheet.data

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

internal interface CustomerSheetSavedSelectionDataSource {

suspend fun retrieveSavedSelection(): CustomerSheetDataResult<SavedSelection?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
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.CustomerSheetCombinedDataSource
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.flow.Flow
Expand All @@ -24,11 +26,16 @@ internal object CustomerSheetHacks {
val adapter: Deferred<CustomerAdapter>
get() = _adapter.asDeferred()

private val _dataSource = MutableStateFlow<CustomerSheetCombinedDataSource?>(null)
val dataSource: Deferred<CustomerSheetCombinedDataSource>
get() = _dataSource.asDeferred()

fun initialize(
lifecycleOwner: LifecycleOwner,
adapter: CustomerAdapter,
) {
_adapter.value = adapter
_dataSource.value = CustomerAdapterDataSource(adapter)

lifecycleOwner.lifecycle.addObserver(
object : DefaultLifecycleObserver {
Expand All @@ -51,6 +58,7 @@ internal object CustomerSheetHacks {

fun clear() {
_adapter.value = null
_dataSource.value = null
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.stripe.android.customersheet.data

import com.google.common.truth.Truth.assertThat
import com.stripe.android.customersheet.CustomerAdapter
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
import com.stripe.android.customersheet.FakeCustomerAdapter
import com.stripe.android.paymentsheet.model.SavedSelection
import com.stripe.android.testing.PaymentMethodFactory
import kotlinx.coroutines.test.runTest
import kotlin.test.Test

@OptIn(ExperimentalCustomerSheetApi::class)
class CustomerAdapterDataSourceTest {
@Test
fun `on retrieve payment methods, should completely successfully from adapter`() = runTest {
val paymentMethods = PaymentMethodFactory.cards(size = 6)
val dataSource = createCustomerAdapterDataSource(
adapter = FakeCustomerAdapter(
paymentMethods = CustomerAdapter.Result.success(paymentMethods),
),
)

val successResult = dataSource.retrievePaymentMethods().asSuccess()

assertThat(successResult.value).containsExactlyElementsIn(paymentMethods)
}

@Test
fun `on retrieve payment methods, should fail from adapter`() = runTest {
val dataSource = createCustomerAdapterDataSource(
adapter = FakeCustomerAdapter(
paymentMethods = CustomerAdapter.Result.failure(
cause = IllegalStateException("Failed to retrieve!"),
displayMessage = "Something went wrong!",
),
),
)

val failedResult = dataSource.retrievePaymentMethods().asFailure()

assertThat(failedResult.cause).isInstanceOf(IllegalStateException::class.java)
assertThat(failedResult.cause.message).isEqualTo("Failed to retrieve!")
assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!")
}

@Test
fun `on retrieve payment option, should completely successfully from adapter`() = runTest {
val paymentOptionId = "pm_1"
val dataSource = createCustomerAdapterDataSource(
adapter = FakeCustomerAdapter(
selectedPaymentOption = CustomerAdapter.Result.success(
value = CustomerAdapter.PaymentOption.fromId(paymentOptionId),
),
),
)

val successResult = dataSource.retrieveSavedSelection().asSuccess()

assertThat(successResult.value).isEqualTo(SavedSelection.PaymentMethod(paymentOptionId))
}

@Test
fun `on retrieve payment option, should fail from adapter`() = runTest {
val dataSource = createCustomerAdapterDataSource(
adapter = FakeCustomerAdapter(
selectedPaymentOption = CustomerAdapter.Result.failure(
cause = IllegalStateException("Failed to retrieve!"),
displayMessage = "Something went wrong!",
)
)
)

val failedResult = dataSource.retrieveSavedSelection().asFailure()

assertThat(failedResult.cause).isInstanceOf(IllegalStateException::class.java)
assertThat(failedResult.cause.message).isEqualTo("Failed to retrieve!")
assertThat(failedResult.displayMessage).isEqualTo("Something went wrong!")
}

private fun createCustomerAdapterDataSource(
adapter: CustomerAdapter = FakeCustomerAdapter(),
): CustomerAdapterDataSource {
return CustomerAdapterDataSource(
customerAdapter = adapter,
)
}

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>
}
}

0 comments on commit d31e470

Please sign in to comment.