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 new CustomerSheet playground #8362

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions paymentsheet-example/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@
/>
</intent-filter>
</activity>
<activity android:name=".playground.customersheet.CustomerSheetPlaygroundActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="stripepaymentsheetexample"
android:host="customersheetplayground"
/>
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like you replaced this menu item, but didn't delete the old playground.

section = MenuItem.Section.Internal,
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity() {
context.startActivity(
QrCodeActivity.create(
context = context,
settingsJson = playgroundSettings.snapshot().asJsonString(),
settingsUri = PaymentSheetPlaygroundUrlHelper.createUri(
playgroundSettings.snapshot().asJsonString()
),
)
)
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,34 @@ 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())
}
}
}

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)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
samer-stripe marked this conversation as resolved.
Show resolved Hide resolved
samer-stripe marked this conversation as resolved.
Show resolved Hide resolved
}

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"
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}