Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial CustomerSheet data sources & use in CustomerSheet #9278

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
CustomerSheetHacks.savedSelectionDataSource.await().retrieveSavedSelection().toResult()
}
val paymentMethodsDeferred = async {
adapter.retrievePaymentMethods()
CustomerSheetHacks.paymentMethodDataSource.await().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)
}
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes here are covered by the pre-existing CustomerSheet tests we added recently.

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,
) : CustomerSheetSavedSelectionDataSource, CustomerSheetPaymentMethodDataSource {
samer-stripe marked this conversation as resolved.
Show resolved Hide resolved
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,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> {
samer-stripe marked this conversation as resolved.
Show resolved Hide resolved
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,16 @@
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
*
* @return a result containing the list of payment methods if operation was successful
*/
suspend fun retrievePaymentMethods(): CustomerSheetDataResult<List<PaymentMethod>>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.stripe.android.customersheet.data

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

/**
* [CustomerSheetSavedSelectionDataSource] defines a set of operations for managing saved payment selections within a
* [com.stripe.android.customersheet.CustomerSheet] context.
*/
internal interface CustomerSheetSavedSelectionDataSource {
/**
* Retrieves a saved selection
*
* @return a result containing the saved selection if operation was successful
*/
suspend fun retrieveSavedSelection(): CustomerSheetDataResult<SavedSelection?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ 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.CustomerSheetPaymentMethodDataSource
import com.stripe.android.customersheet.data.CustomerSheetSavedSelectionDataSource
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.flow.Flow
Expand All @@ -24,11 +27,20 @@ internal object CustomerSheetHacks {
val adapter: Deferred<CustomerAdapter>
get() = _adapter.asDeferred()

private val _dataSource = MutableStateFlow<CombinedDataSource<*>?>(null)

val paymentMethodDataSource: Deferred<CustomerSheetPaymentMethodDataSource>
get() = _dataSource.asDeferred()

val savedSelectionDataSource: Deferred<CustomerSheetSavedSelectionDataSource>
get() = _dataSource.asDeferred()

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

lifecycleOwner.lifecycle.addObserver(
object : DefaultLifecycleObserver {
Expand All @@ -49,8 +61,15 @@ internal object CustomerSheetHacks {
)
}

private class CombinedDataSource<T>(dataSource: T) :
CustomerSheetSavedSelectionDataSource by dataSource,
CustomerSheetPaymentMethodDataSource by dataSource
where T : CustomerSheetSavedSelectionDataSource,
T : CustomerSheetPaymentMethodDataSource

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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.isInstanceOf
import com.stripe.android.model.PaymentMethod
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 complete successfully from adapter`() = runTest {
val paymentMethods = PaymentMethodFactory.cards(size = 6)
val dataSource = createCustomerAdapterDataSource(
adapter = FakeCustomerAdapter(
paymentMethods = CustomerAdapter.Result.success(paymentMethods),
),
)

val result = dataSource.retrievePaymentMethods()

assertThat(result).isInstanceOf<CustomerSheetDataResult.Success<List<PaymentMethod>>>()

val successResult = result.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 result = dataSource.retrievePaymentMethods()

assertThat(result).isInstanceOf<CustomerSheetDataResult.Failure<List<PaymentMethod>>>()

val failedResult = result.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 complete successfully from adapter`() = runTest {
val paymentOptionId = "pm_1"
val dataSource = createCustomerAdapterDataSource(
adapter = FakeCustomerAdapter(
selectedPaymentOption = CustomerAdapter.Result.success(
value = CustomerAdapter.PaymentOption.fromId(paymentOptionId),
),
),
)

val result = dataSource.retrieveSavedSelection()

assertThat(result).isInstanceOf<CustomerSheetDataResult.Success<SavedSelection?>>()

val successResult = result.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 result = dataSource.retrieveSavedSelection()

assertThat(result).isInstanceOf<CustomerSheetDataResult.Failure<SavedSelection?>>()

val failedResult = result.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>
}
}
Loading