diff --git a/paymentsheet-example/detekt-baseline.xml b/paymentsheet-example/detekt-baseline.xml index d8ccd78c2c5..67e65c7348b 100644 --- a/paymentsheet-example/detekt-baseline.xml +++ b/paymentsheet-example/detekt-baseline.xml @@ -5,13 +5,19 @@ EmptyFunctionBlock:DrawablePainter.kt$EmptyPainter${} FunctionNaming:Receipt.kt$@Preview @Composable fun Receipt_Editable() FunctionNaming:Receipt.kt$@Preview @Composable fun Receipt_NotEditable() + LongMethod:AppearanceBottomSheetDialogFragment.kt$@Composable private fun AppearancePicker( currentAppearance: PaymentSheet.Appearance, updateAppearance: (PaymentSheet.Appearance) -> Unit, embeddedAppearance: EmbeddedAppearance, updateEmbedded: (EmbeddedAppearance) -> Unit, resetAppearance: () -> Unit, ) LongMethod:AppearanceBottomSheetDialogFragment.kt$@Composable private fun Colors( currentAppearance: PaymentSheet.Appearance, updateAppearance: (PaymentSheet.Appearance) -> Unit, ) + LongMethod:AppearanceBottomSheetDialogFragment.kt$@Composable private fun EmbeddedPicker( embeddedAppearance: EmbeddedAppearance, updateEmbedded: (EmbeddedAppearance) -> Unit ) LongMethod:AppearanceBottomSheetDialogFragment.kt$@Composable private fun PrimaryButton( currentAppearance: PaymentSheet.Appearance, updateAppearance: (PaymentSheet.Appearance) -> Unit, ) LongMethod:Receipt.kt$@Composable fun Receipt( isLoading: Boolean, cartState: CartState, isEditable: Boolean = false, onQuantityChanged: (CartProduct.Id, Int) -> Unit = { _, _ -> }, bottomContent: @Composable () -> Unit, ) MagicNumber:AppearanceBottomSheetDialogFragment.kt$16 + MagicNumber:AppearanceBottomSheetDialogFragment.kt$33 MagicNumber:CartProduct.kt$100 MagicNumber:DrawablePainter.kt$DrawablePainter$23 MagicNumber:DrawablePainter.kt$DrawablePainter$255 + MagicNumber:EmbeddedAppearanceSettingsDefinition.kt$EmbeddedAppearance$0x33787880 + MagicNumber:EmbeddedAppearanceSettingsDefinition.kt$EmbeddedAppearance$0xFF007AFF + MagicNumber:EmbeddedAppearanceSettingsDefinition.kt$EmbeddedAppearance$0xFF787880 MagicNumber:Payment.kt$0.5f PackageNaming:CompleteFlowActivity.kt$package com.stripe.android.paymentsheet.example.samples.ui.paymentsheet.complete_flow PackageNaming:CompleteFlowViewModel.kt$package com.stripe.android.paymentsheet.example.samples.ui.paymentsheet.complete_flow 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 c42b346eb15..edbb0a70ac7 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 @@ -29,7 +29,6 @@ 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.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -53,8 +52,10 @@ import com.stripe.android.paymentsheet.example.playground.activity.AppearanceBot import com.stripe.android.paymentsheet.example.playground.activity.AppearanceStore import com.stripe.android.paymentsheet.example.playground.activity.FawryActivity import com.stripe.android.paymentsheet.example.playground.activity.QrCodeActivity +import com.stripe.android.paymentsheet.example.playground.activity.getEmbeddedAppearance import com.stripe.android.paymentsheet.example.playground.embedded.EmbeddedPlaygroundContract import com.stripe.android.paymentsheet.example.playground.settings.CheckoutMode +import com.stripe.android.paymentsheet.example.playground.settings.EmbeddedAppearanceSettingsDefinition import com.stripe.android.paymentsheet.example.playground.settings.InitializationType import com.stripe.android.paymentsheet.example.playground.settings.PlaygroundConfigurationData import com.stripe.android.paymentsheet.example.playground.settings.PlaygroundSettings @@ -63,6 +64,7 @@ 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.uicore.utils.collectAsState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext @@ -210,9 +212,23 @@ internal class PaymentSheetPlaygroundActivity : AppCompatActivity(), ExternalPay @Composable private fun AppearanceButton() { + val settings = viewModel.playgroundSettingsFlow.collectAsState().value + val embeddedAppearance = settings?.get(EmbeddedAppearanceSettingsDefinition)?.collectAsState()?.value + supportFragmentManager.setFragmentResultListener( + AppearanceBottomSheetDialogFragment.REQUEST_KEY, + this@PaymentSheetPlaygroundActivity + ) { _, bundle -> + viewModel.updateEmbeddedAppearance( + EmbeddedAppearanceSettingsDefinition, + bundle.getEmbeddedAppearance() + ) + } Button( onClick = { val bottomSheet = AppearanceBottomSheetDialogFragment.newInstance() + bottomSheet.arguments = Bundle().apply { + putParcelable(AppearanceBottomSheetDialogFragment.EMBEDDED_KEY, embeddedAppearance) + } bottomSheet.show(supportFragmentManager, bottomSheet.tag) }, modifier = Modifier.fillMaxWidth(), 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 a4e7fabea2a..5152020d09e 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 @@ -39,6 +39,8 @@ import com.stripe.android.paymentsheet.example.playground.settings.Country import com.stripe.android.paymentsheet.example.playground.settings.CustomEndpointDefinition import com.stripe.android.paymentsheet.example.playground.settings.CustomerSettingsDefinition import com.stripe.android.paymentsheet.example.playground.settings.CustomerType +import com.stripe.android.paymentsheet.example.playground.settings.EmbeddedAppearance +import com.stripe.android.paymentsheet.example.playground.settings.EmbeddedAppearanceSettingsDefinition import com.stripe.android.paymentsheet.example.playground.settings.InitializationType import com.stripe.android.paymentsheet.example.playground.settings.PlaygroundConfigurationData import com.stripe.android.paymentsheet.example.playground.settings.PlaygroundSettings @@ -591,6 +593,21 @@ internal class PaymentSheetPlaygroundViewModel( } } + fun updateEmbeddedAppearance(appearanceSetting: EmbeddedAppearanceSettingsDefinition, value: EmbeddedAppearance) { + playgroundSettingsFlow.value?.let { settings -> + settings[appearanceSetting] = value + setPlaygroundState( + state.value?.let { state -> + val updatedSnapshot = settings.snapshot() + when (state) { + is PlaygroundState.Customer -> state.copy(snapshot = updatedSnapshot) + is PlaygroundState.Payment -> state.copy(snapshot = updatedSnapshot) + } + } + ) + } + } + private fun updatePaymentOptionForCustomerSheet(paymentOption: PaymentOption?) { customerSheetState.update { existingState -> existingState?.copy( diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/AppearanceBottomSheetDialogFragment.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/AppearanceBottomSheetDialogFragment.kt index 4e37c710e91..97d0fdb9c7b 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/AppearanceBottomSheetDialogFragment.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/activity/AppearanceBottomSheetDialogFragment.kt @@ -1,5 +1,6 @@ package com.stripe.android.paymentsheet.example.playground.activity +import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -35,6 +36,7 @@ import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Scaffold import androidx.compose.material.Slider +import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar @@ -61,11 +63,15 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.fragment.app.setFragmentResult import com.godaddy.android.colorpicker.ClassicColorPicker import com.godaddy.android.colorpicker.HsvColor import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.stripe.android.paymentsheet.PaymentSheet import com.stripe.android.paymentsheet.example.R +import com.stripe.android.paymentsheet.example.playground.activity.AppearanceBottomSheetDialogFragment.Companion.EMBEDDED_KEY +import com.stripe.android.paymentsheet.example.playground.settings.EmbeddedAppearance +import com.stripe.android.paymentsheet.example.playground.settings.EmbeddedRow private val BASE_FONT_SIZE = 20.sp private val BASE_PADDING = 8.dp @@ -73,25 +79,48 @@ private val SECTION_LABEL_COLOR = Color(159, 159, 169) internal class AppearanceBottomSheetDialogFragment : BottomSheetDialogFragment() { + private var embeddedAppearance by mutableStateOf(EmbeddedAppearance()) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + embeddedAppearance = arguments.getEmbeddedAppearance() + return ComposeView(requireContext()).apply { setContent { AppearancePicker( currentAppearance = AppearanceStore.state, updateAppearance = { AppearanceStore.state = it }, + embeddedAppearance = embeddedAppearance, + updateEmbedded = { embeddedAppearance = it }, + resetAppearance = ::resetAppearance ) } } } + private fun resetAppearance() { + AppearanceStore.reset() + embeddedAppearance = EmbeddedAppearance() + } + + override fun onDestroy() { + super.onDestroy() + val result = Bundle().apply { + putParcelable(EMBEDDED_KEY, embeddedAppearance) + } + setFragmentResult(REQUEST_KEY, result) + } + companion object { fun newInstance(): AppearanceBottomSheetDialogFragment { return AppearanceBottomSheetDialogFragment() } + + const val REQUEST_KEY = "REQUEST_KEY" + const val EMBEDDED_KEY = "EMBEDDED_APPEARANCE" } } @@ -99,6 +128,9 @@ internal class AppearanceBottomSheetDialogFragment : BottomSheetDialogFragment() private fun AppearancePicker( currentAppearance: PaymentSheet.Appearance, updateAppearance: (PaymentSheet.Appearance) -> Unit, + embeddedAppearance: EmbeddedAppearance, + updateEmbedded: (EmbeddedAppearance) -> Unit, + resetAppearance: () -> Unit, ) { val scrollState = rememberScrollState() val nestedScrollConnection = rememberNestedScrollInteropConnection() @@ -108,7 +140,9 @@ private fun AppearancePicker( TopAppBar( title = { Text("Appearance") }, actions = { - TextButton(onClick = AppearanceStore::reset) { + TextButton(onClick = { + resetAppearance() + }) { Text(text = stringResource(R.string.reset)) } }, @@ -151,6 +185,12 @@ private fun AppearancePicker( updateAppearance = updateAppearance, ) } + CustomizationCard("Embedded") { + EmbeddedPicker( + embeddedAppearance = embeddedAppearance, + updateEmbedded = updateEmbedded + ) + } Spacer(modifier = Modifier.height(16.dp)) } @@ -590,11 +630,126 @@ private fun PrimaryButton( } @Composable -private fun ColorItem( +private fun EmbeddedPicker( + embeddedAppearance: EmbeddedAppearance, + updateEmbedded: (EmbeddedAppearance) -> Unit +) { + RowStyleDropDown(embeddedAppearance.embeddedRowStyle) { style -> + updateEmbedded( + embeddedAppearance.copy( + embeddedRowStyle = style + ) + ) + } + ColorItem( + label = "separatorColor", + currentColor = Color(embeddedAppearance.separatorColor), + onColorPicked = { + embeddedAppearance.copy( + separatorColor = it.toArgb() + ) + }, + updateAppearance = updateEmbedded, + ) + Divider() + + ColorItem( + label = "selectedColor", + currentColor = Color(embeddedAppearance.selectedColor), + onColorPicked = { + embeddedAppearance.copy( + selectedColor = it.toArgb() + ) + }, + updateAppearance = updateEmbedded, + ) + Divider() + + ColorItem( + label = "unselectedColor", + currentColor = Color(embeddedAppearance.unselectedColor), + onColorPicked = { + embeddedAppearance.copy( + unselectedColor = it.toArgb() + ) + }, + updateAppearance = updateEmbedded, + ) + Divider() + + ColorItem( + label = "checkmarkColor", + currentColor = Color(embeddedAppearance.checkmarkColor), + onColorPicked = { + embeddedAppearance.copy( + checkmarkColor = it.toArgb() + ) + }, + updateAppearance = updateEmbedded, + ) + Divider() + + IncrementDecrementItem("separatorInsetsDp", embeddedAppearance.separatorInsetsDp) { + updateEmbedded( + embeddedAppearance.copy( + separatorInsetsDp = it + ) + ) + } + Divider() + + IncrementDecrementItem("separatorThicknessDp", embeddedAppearance.separatorThicknessDp) { + updateEmbedded( + embeddedAppearance.copy( + separatorThicknessDp = it + ) + ) + } + Divider() + + IncrementDecrementItem("additionalInsetsDp", embeddedAppearance.additionalInsetsDp) { + updateEmbedded( + embeddedAppearance.copy( + additionalInsetsDp = it + ) + ) + } + Divider() + + IncrementDecrementItem("checkmarkInsetsDp", embeddedAppearance.checkmarkInsetsDp) { + updateEmbedded( + embeddedAppearance.copy( + checkmarkInsetsDp = it + ) + ) + } + Divider() + + AppearanceToggle("topSeparatorEnabled", embeddedAppearance.topSeparatorEnabled) { + updateEmbedded( + embeddedAppearance.copy( + topSeparatorEnabled = it + ) + ) + } + Divider() + + AppearanceToggle("bottomSeparatorEnabled", embeddedAppearance.bottomSeparatorEnabled) { + updateEmbedded( + embeddedAppearance.copy( + bottomSeparatorEnabled = it + ) + ) + } + Divider() +} + +@Composable +private fun ColorItem( label: String, currentColor: Color, - onColorPicked: (Color) -> PaymentSheet.Appearance, - updateAppearance: (PaymentSheet.Appearance) -> Unit, + onColorPicked: (Color) -> T, + updateAppearance: (T) -> Unit, ) { val openDialog = remember { mutableStateOf(false) } ColorPicker(openDialog, currentColor) { @@ -724,6 +879,26 @@ private fun IncrementDecrementItem(label: String, value: Float, onValueChange: ( } } +@Composable +private fun AppearanceToggle(label: String, value: Boolean, onValueChange: (Boolean) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(all = BASE_PADDING), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "$label: $value", fontSize = BASE_FONT_SIZE) + Spacer(modifier = Modifier.weight(1f)) + Switch( + checked = value, + onCheckedChange = { + onValueChange(it) + } + ) + } +} + @Composable private fun FontScaleSlider(sliderPosition: Float, onValueChange: (Float) -> Unit) { Text( @@ -740,6 +915,43 @@ private fun FontScaleSlider(sliderPosition: Float, onValueChange: (Float) -> Uni ) } +@Composable +private fun RowStyleDropDown(style: EmbeddedRow, rowStyleSelectedCallback: (EmbeddedRow) -> Unit) { + var expanded by remember { mutableStateOf(false) } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxSize() + .padding(all = BASE_PADDING) + .wrapContentSize(Alignment.TopStart) + ) { + Text( + text = "RowStyle: $style", + fontSize = BASE_FONT_SIZE, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = { expanded = true }) + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth() + ) { + EmbeddedRow.entries.forEach { + DropdownMenuItem( + onClick = { + expanded = false + rowStyleSelectedCallback(it) + } + ) { + Text(it.name) + } + } + } + } +} + @Composable private fun FontDropDown(fontResId: Int?, fontSelectedCallback: (Int?) -> Unit) { var expanded by remember { mutableStateOf(false) } @@ -800,3 +1012,13 @@ private fun getFontFromResource(fontResId: Int?): FontFamily { FontFamily.Default } } + +internal fun Bundle?.getEmbeddedAppearance(): EmbeddedAppearance { + val appearance = if (SDK_INT >= 33) { + this?.getParcelable(EMBEDDED_KEY, EmbeddedAppearance::class.java) + } else { + @Suppress("DEPRECATION") + this?.getParcelable(EMBEDDED_KEY) + } + return appearance ?: EmbeddedAppearance() +} diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/EmbeddedAppearanceSettingsDefinition.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/EmbeddedAppearanceSettingsDefinition.kt new file mode 100644 index 00000000000..da319c4530a --- /dev/null +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/EmbeddedAppearanceSettingsDefinition.kt @@ -0,0 +1,91 @@ +package com.stripe.android.paymentsheet.example.playground.settings + +import android.os.Parcelable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.stripe.android.paymentelement.ExperimentalEmbeddedPaymentElementApi +import com.stripe.android.paymentsheet.PaymentSheet +import com.stripe.android.paymentsheet.example.playground.activity.AppearanceStore +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +internal object EmbeddedAppearanceSettingsDefinition : + PlaygroundSettingDefinition, + PlaygroundSettingDefinition.Saveable { + override val key: String + get() = "embeddedAppearance" + + override val defaultValue: EmbeddedAppearance + get() = EmbeddedAppearance() + + override fun convertToValue(value: String): EmbeddedAppearance { + return Json.decodeFromString(value) + } + + override fun convertToString(value: EmbeddedAppearance): String { + return Json.encodeToString(value) + } + + @OptIn(ExperimentalEmbeddedPaymentElementApi::class) + override fun valueUpdated(value: EmbeddedAppearance, playgroundSettings: PlaygroundSettings) { + super.valueUpdated(value, playgroundSettings) + AppearanceStore.state = AppearanceStore.state.copy( + embeddedAppearance = PaymentSheet.Appearance.Embedded(value.getRow()) + ) + } +} + +internal enum class EmbeddedRow { + FlatWithRadio, + FlatWithCheckmark, + FloatingButton +} + +@OptIn(ExperimentalEmbeddedPaymentElementApi::class) +@Serializable +@Parcelize +internal data class EmbeddedAppearance( + val embeddedRowStyle: EmbeddedRow = EmbeddedRow.FlatWithRadio, + val separatorThicknessDp: Float = 1.0f, + val separatorInsetsDp: Float = 0.0f, + val additionalInsetsDp: Float = 4.0f, + val checkmarkInsetsDp: Float = 12.0f, + val floatingButtonSpacingDp: Float = 12.0f, + val topSeparatorEnabled: Boolean = true, + val bottomSeparatorEnabled: Boolean = true, + val separatorColor: Int = Color(0xFF787880).toArgb(), + val selectedColor: Int = Color(0xFF007AFF).toArgb(), + val unselectedColor: Int = Color(0x33787880).toArgb(), + val checkmarkColor: Int = Color(0xFF007AFF).toArgb() +) : Parcelable { + fun getRow(): PaymentSheet.Appearance.Embedded.RowStyle { + return when (embeddedRowStyle) { + EmbeddedRow.FlatWithRadio -> PaymentSheet.Appearance.Embedded.RowStyle.FlatWithRadio( + separatorThicknessDp = separatorThicknessDp, + separatorColor = separatorColor, + separatorInsetsDp = separatorInsetsDp, + topSeparatorEnabled = topSeparatorEnabled, + bottomSeparatorEnabled = bottomSeparatorEnabled, + selectedColor = selectedColor, + unselectedColor = unselectedColor, + additionalInsetsDp = additionalInsetsDp + ) + EmbeddedRow.FlatWithCheckmark -> PaymentSheet.Appearance.Embedded.RowStyle.FlatWithCheckmark( + separatorThicknessDp = separatorThicknessDp, + separatorColor = separatorColor, + separatorInsetsDp = separatorInsetsDp, + topSeparatorEnabled = topSeparatorEnabled, + bottomSeparatorEnabled = bottomSeparatorEnabled, + checkmarkColor = checkmarkColor, + checkmarkInsetDp = checkmarkInsetsDp, + additionalInsetsDp = additionalInsetsDp + ) + EmbeddedRow.FloatingButton -> PaymentSheet.Appearance.Embedded.RowStyle.FloatingButton( + spacingDp = floatingButtonSpacingDp, + additionalInsetsDp = additionalInsetsDp + ) + } + } +} diff --git a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt index f398642e150..40625216c39 100644 --- a/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt +++ b/paymentsheet-example/src/main/java/com/stripe/android/paymentsheet/example/playground/settings/PlaygroundSettings.kt @@ -456,6 +456,7 @@ internal class PlaygroundSettings private constructor( AppearanceSettingsDefinition, CustomEndpointDefinition, ShippingAddressSettingsDefinition, + EmbeddedAppearanceSettingsDefinition ) private val allSettingDefinitions: List> =