Skip to content

Commit

Permalink
Add CustomerSheet playground UI (#8524)
Browse files Browse the repository at this point in the history
* Add `CustomerSheet` playground UI

* Move `rememberCustomerSheet` to `onCreate` & properly fetch option.
  • Loading branch information
samer-stripe committed May 23, 2024
1 parent 6f7a522 commit 33efc82
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.stripe.android.paymentsheet.example.playground

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import com.stripe.android.paymentsheet.model.PaymentOption

internal data class CustomerSheetState(
val selectedPaymentOption: PaymentOption? = null,
val shouldFetchPaymentOption: Boolean = true
)

internal fun CustomerSheetState?.paymentMethodLabel(): String {
return this?.selectedPaymentOption?.label ?: "Select"
}

@Composable
internal fun CustomerSheetState?.paymentMethodPainter(): Painter? {
return this?.selectedPaymentOption?.iconPainter
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.stripe.android.customersheet.CustomerSheet
import com.stripe.android.customersheet.CustomerSheetResult
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
import com.stripe.android.customersheet.rememberCustomerSheet
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.ExternalPaymentMethodConfirmHandler
import com.stripe.android.paymentsheet.PaymentSheet
Expand All @@ -53,9 +57,12 @@ import com.stripe.android.paymentsheet.example.playground.settings.SettingsUi
import com.stripe.android.paymentsheet.example.samples.ui.shared.BuyButton
import com.stripe.android.paymentsheet.example.samples.ui.shared.CHECKOUT_TEST_TAG
import com.stripe.android.paymentsheet.example.samples.ui.shared.PaymentMethodSelector
import com.stripe.android.paymentsheet.model.PaymentOption
import com.stripe.android.paymentsheet.rememberPaymentSheet
import com.stripe.android.paymentsheet.rememberPaymentSheetFlowController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext

internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPaymentMethodConfirmHandler {
companion object {
Expand All @@ -74,6 +81,7 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay
)
}

@OptIn(ExperimentalCustomerSheetApi::class)
@Suppress("LongMethod")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -99,6 +107,14 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay

val playgroundState by viewModel.state.collectAsState()

val customerSheet = playgroundState?.asCustomerState()?.let { customerPlaygroundState ->
rememberCustomerSheet(
configuration = customerPlaygroundState.customerSheetConfiguration(),
customerAdapter = customerPlaygroundState.adapter,
callback = viewModel::onCustomerSheetCallback
)
}

PlaygroundTheme(
content = {
playgroundState?.asPaymentState()?.stripeIntentId?.let { stripeIntentId ->
Expand Down Expand Up @@ -128,6 +144,7 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay
playgroundState = playgroundState,
paymentSheet = paymentSheet,
flowController = flowController,
customerSheet = customerSheet,
addressLauncher = addressLauncher,
)
}
Expand Down Expand Up @@ -206,11 +223,13 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay
}
}

@OptIn(ExperimentalCustomerSheetApi::class)
@Composable
private fun PlaygroundStateUi(
playgroundState: PlaygroundState?,
paymentSheet: PaymentSheet,
flowController: PaymentSheet.FlowController,
customerSheet: CustomerSheet?,
addressLauncher: AddressLauncher
) {
if (playgroundState == null) {
Expand Down Expand Up @@ -241,8 +260,8 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay
else -> Unit
}
}
is PlaygroundState.Customer -> {
// TODO(samer-stripe): Implement Customer Sheet UI
is PlaygroundState.Customer -> customerSheet?.run {
CustomerSheetUi(customerSheet = this)
}
}
}
Expand Down Expand Up @@ -306,6 +325,46 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay
)
}

@OptIn(ExperimentalCustomerSheetApi::class)
@Composable
fun CustomerSheetUi(
customerSheet: CustomerSheet,
) {
val customerSheetState by viewModel.customerSheetState.collectAsState()

customerSheetState?.let { state ->
LaunchedEffect(state) {
if (state.shouldFetchPaymentOption) {
fetchOption(customerSheet).onSuccess { option ->
viewModel.customerSheetState.emit(
CustomerSheetState(
selectedPaymentOption = option,
shouldFetchPaymentOption = false
)
)
}.onFailure { exception ->
viewModel.status.emit(
StatusMessage(
message = "Failed to retrieve payment options:\n${exception.message}"
)
)
}
}
}

if (state.shouldFetchPaymentOption) {
return
}

PaymentMethodSelector(
isEnabled = true,
paymentMethodLabel = customerSheetState.paymentMethodLabel(),
paymentMethodPainter = customerSheetState.paymentMethodPainter(),
onClick = customerSheet::present
)
}
}

@Composable
private fun ShippingAddressButton(
addressLauncher: AddressLauncher,
Expand Down Expand Up @@ -382,6 +441,17 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay
}
}

@OptIn(ExperimentalCustomerSheetApi::class)
private suspend fun fetchOption(
customerSheet: CustomerSheet
): Result<PaymentOption?> = withContext(Dispatchers.IO) {
when (val result = customerSheet.retrievePaymentOptionSelection()) {
is CustomerSheetResult.Selected -> Result.success(result.selection?.paymentOption)
is CustomerSheetResult.Failed -> Result.failure(result.exception)
is CustomerSheetResult.Canceled -> Result.success(null)
}
}

override fun confirmExternalPaymentMethod(
externalPaymentMethodType: String,
billingDetails: PaymentMethod.BillingDetails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.github.kittinunf.result.Result
import com.stripe.android.PaymentConfiguration
import com.stripe.android.customersheet.CustomerAdapter
import com.stripe.android.customersheet.CustomerEphemeralKey
import com.stripe.android.customersheet.CustomerSheetResult
import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
import com.stripe.android.model.PaymentMethod
import com.stripe.android.paymentsheet.CreateIntentResult
Expand Down Expand Up @@ -59,6 +60,7 @@ internal class PaymentSheetPlaygroundViewModel(
val status = MutableStateFlow<StatusMessage?>(null)
val state = MutableStateFlow<PlaygroundState?>(null)
val flowControllerState = MutableStateFlow<FlowControllerState?>(null)
val customerSheetState = MutableStateFlow<CustomerSheetState?>(null)

init {
viewModelScope.launch(Dispatchers.IO) {
Expand All @@ -73,6 +75,7 @@ internal class PaymentSheetPlaygroundViewModel(
) {
state.value = null
flowControllerState.value = null
customerSheetState.value = null

if (playgroundSettings.configurationData.value.integrationType.isPaymentFlow()) {
prepareCheckout(playgroundSettings)
Expand Down Expand Up @@ -162,6 +165,7 @@ internal class PaymentSheetPlaygroundViewModel(
}
)
)
customerSheetState.value = CustomerSheetState()
}
}

Expand Down Expand Up @@ -299,6 +303,30 @@ internal class PaymentSheetPlaygroundViewModel(
status.value = StatusMessage(statusMessage)
}

@OptIn(ExperimentalCustomerSheetApi::class)
fun onCustomerSheetCallback(result: CustomerSheetResult) {
val statusMessage = when (result) {
is CustomerSheetResult.Selected -> {
customerSheetState.update { existingState ->
existingState?.copy(
selectedPaymentOption = result.selection?.paymentOption,
shouldFetchPaymentOption = false
)
}

null
}
is CustomerSheetResult.Failed -> "An error occurred: ${result.exception.message}"
is CustomerSheetResult.Canceled -> "Canceled"
}

statusMessage?.let { message ->
viewModelScope.launch {
status.emit(StatusMessage(message))
}
}
}

private fun createIntent(clientSecret: String): CreateIntentResult {
// Note: This is not how you'd do this in a real application. Instead, your app would
// call your backend and create (and optionally confirm) a payment or setup intent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ internal sealed interface PlaygroundState {
return this as? Payment
}

fun asCustomerState(): Customer? {
return this as? Customer
}

companion object {
fun CheckoutResponse.asPlaygroundState(
snapshot: PlaygroundSettings.Snapshot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ internal object CountrySettingsDefinition :
PlaygroundSettingDefinition.Displayable<Country> {
private val supportedPaymentFlowCountries = Country.entries.map { it.value }.toSet()
private val supportedCustomerFlowCountries = setOf(
Country.US,
Country.FR,
Country.US.value,
Country.FR.value,
)

override val displayName: String = "Merchant"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ private fun IntegrationTypeConfigurableSetting(
name = "Flow Controller",
value = PlaygroundConfigurationData.IntegrationType.FlowController
),
PlaygroundSettingDefinition.Displayable.Option(
name = "Customer Sheet",
value = PlaygroundConfigurationData.IntegrationType.CustomerSheet
),
),
value = configurationData.integrationType
) { integrationType ->
Expand Down

0 comments on commit 33efc82

Please sign in to comment.