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 CustomerSheet playground UI #8524

Merged
merged 2 commits into from
May 23, 2024
Merged
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
@@ -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 ->
Copy link
Collaborator

Choose a reason for hiding this comment

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

We want this to be unconditionally created like the others. Is that not possible with customer sheet? If not this integration shape is probably wrong :/

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
Loading