diff --git a/paymentsheet-example/src/main/AndroidManifest.xml b/paymentsheet-example/src/main/AndroidManifest.xml
index f3f12bde90a..7ee32f1113a 100644
--- a/paymentsheet-example/src/main/AndroidManifest.xml
+++ b/paymentsheet-example/src/main/AndroidManifest.xml
@@ -61,6 +61,19 @@
/>
+
+
+
+
+
+
+
+
+
+
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/MainActivity.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/MainActivity.kt
index 69c1809d206..551d107a500 100644
--- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/MainActivity.kt
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/MainActivity.kt
@@ -35,13 +35,13 @@ import com.stripe.android.paymentsheet.example.databinding.ActivityMainBinding
import com.stripe.android.paymentsheet.example.samples.ui.SECTION_ALPHA
import com.stripe.android.paymentsheet.example.samples.ui.addresselement.AddressElementExampleActivity
import com.stripe.android.paymentsheet.example.samples.ui.customersheet.CustomerSheetExampleActivity
-import com.stripe.android.paymentsheet.example.samples.ui.customersheet.playground.CustomerSheetPlaygroundActivity
import com.stripe.android.paymentsheet.example.samples.ui.paymentsheet.complete_flow.CompleteFlowActivity
import com.stripe.android.paymentsheet.example.samples.ui.paymentsheet.custom_flow.CustomFlowActivity
import com.stripe.android.paymentsheet.example.samples.ui.paymentsheet.server_side_confirm.complete_flow.ServerSideConfirmationCompleteFlowActivity
import com.stripe.android.paymentsheet.example.samples.ui.paymentsheet.server_side_confirm.custom_flow.ServerSideConfirmationCustomFlowActivity
import com.stripe.android.paymentsheet.example.samples.ui.shared.PaymentSheetExampleTheme
import com.stripe.android.paymentsheet.example.playground.PaymentSheetPlaygroundActivity as NewPaymentSheetPlaygroundActivity
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundActivity as NewCustomerSheetPlaygroundActivity
private const val SurfaceOverlayOpacity = 0.12f
@@ -101,7 +101,7 @@ class MainActivity : AppCompatActivity() {
MenuItem(
titleResId = R.string.customersheet_playground_title,
subtitleResId = R.string.playground_subtitle,
- klass = CustomerSheetPlaygroundActivity::class.java,
+ klass = NewCustomerSheetPlaygroundActivity::class.java,
section = MenuItem.Section.Internal,
),
)
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 3223b31bf21..a90c9a58fa3 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
@@ -161,7 +161,9 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity() {
context.startActivity(
QrCodeActivity.create(
context = context,
- settingsJson = playgroundSettings.snapshot().asJsonString(),
+ settingsUri = PaymentSheetPlaygroundUrlHelper.createUri(
+ playgroundSettings.snapshot().asJsonString()
+ ),
)
)
},
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/QrCodeActivity.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/QrCodeActivity.kt
index 0de6c2b487a..8823d3c03c8 100644
--- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/QrCodeActivity.kt
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/QrCodeActivity.kt
@@ -20,15 +20,14 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import com.google.zxing.BarcodeFormat
import com.google.zxing.qrcode.QRCodeWriter
-import com.stripe.android.paymentsheet.example.playground.PaymentSheetPlaygroundUrlHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
internal class QrCodeActivity : AppCompatActivity() {
companion object {
- fun create(context: Context, settingsJson: String): Intent {
+ fun create(context: Context, settingsUri: Uri): Intent {
return Intent(context, QrCodeActivity::class.java).apply {
- putExtra("settingsJson", settingsJson)
+ putExtra("settingsUri", settingsUri.toString())
}
}
}
@@ -36,20 +35,19 @@ internal class QrCodeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val settingsJson = intent.getStringExtra("settingsJson")
- if (settingsJson == null) {
+ val settingsUri = Uri.parse(intent.getStringExtra("settingsUri"))
+
+ if (settingsUri == null) {
finish()
return
}
- val uri = PaymentSheetPlaygroundUrlHelper.createUri(settingsJson)
-
setContent {
var bitmap: Bitmap? by remember { mutableStateOf(null) }
- LaunchedEffect(uri) {
+ LaunchedEffect(settingsUri) {
launch(Dispatchers.IO) {
- bitmap = getQrCodeBitmap(uri)
+ bitmap = getQrCodeBitmap(settingsUri)
}
}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundActivity.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundActivity.kt
new file mode 100644
index 00000000000..2eefb19ff8e
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundActivity.kt
@@ -0,0 +1,240 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.compose.setContent
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+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.CustomerSheetResultCallback
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.customersheet.rememberCustomerSheet
+import com.stripe.android.paymentsheet.example.playground.activity.AppearanceBottomSheetDialogFragment
+import com.stripe.android.paymentsheet.example.playground.activity.AppearanceStore
+import com.stripe.android.paymentsheet.example.playground.activity.QrCodeActivity
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.CustomerSheetPlaygroundSettings
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.SettingsUi
+import com.stripe.android.paymentsheet.example.samples.ui.shared.PaymentMethodSelector
+
+internal class CustomerSheetPlaygroundActivity : AppCompatActivity() {
+ companion object {
+ fun createTestIntent(settingsJson: String): Intent {
+ return Intent(
+ Intent.ACTION_VIEW,
+ CustomerSheetPlaygroundUrlHelper.createUri(settingsJson)
+ )
+ }
+ }
+
+ val viewModel: CustomerSheetPlaygroundViewModel by viewModels {
+ CustomerSheetPlaygroundViewModel.Factory(
+ applicationSupplier = { application },
+ uriSupplier = { intent.data },
+ )
+ }
+
+ @SuppressLint("UnusedContentLambdaTargetStateParameter")
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val playgroundSettings by viewModel.playgroundSettingsFlow.collectAsState()
+ val localPlaygroundSettings = playgroundSettings ?: return@setContent
+
+ val playgroundState by viewModel.playgroundState.collectAsState()
+ val context = LocalContext.current
+
+ LaunchedEffect(viewModel.status) {
+ viewModel.status.collect { status ->
+ Toast.makeText(context, status.message, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ PlaygroundTheme(
+ content = {
+ SettingsUi(playgroundSettings = localPlaygroundSettings)
+
+ AppearanceButton()
+
+ QrCodeButton(playgroundSettings = localPlaygroundSettings)
+ },
+ bottomBarContent = {
+ ReloadButton()
+
+ AnimatedContent(
+ label = PLAYGROUND_BOTTOM_BAR_LABEL,
+ targetState = playgroundState != null,
+ ) {
+ Column {
+ PlaygroundStateUi(
+ playgroundState = playgroundState,
+ callback = viewModel::onCustomerSheetCallback
+ )
+ }
+ }
+ },
+ )
+ }
+ }
+
+ @Composable
+ private fun AppearanceButton() {
+ Button(
+ onClick = {
+ val bottomSheet = AppearanceBottomSheetDialogFragment.newInstance()
+ bottomSheet.show(supportFragmentManager, bottomSheet.tag)
+ },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text("Change Appearance")
+ }
+ }
+
+ @Composable
+ private fun QrCodeButton(playgroundSettings: CustomerSheetPlaygroundSettings) {
+ val context = LocalContext.current
+ Button(
+ onClick = {
+ context.startActivity(
+ QrCodeActivity.create(
+ context = context,
+ settingsUri = CustomerSheetPlaygroundUrlHelper.createUri(
+ playgroundSettings.snapshot().asJsonString()
+ ),
+ )
+ )
+ },
+ modifier = Modifier.fillMaxWidth(),
+ ) {
+ Text("QR code for current settings")
+ }
+ }
+
+ @Composable
+ private fun ReloadButton() {
+ Button(
+ onClick = viewModel::reset,
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(RELOAD_TEST_TAG),
+ ) {
+ Text("Reload")
+ }
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ @Composable
+ private fun PlaygroundStateUi(
+ playgroundState: CustomerSheetPlaygroundState?,
+ callback: CustomerSheetResultCallback,
+ ) {
+ if (playgroundState == null) {
+ return
+ }
+
+ val customerSheet = rememberCustomerSheet(
+ configuration = playgroundState.customerSheetConfiguration(),
+ customerAdapter = playgroundState.adapter,
+ callback = callback,
+ )
+
+ LaunchedEffect(customerSheet) {
+ viewModel.fetchOption(customerSheet)
+ }
+
+ val loaded = playgroundState.optionState as? CustomerSheetPlaygroundState.PaymentOptionState.Loaded
+ val option = loaded?.paymentOption
+
+ PaymentMethodSelector(
+ isEnabled = playgroundState.optionState is CustomerSheetPlaygroundState.PaymentOptionState.Loaded,
+ paymentMethodLabel = option?.label ?: "Select",
+ paymentMethodPainter = option?.iconPainter,
+ onClick = customerSheet::present
+ )
+ }
+}
+
+@Composable
+private fun PlaygroundTheme(
+ content: @Composable ColumnScope.() -> Unit,
+ bottomBarContent: @Composable ColumnScope.() -> Unit,
+) {
+ val colors = if (isSystemInDarkTheme() || AppearanceStore.forceDarkMode) {
+ darkColors()
+ } else {
+ lightColors()
+ }
+ MaterialTheme(
+ typography = MaterialTheme.typography.copy(
+ body1 = MaterialTheme.typography.body1.copy(fontSize = 14.sp)
+ ),
+ colors = colors,
+ ) {
+ Surface(
+ color = MaterialTheme.colors.background,
+ ) {
+ Scaffold(
+ bottomBar = {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colors.surface)
+ .animateContentSize()
+ ) {
+ Divider()
+ Column(
+ content = bottomBarContent,
+ modifier = Modifier
+ .padding(horizontal = 12.dp, vertical = 8.dp)
+ .fillMaxWidth()
+ )
+ }
+ },
+ ) { paddingValues ->
+ Box(modifier = Modifier.padding(paddingValues)) {
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .fillMaxSize()
+ .padding(16.dp),
+ content = content,
+ )
+ }
+ }
+ }
+ }
+}
+
+const val RELOAD_TEST_TAG = "RELOAD"
+private const val PLAYGROUND_BOTTOM_BAR_LABEL = "CustomerSheetPlaygroundBottomBar"
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundState.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundState.kt
new file mode 100644
index 00000000000..4206029d87b
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundState.kt
@@ -0,0 +1,27 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet
+
+import androidx.compose.runtime.Stable
+import com.stripe.android.customersheet.CustomerAdapter
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.CustomerSheetPlaygroundSettings
+import com.stripe.android.paymentsheet.model.PaymentOption
+
+@Stable
+@OptIn(ExperimentalCustomerSheetApi::class)
+internal data class CustomerSheetPlaygroundState(
+ private val snapshot: CustomerSheetPlaygroundSettings.Snapshot,
+ val adapter: CustomerAdapter,
+ val optionState: PaymentOptionState,
+) {
+ sealed interface PaymentOptionState {
+ data object Unloaded : PaymentOptionState
+
+ data class Loaded(val paymentOption: PaymentOption?) : PaymentOptionState
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ fun customerSheetConfiguration(): CustomerSheet.Configuration {
+ return snapshot.customerSheetConfiguration(this)
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundUrlHelper.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundUrlHelper.kt
new file mode 100644
index 00000000000..8a955960c8d
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundUrlHelper.kt
@@ -0,0 +1,23 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet
+
+import android.net.Uri
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.CustomerSheetPlaygroundSettings
+import okio.ByteString.Companion.decodeBase64
+import okio.ByteString.Companion.toByteString
+
+internal object CustomerSheetPlaygroundUrlHelper {
+ fun createUri(settingsJson: String): Uri {
+ val base64Settings = settingsJson.encodeToByteArray().toByteString().base64Url()
+ .trimEnd('=')
+ return Uri.parse(
+ "stripepaymentsheetexample://customersheetplayground?settings=" +
+ base64Settings
+ )
+ }
+
+ fun settingsFromUri(uri: Uri?): CustomerSheetPlaygroundSettings? {
+ val settingsJson = uri?.getQueryParameter("settings")
+ ?.decodeBase64()?.utf8() ?: return null
+ return CustomerSheetPlaygroundSettings.createFromJsonString(settingsJson)
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundViewModel.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundViewModel.kt
new file mode 100644
index 00000000000..b3ad045fbf7
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/CustomerSheetPlaygroundViewModel.kt
@@ -0,0 +1,242 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.github.kittinunf.fuel.Fuel
+import com.github.kittinunf.fuel.core.extensions.jsonBody
+import com.github.kittinunf.fuel.core.requests.suspendable
+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.CustomerSheet
+import com.stripe.android.customersheet.CustomerSheetResult
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.example.Settings
+import com.stripe.android.paymentsheet.example.playground.customersheet.model.CreateSetupIntentRequest
+import com.stripe.android.paymentsheet.example.playground.customersheet.model.CreateSetupIntentResponse
+import com.stripe.android.paymentsheet.example.playground.customersheet.model.CustomerEphemeralKeyRequest
+import com.stripe.android.paymentsheet.example.playground.customersheet.model.CustomerEphemeralKeyResponse
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.CountrySettingsDefinition
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.CustomerSheetPlaygroundSettings
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.PaymentMethodMode
+import com.stripe.android.paymentsheet.example.playground.customersheet.settings.PaymentMethodModeDefinition
+import com.stripe.android.paymentsheet.example.samples.networking.awaitModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.Json
+
+internal class CustomerSheetPlaygroundViewModel(
+ application: Application,
+ launchUri: Uri?,
+) : AndroidViewModel(application) {
+ private val settings by lazy {
+ Settings(application)
+ }
+
+ val playgroundSettingsFlow = MutableStateFlow(null)
+ val playgroundState = MutableStateFlow(null)
+
+ val status = MutableSharedFlow()
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ playgroundSettingsFlow.value =
+ CustomerSheetPlaygroundUrlHelper.settingsFromUri(launchUri)
+ ?: CustomerSheetPlaygroundSettings.createFromSharedPreferences(application)
+ }
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ fun reset() {
+ val playgroundSettings = playgroundSettingsFlow.value ?: return
+
+ val setupIntentClientSecretProvider = if (
+ playgroundSettings[PaymentMethodModeDefinition].value == PaymentMethodMode.SetupIntent
+ ) {
+ ::createSetupIntentClientSecret
+ } else {
+ null
+ }
+
+ playgroundState.value = CustomerSheetPlaygroundState(
+ snapshot = playgroundSettings.snapshot(),
+ adapter = CustomerAdapter.create(
+ context = getApplication(),
+ customerEphemeralKeyProvider = ::fetchEphemeralKey,
+ setupIntentClientSecretProvider = setupIntentClientSecretProvider
+
+ ),
+ optionState = CustomerSheetPlaygroundState.PaymentOptionState.Unloaded
+ )
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ fun fetchOption(customerSheet: CustomerSheet) {
+ viewModelScope.launch(Dispatchers.IO) {
+ when (val result = customerSheet.retrievePaymentOptionSelection()) {
+ is CustomerSheetResult.Selected -> {
+ playgroundState.update { existingState ->
+ existingState?.let { state ->
+ return@update state.copy(
+ optionState = CustomerSheetPlaygroundState.PaymentOptionState.Loaded(
+ paymentOption = result.selection?.paymentOption
+ )
+ )
+ }
+ }
+ }
+ is CustomerSheetResult.Failed -> {
+ status.emit(
+ StatusMessage(
+ message = "Failed to retrieve payment options:\n${result.exception.message}"
+ )
+ )
+ }
+ is CustomerSheetResult.Canceled -> Unit
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ fun onCustomerSheetCallback(result: CustomerSheetResult) {
+ val statusMessage = when (result) {
+ is CustomerSheetResult.Selected -> {
+ playgroundState.update { existingState ->
+ existingState?.let { state ->
+ if (state.optionState is CustomerSheetPlaygroundState.PaymentOptionState.Loaded) {
+ return@update state.copy(
+ optionState = CustomerSheetPlaygroundState.PaymentOptionState.Loaded(
+ paymentOption = result.selection?.paymentOption
+ )
+ )
+ }
+ }
+
+ existingState
+ }
+
+ null
+ }
+ is CustomerSheetResult.Failed -> "An error occurred: ${result.exception.message}"
+ is CustomerSheetResult.Canceled -> "Canceled"
+ }
+
+ statusMessage?.let { message ->
+ viewModelScope.launch {
+ status.emit(StatusMessage(message))
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ private suspend fun fetchEphemeralKey(): CustomerAdapter.Result {
+ val playgroundSettings = playgroundSettingsFlow.value
+ ?: return CustomerAdapter.Result.failure(
+ cause = Exception("Unexpected error: Playground settings were not found!"),
+ displayMessage = null,
+ )
+
+ // Snapshot before making the network request to not rely on UI staying in sync.
+ val playgroundSettingsSnapshot = playgroundSettings.snapshot()
+
+ playgroundSettingsSnapshot.saveToSharedPreferences(getApplication())
+
+ val requestBody = playgroundSettingsSnapshot.customerEphemeralKeyRequest()
+
+ val apiResponse = Fuel.post(settings.playgroundBackendUrl + "customer_ephemeral_key")
+ .jsonBody(Json.encodeToString(CustomerEphemeralKeyRequest.serializer(), requestBody))
+ .suspendable()
+ .awaitModel(CustomerEphemeralKeyResponse.serializer())
+
+ return when (apiResponse) {
+ is Result.Failure -> {
+ val exception = apiResponse.getException()
+
+ CustomerAdapter.Result.failure(
+ cause = exception,
+ displayMessage = "Failed to fetch ephemeral key:\n${exception.message}"
+ )
+ }
+ is Result.Success -> {
+ val response = apiResponse.value
+
+ // Init PaymentConfiguration with the publishable key returned from the backend,
+ // which will be used on all Stripe API calls
+ PaymentConfiguration.init(
+ getApplication(),
+ response.publishableKey
+ )
+
+ try {
+ CustomerAdapter.Result.success(
+ CustomerEphemeralKey.create(
+ customerId = response.customerId,
+ ephemeralKey = response.customerEphemeralKeySecret
+ ?: throw IllegalStateException(
+ "No 'customerEphemeralKeySecret' was found in backend response!"
+ )
+ )
+ )
+ } catch (exception: IllegalStateException) {
+ CustomerAdapter.Result.failure(
+ cause = exception,
+ displayMessage = "Failed to fetch ephemeral key:\n${exception.message}",
+ )
+ }
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ private suspend fun createSetupIntentClientSecret(customerId: String): CustomerAdapter.Result {
+ val playgroundSettings = playgroundSettingsFlow.value
+ ?: return CustomerAdapter.Result.failure(
+ cause = Exception("Unexpected error: Playground settings were not found!"),
+ displayMessage = null,
+ )
+
+ val request = CreateSetupIntentRequest(
+ customerId = customerId,
+ merchantCountryCode = playgroundSettings[CountrySettingsDefinition].value.value,
+ )
+
+ val apiResponse = Fuel.post(settings.playgroundBackendUrl + "create_setup_intent")
+ .jsonBody(Json.encodeToString(CreateSetupIntentRequest.serializer(), request))
+ .suspendable()
+ .awaitModel(CreateSetupIntentResponse.serializer())
+
+ return when (apiResponse) {
+ is Result.Failure -> {
+ val exception = apiResponse.getException()
+
+ CustomerAdapter.Result.failure(
+ cause = exception,
+ displayMessage = "Failed to fetch setup intent secret:\n${exception.message}"
+ )
+ }
+ is Result.Success -> CustomerAdapter.Result.success(apiResponse.value.clientSecret)
+ }
+ }
+
+ internal class Factory(
+ private val applicationSupplier: () -> Application,
+ private val uriSupplier: () -> Uri?,
+ ) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ @Suppress("UNCHECKED_CAST")
+ return CustomerSheetPlaygroundViewModel(applicationSupplier(), uriSupplier()) as T
+ }
+ }
+}
+
+data class StatusMessage(
+ val message: String,
+)
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/model/PlaygroundCheckoutModel.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/model/PlaygroundCheckoutModel.kt
new file mode 100644
index 00000000000..ffdd38c9210
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/model/PlaygroundCheckoutModel.kt
@@ -0,0 +1,70 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+class CustomerEphemeralKeyRequest private constructor(
+ @SerialName("customer_type")
+ val customerType: String?,
+ @SerialName("customer_key_type")
+ val customerKeyType: CustomerKeyType?,
+ @SerialName("merchant_country_code")
+ val merchantCountryCode: String?,
+) {
+ @Serializable
+ enum class CustomerKeyType {
+ @SerialName("customer_session")
+ CustomerSession,
+
+ @SerialName("legacy")
+ Legacy;
+ }
+
+ class Builder {
+ private var customerType: String? = null
+ private var merchantCountryCode: String? = null
+
+ fun customerType(customerType: String) = apply {
+ this.customerType = customerType
+ }
+
+ fun merchantCountryCode(merchantCountryCode: String?) = apply {
+ this.merchantCountryCode = merchantCountryCode
+ }
+
+ fun build(): CustomerEphemeralKeyRequest {
+ return CustomerEphemeralKeyRequest(
+ customerType = customerType,
+ customerKeyType = CustomerKeyType.Legacy,
+ merchantCountryCode = merchantCountryCode,
+ )
+ }
+ }
+}
+
+@Serializable
+data class CustomerEphemeralKeyResponse(
+ @SerialName("publishableKey")
+ val publishableKey: String,
+ @SerialName("customerId")
+ val customerId: String,
+ @SerialName("customerEphemeralKeySecret")
+ val customerEphemeralKeySecret: String? = null,
+ @SerialName("customerSessionClientSecret")
+ val customerSessionClientSecret: String? = null,
+)
+
+@Serializable
+data class CreateSetupIntentRequest(
+ @SerialName("customer_id")
+ val customerId: String,
+ @SerialName("merchant_country_code")
+ val merchantCountryCode: String,
+)
+
+@Serializable
+data class CreateSetupIntentResponse(
+ @SerialName("client_secret")
+ val clientSecret: String,
+)
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AllowsRemovalOfLastSavedPaymentMethodSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AllowsRemovalOfLastSavedPaymentMethodSettingsDefinition.kt
new file mode 100644
index 00000000000..be7f08073a6
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AllowsRemovalOfLastSavedPaymentMethodSettingsDefinition.kt
@@ -0,0 +1,22 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+
+internal object AllowsRemovalOfLastSavedPaymentMethodSettingsDefinition : BooleanSettingsDefinition(
+ key = "allowsRemovalOfLastSavedPaymentMethod",
+ displayName = "Allows removal of last saved payment method",
+ defaultValue = true,
+) {
+ @OptIn(ExperimentalAllowsRemovalOfLastSavedPaymentMethodApi::class, ExperimentalCustomerSheetApi::class)
+ override fun configure(
+ value: Boolean,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetPlaygroundSettingDefinition.CustomerSheetConfigurationData
+ ) {
+ configurationBuilder.allowsRemovalOfLastSavedPaymentMethod(value)
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AppearanceSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AppearanceSettingsDefinition.kt
new file mode 100644
index 00000000000..859118351e7
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AppearanceSettingsDefinition.kt
@@ -0,0 +1,20 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.example.playground.activity.AppearanceStore
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+
+internal object AppearanceSettingsDefinition : CustomerSheetPlaygroundSettingDefinition {
+ override val defaultValue: Unit = Unit
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ override fun configure(
+ value: Unit,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetPlaygroundSettingDefinition.CustomerSheetConfigurationData
+ ) {
+ configurationBuilder.appearance(AppearanceStore.state)
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AttachDefaultBillingDetailsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AttachDefaultBillingDetailsDefinition.kt
new file mode 100644
index 00000000000..92eb0ece80c
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/AttachDefaultBillingDetailsDefinition.kt
@@ -0,0 +1,21 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+
+internal object AttachDefaultBillingDetailsDefinition : BooleanSettingsDefinition(
+ key = "attachDefaults",
+ displayName = "Attach Billing Details to Payment Method",
+ defaultValue = true,
+) {
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ override fun configure(
+ value: Boolean,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetPlaygroundSettingDefinition.CustomerSheetConfigurationData,
+ ) {
+ configurationData.updateBillingDetails { copy(attachDefaultsToPaymentMethod = value) }
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/BooleanSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/BooleanSettingsDefinition.kt
new file mode 100644
index 00000000000..a69ab6af97e
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/BooleanSettingsDefinition.kt
@@ -0,0 +1,24 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+internal abstract class BooleanSettingsDefinition(
+ override val key: String,
+ override val displayName: String,
+ override val defaultValue: Boolean,
+) : CustomerSheetPlaygroundSettingDefinition,
+ CustomerSheetPlaygroundSettingDefinition.Saveable,
+ CustomerSheetPlaygroundSettingDefinition.Displayable {
+ override val options: List> by lazy {
+ listOf(
+ option("On", true),
+ option("Off", false),
+ )
+ }
+
+ override fun convertToString(value: Boolean): String {
+ return value.toString()
+ }
+
+ override fun convertToValue(value: String): Boolean {
+ return value == "true"
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectAddressSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectAddressSettingsDefinition.kt
new file mode 100644
index 00000000000..e6fe19e47b0
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectAddressSettingsDefinition.kt
@@ -0,0 +1,46 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.AddressCollectionMode as CollectionMode
+
+internal object CollectAddressSettingsDefinition :
+ CustomerSheetPlaygroundSettingDefinition,
+ CustomerSheetPlaygroundSettingDefinition.Saveable,
+ CustomerSheetPlaygroundSettingDefinition.Displayable {
+
+ override val defaultValue: CollectionMode = CollectionMode.Automatic
+ override val key: String = "collectAddress"
+ override val displayName: String = "Collect Address"
+ override val options: List> by lazy {
+ listOf(
+ option("Auto", CollectionMode.Automatic),
+ option("Never", CollectionMode.Never),
+ option("Full", CollectionMode.Full),
+ )
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ override fun configure(
+ value: CollectionMode,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetPlaygroundSettingDefinition.CustomerSheetConfigurationData,
+ ) {
+ configurationData.updateBillingDetails { copy(address = value) }
+ }
+
+ override fun convertToString(value: CollectionMode): String = when (value) {
+ CollectionMode.Automatic -> "auto"
+ CollectionMode.Never -> "never"
+ CollectionMode.Full -> "full"
+ }
+
+ override fun convertToValue(value: String): CollectionMode = when (value) {
+ "auto" -> CollectionMode.Automatic
+ "never" -> CollectionMode.Never
+ "full" -> CollectionMode.Full
+ else -> CollectionMode.Automatic
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectEmailSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectEmailSettingsDefinition.kt
new file mode 100644
index 00000000000..4f1772a0ce4
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectEmailSettingsDefinition.kt
@@ -0,0 +1,21 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+
+internal object CollectEmailSettingsDefinition : CollectionModeSettingsDefinition(
+ key = "collectEmail",
+ displayName = "Collect Email",
+) {
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ override fun configure(
+ value: CollectionMode,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetPlaygroundSettingDefinition.CustomerSheetConfigurationData,
+ ) {
+ configurationData.updateBillingDetails { copy(email = value) }
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectNameSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectNameSettingsDefinition.kt
new file mode 100644
index 00000000000..bec2ee0588c
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectNameSettingsDefinition.kt
@@ -0,0 +1,21 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+
+internal object CollectNameSettingsDefinition : CollectionModeSettingsDefinition(
+ key = "collectName",
+ displayName = "Collect Name",
+) {
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ override fun configure(
+ value: CollectionMode,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetPlaygroundSettingDefinition.CustomerSheetConfigurationData,
+ ) {
+ configurationData.updateBillingDetails { copy(name = value) }
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectPhoneSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectPhoneSettingsDefinition.kt
new file mode 100644
index 00000000000..4f43c3b276b
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectPhoneSettingsDefinition.kt
@@ -0,0 +1,21 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+
+internal object CollectPhoneSettingsDefinition : CollectionModeSettingsDefinition(
+ key = "collectPhone",
+ displayName = "Collect Phone",
+) {
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ override fun configure(
+ value: CollectionMode,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetPlaygroundSettingDefinition.CustomerSheetConfigurationData,
+ ) {
+ configurationData.updateBillingDetails { copy(phone = value) }
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectionModeSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectionModeSettingsDefinition.kt
new file mode 100644
index 00000000000..0ce08e4f035
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CollectionModeSettingsDefinition.kt
@@ -0,0 +1,32 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.paymentsheet.PaymentSheet.BillingDetailsCollectionConfiguration.CollectionMode
+
+internal open class CollectionModeSettingsDefinition(
+ override val key: String,
+ override val displayName: String
+) : CustomerSheetPlaygroundSettingDefinition.Saveable,
+ CustomerSheetPlaygroundSettingDefinition.Displayable {
+
+ override val defaultValue: CollectionMode = CollectionMode.Automatic
+ override val options: List> by lazy {
+ listOf(
+ option("Auto", CollectionMode.Automatic),
+ option("Never", CollectionMode.Never),
+ option("Always", CollectionMode.Always),
+ )
+ }
+
+ override fun convertToString(value: CollectionMode): String = when (value) {
+ CollectionMode.Automatic -> "auto"
+ CollectionMode.Never -> "never"
+ CollectionMode.Always -> "always"
+ }
+
+ override fun convertToValue(value: String): CollectionMode = when (value) {
+ "auto" -> CollectionMode.Automatic
+ "never" -> CollectionMode.Never
+ "always" -> CollectionMode.Always
+ else -> CollectionMode.Automatic
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CountrySettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CountrySettingsDefinition.kt
new file mode 100644
index 00000000000..84ea4cf8511
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CountrySettingsDefinition.kt
@@ -0,0 +1,41 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.core.model.CountryUtils
+import com.stripe.android.paymentsheet.example.playground.customersheet.model.CustomerEphemeralKeyRequest
+import java.util.Locale
+
+internal object CountrySettingsDefinition :
+ CustomerSheetPlaygroundSettingDefinition,
+ CustomerSheetPlaygroundSettingDefinition.Saveable by EnumSaveable(
+ key = "country",
+ values = Country.entries.toTypedArray(),
+ defaultValue = Country.US,
+ ),
+ CustomerSheetPlaygroundSettingDefinition.Displayable {
+ private val supportedCountries = Country.entries.map { it.value }.toSet()
+
+ override val displayName: String = "Merchant Country"
+
+ override val options: List>
+ get() = CountryUtils.getOrderedCountries(Locale.getDefault()).filter { country ->
+ country.code.value in supportedCountries
+ }.map { country ->
+ option(country.name, convertToValue(country.code.value))
+ }.toList()
+
+ override fun configure(value: Country, requestBuilder: CustomerEphemeralKeyRequest.Builder) {
+ requestBuilder.merchantCountryCode(value.value)
+ }
+
+ override fun valueUpdated(value: Country, playgroundSettings: CustomerSheetPlaygroundSettings) {
+ // When the country changes via the UI, reset the customer.
+ if (playgroundSettings[CustomerSettingsDefinition].value is CustomerType.Existing) {
+ playgroundSettings[CustomerSettingsDefinition] = CustomerType.New
+ }
+ }
+}
+
+enum class Country(override val value: String) : ValueEnum {
+ US("US"),
+ FR("FR"),
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CustomerSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CustomerSettingsDefinition.kt
new file mode 100644
index 00000000000..691f4aff8bd
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CustomerSettingsDefinition.kt
@@ -0,0 +1,55 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.paymentsheet.example.playground.customersheet.model.CustomerEphemeralKeyRequest
+
+internal object CustomerSettingsDefinition :
+ CustomerSheetPlaygroundSettingDefinition,
+ CustomerSheetPlaygroundSettingDefinition.Saveable,
+ CustomerSheetPlaygroundSettingDefinition.Displayable {
+ override val displayName: String = "Customer"
+ override val options: List> =
+ listOf(
+ CustomerSettingsDefinition.option("New", CustomerType.New),
+ CustomerSettingsDefinition.option("Returning", CustomerType.Returning),
+ )
+
+ override fun configure(
+ value: CustomerType,
+ requestBuilder: CustomerEphemeralKeyRequest.Builder,
+ ) {
+ requestBuilder.customerType(value.value)
+ }
+
+ override val key: String = "customer"
+ override val defaultValue: CustomerType = CustomerType.New
+
+ override fun convertToValue(value: String): CustomerType {
+ val hardcodedCustomerTypes = mapOf(
+ CustomerType.New.value to CustomerType.New,
+ CustomerType.Returning.value to CustomerType.Returning,
+ )
+ return if (value.startsWith("cus_")) {
+ CustomerType.Existing(value)
+ } else if (hardcodedCustomerTypes.containsKey(value)) {
+ hardcodedCustomerTypes[value]!!
+ } else {
+ defaultValue
+ }
+ }
+
+ override fun convertToString(value: CustomerType): String {
+ return value.value
+ }
+}
+
+sealed class CustomerType(val value: String) {
+ data object New : CustomerType("new")
+
+ data object Returning : CustomerType("returning")
+
+ class Existing(val customerId: String) : CustomerType(customerId) {
+ override fun toString(): String {
+ return customerId
+ }
+ }
+}
diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CustomerSheetPlaygroundSettingDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CustomerSheetPlaygroundSettingDefinition.kt
new file mode 100644
index 00000000000..6a203cc2675
--- /dev/null
+++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/customersheet/settings/CustomerSheetPlaygroundSettingDefinition.kt
@@ -0,0 +1,78 @@
+package com.stripe.android.paymentsheet.example.playground.customersheet.settings
+
+import com.stripe.android.customersheet.CustomerSheet
+import com.stripe.android.customersheet.ExperimentalCustomerSheetApi
+import com.stripe.android.paymentsheet.PaymentSheet
+import com.stripe.android.paymentsheet.example.playground.customersheet.CustomerSheetPlaygroundState
+import com.stripe.android.paymentsheet.example.playground.customersheet.model.CustomerEphemeralKeyRequest
+
+internal interface CustomerSheetPlaygroundSettingDefinition {
+ val defaultValue: T
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ fun configure(
+ value: T,
+ configurationBuilder: CustomerSheet.Configuration.Builder,
+ playgroundState: CustomerSheetPlaygroundState,
+ configurationData: CustomerSheetConfigurationData,
+ ) {
+ }
+
+ fun configure(
+ value: T,
+ requestBuilder: CustomerEphemeralKeyRequest.Builder,
+ ) {
+ }
+
+ fun valueUpdated(value: T, playgroundSettings: CustomerSheetPlaygroundSettings) {}
+
+ fun saveable(): Saveable? {
+ @Suppress("UNCHECKED_CAST")
+ return this as? Saveable?
+ }
+
+ fun displayable(): Displayable? {
+ return this as? Displayable?
+ }
+
+ @OptIn(ExperimentalCustomerSheetApi::class)
+ data class CustomerSheetConfigurationData(
+ private val configurationBuilder: CustomerSheet.Configuration.Builder,
+ var billingDetailsCollectionConfiguration: PaymentSheet.BillingDetailsCollectionConfiguration =
+ PaymentSheet.BillingDetailsCollectionConfiguration()
+ ) {
+ // Billing details is a nested configuration, but we have individual settings for it in the
+ // UI, this helper keeps all of the configurations, rather than just the most recent.
+ fun updateBillingDetails(
+ block: PaymentSheet.BillingDetailsCollectionConfiguration.() ->
+ PaymentSheet.BillingDetailsCollectionConfiguration
+ ) {
+ billingDetailsCollectionConfiguration.apply {
+ billingDetailsCollectionConfiguration = block()
+ }
+ configurationBuilder.billingDetailsCollectionConfiguration(
+ billingDetailsCollectionConfiguration
+ )
+ }
+ }
+
+ interface Saveable {
+ val key: String
+ val defaultValue: T
+ fun convertToString(value: T): String
+ fun convertToValue(value: String): T
+ val saveToSharedPreferences: Boolean
+ get() = true
+ }
+
+ interface Displayable : CustomerSheetPlaygroundSettingDefinition {
+ val displayName: String
+ val options: List