diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/CustomerSheetState.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/CustomerSheetState.kt new file mode 100644 index 00000000000..824a3421e8a --- /dev/null +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/CustomerSheetState.kt @@ -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 +} diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt index b24f25a76f3..cbe796b3559 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundActivity.kt @@ -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 @@ -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 { @@ -74,6 +81,7 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay ) } + @OptIn(ExperimentalCustomerSheetApi::class) @Suppress("LongMethod") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -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 -> @@ -128,6 +144,7 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay playgroundState = playgroundState, paymentSheet = paymentSheet, flowController = flowController, + customerSheet = customerSheet, addressLauncher = addressLauncher, ) } @@ -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) { @@ -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) } } } @@ -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, @@ -382,6 +441,17 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay } } + @OptIn(ExperimentalCustomerSheetApi::class) + private suspend fun fetchOption( + customerSheet: CustomerSheet + ): Result = 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 diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt index 74ac71ef507..f4922d6ae43 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PaymentSheetPlaygroundViewModel.kt @@ -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 @@ -59,6 +60,7 @@ internal class PaymentSheetPlaygroundViewModel( val status = MutableStateFlow(null) val state = MutableStateFlow(null) val flowControllerState = MutableStateFlow(null) + val customerSheetState = MutableStateFlow(null) init { viewModelScope.launch(Dispatchers.IO) { @@ -73,6 +75,7 @@ internal class PaymentSheetPlaygroundViewModel( ) { state.value = null flowControllerState.value = null + customerSheetState.value = null if (playgroundSettings.configurationData.value.integrationType.isPaymentFlow()) { prepareCheckout(playgroundSettings) @@ -162,6 +165,7 @@ internal class PaymentSheetPlaygroundViewModel( } ) ) + customerSheetState.value = CustomerSheetState() } } @@ -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. diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt index 80274f324b0..9a06ad7b868 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/PlaygroundState.kt @@ -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, diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/CountrySettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/CountrySettingsDefinition.kt index c93dab44f30..43cd89be76f 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/CountrySettingsDefinition.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/CountrySettingsDefinition.kt @@ -15,8 +15,8 @@ internal object CountrySettingsDefinition : PlaygroundSettingDefinition.Displayable { 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" diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/SettingsUI.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/SettingsUI.kt index 9f738d87cdb..f1ec56c63f4 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/SettingsUI.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/SettingsUI.kt @@ -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 ->