ProcessPaymentFragment.kt
/*
* This file is part of GNU Taler
* (C) 2020 Taler Systems S.A.
*
* GNU Taler is free software; you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3, or (at your option) any later version.
*
* GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
package net.taler.merchantpos.payment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import net.taler.common.Timestamp
import net.taler.lib.android.AnimatedQrCodeComposable
import net.taler.lib.android.AnimatedQrCodeFrame
import net.taler.lib.android.TalerNfcService.Companion.hasNfc
import net.taler.lib.android.copyToClipBoard
import net.taler.lib.android.shareText
import net.taler.merchantpos.MainActivity
import net.taler.merchantpos.MainViewModel
import net.taler.merchantpos.PosDestination
import net.taler.merchantpos.R
import net.taler.merchantpos.checkout.CheckoutResult
import net.taler.merchantpos.checkout.RevertResult
import net.taler.merchantpos.compose.PosTheme
import net.taler.merchantpos.devices.TalerBleStatus
import net.taler.merchantpos.devices.TalerUsbStatus
import net.taler.merchantpos.showPosError
import java.text.DateFormat
import java.util.Date
import kotlinx.coroutines.launch
class ProcessPaymentFragment : Fragment()
{
private val model: MainViewModel by activityViewModels()
private val paymentManager by lazy { model.paymentManager }
private var deviceHasNfc = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
) = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val scope = rememberCoroutineScope()
val payment by paymentManager.payment.observeAsState()
val usb by model.talerUsb.state.observeAsState()
val bluetooth by model.talerBluetooth.state.observeAsState()
val deleteNeedsForce by paymentManager.deleteNeedsForce.observeAsState()
payment?.let {
ProcessPaymentScreen(
payment = it,
deviceHasNfc =
deviceHasNfc || bluetooth?.status == TalerBleStatus.Ready || (
usb?.enabled == true &&
usb?.devices?.any { it.status == TalerUsbStatus.Ready } == true
),
showForceDeleteDialog = deleteNeedsForce == true,
onTryHttps = paymentManager::tryHttpsPaymentUri,
onContinueAsIs = paymentManager::continueWithInsecurePaymentUri,
onCancelInsecurePayment = paymentManager::tryDeleteOrder,
onCancel = {
if (it.checkoutId != null)
{
scope.launch {
when (val result = model.checkoutManager.revertTalerStep())
{
RevertResult.Reverted ->
{
paymentManager.cancelPayment()
(requireActivity() as MainActivity).returnToCheckout()
}
RevertResult.AlreadyPaid ->
{
(requireActivity() as MainActivity).navigateTo(PosDestination.PaymentSuccess)
}
is RevertResult.Failed ->
{
requireActivity().showPosError(
getString(R.string.checkout_revert_failed),
result.message,
)
}
}
}
}
else if (it.claimed)
{
paymentManager.tryDeleteOrder()
}
else
{
onPaymentCancel()
}
},
onForceDelete = ::onForceDeleteOrder,
onDismissForceDelete = {
paymentManager.clearDeleteNeedsForce()
},
onShare = { uri -> requireContext().shareText(uri) },
onCopy = { uri ->
copyToClipBoard(requireContext(), "Payment URI", uri)
},
// For a checkout this step is reversible, so it is not a cancellation.
cancelLabel =
stringResource(
if (it.checkoutId != null)
{
R.string.checkout_back_to_options
}
else
{
R.string.payment_cancel
},
),
onSelfCollect =
if (it.selfCollect)
{
{
scope.launch {
when (val result = model.checkoutManager.collectByMerchant())
{
CheckoutResult.Completed ->
{
(requireActivity() as MainActivity).navigateTo(
PosDestination.PaymentSuccess,
)
}
is CheckoutResult.Failed ->
{
requireActivity().showPosError(
getString(R.string.error_payment),
result.message,
)
}
is CheckoutResult.AwaitingWallet ->
{
Unit
}
}
}
}
}
else
{
null
},
)
}
// Navigate back on successful (non-force) delete
LaunchedEffect(deleteNeedsForce) {
if (deleteNeedsForce == false)
{
paymentManager.clearDeleteNeedsForce()
(requireActivity() as MainActivity).navigateBack()
}
}
}
}
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
)
{
deviceHasNfc = hasNfc(requireContext())
paymentManager.payment.observe(viewLifecycleOwner) { payment ->
onPaymentStateChanged(payment)
}
}
override fun onStart()
{
super.onStart()
updateUsbUri(paymentManager.payment.value)
}
override fun onStop()
{
model.talerUsb.dismiss(this)
model.talerBluetooth.dismiss(this)
super.onStop()
}
private fun updateUsbUri(payment: Payment?)
{
val uri =
payment
?.takeIf {
!it.claimed && !it.paid && it.error == null
}?.talerPayUri
model.talerUsb.present(this, uri)
model.talerBluetooth.present(this, uri)
}
private fun onPaymentStateChanged(payment: Payment?)
{
updateUsbUri(payment)
payment ?: return
if (payment.error != null)
{
val (mainText, detailText) = getPaymentErrorDisplay(payment)
requireActivity().showPosError(mainText, detailText)
(requireActivity() as MainActivity).navigateBack()
return
}
if (payment.paid)
{
val orderId = payment.orderId
if (payment.checkoutId != null &&
orderId != null)
{
model.checkoutManager.onBackendOrderPaid(orderId)
}
else
{
model.orderManager.onOrderPaid(payment.order.id)
}
(requireActivity() as MainActivity).navigateTo(PosDestination.PaymentSuccess)
}
}
private fun onPaymentCancel()
{
paymentManager.cancelPayment()
(requireActivity() as MainActivity).navigateBack()
}
private fun onForceDeleteOrder()
{
paymentManager.forceDeleteOrder()
(requireActivity() as MainActivity).navigateBack()
}
private fun getPaymentErrorDisplay(payment: Payment): Pair<String, String>
{
val error = payment.error.orEmpty()
if (payment.orderId != null)
{
return getString(R.string.error_payment) to error
}
val normalized = error.lowercase()
return when
{
"inventory" in normalized ||
"stock" in normalized ||
"insufficient" in normalized ||
"sold out" in normalized ||
"out of stock" in normalized ->
{
getString(R.string.error_inventory_unavailable) to error
}
else ->
{
getString(R.string.error_order_creation) to error
}
}
}
}
@Composable
private fun ProcessPaymentScreen(
payment: Payment,
deviceHasNfc: Boolean,
showForceDeleteDialog: Boolean,
onTryHttps: () -> Unit,
onContinueAsIs: () -> Unit,
onCancelInsecurePayment: () -> Unit,
onCancel: () -> Unit,
onForceDelete: () -> Unit,
onDismissForceDelete: () -> Unit,
onShare: (String) -> Unit,
onCopy: (String) -> Unit,
/** Offered only when the merchant reached this from a fully settled checkout. */
onSelfCollect: (() -> Unit)?,
cancelLabel: String,
)
{
if (showForceDeleteDialog)
{
AlertDialog(
onDismissRequest = onDismissForceDelete,
title = { Text(stringResource(R.string.force_delete_dialog_title)) },
text = { Text(stringResource(R.string.force_delete_dialog_message)) },
confirmButton = {
Button(
onClick = onForceDelete,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
),
) {
Text(stringResource(R.string.force_delete_dialog_confirm))
}
},
dismissButton = {
OutlinedButton(onClick = onDismissForceDelete) {
Text(stringResource(R.string.payment_cancel))
}
},
)
}
else if (payment.insecureTalerPayUri != null)
{
AlertDialog(
onDismissRequest = {},
title = { Text(stringResource(R.string.insecure_payment_dialog_title)) },
text = { Text(stringResource(R.string.insecure_payment_dialog_message)) },
confirmButton = {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Button(
onClick = onTryHttps,
modifier = Modifier.fillMaxWidth(),
) {
Text(stringResource(R.string.insecure_payment_dialog_try_https))
}
OutlinedButton(
onClick = onContinueAsIs,
modifier = Modifier.fillMaxWidth(),
) {
Text(stringResource(R.string.insecure_payment_dialog_continue_as_is))
}
OutlinedButton(
onClick = onCancelInsecurePayment,
modifier = Modifier.fillMaxWidth(),
colors =
ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error,
),
) {
Text(stringResource(R.string.insecure_payment_dialog_cancel_order))
}
}
},
)
}
PosTheme {
val introText =
when
{
payment.claimed -> stringResource(R.string.checkout_complete_in_wallet)
payment.selfCollect -> stringResource(R.string.checkout_collect_receipt_title)
deviceHasNfc && payment.talerPayUri != null -> stringResource(R.string.payment_intro_nfc)
else -> stringResource(R.string.checkout_scan_qr_prompt)
}
val isTablet = LocalConfiguration.current.smallestScreenWidthDp >= 600
Surface(Modifier.fillMaxSize()) {
if (payment.statusLoading)
{
Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
) {
CircularProgressIndicator()
Text(stringResource(R.string.order_details_loading), textAlign = TextAlign.Center)
OutlinedButton(onClick = onCancel) { Text(cancelLabel) }
}
return@Surface
}
BoxWithConstraints(Modifier.fillMaxSize()) {
if (maxWidth > maxHeight)
{
LandscapePaymentScreen(
payment,
introText,
isTablet,
onCancel,
onShare,
onCopy,
onSelfCollect,
cancelLabel,
)
}
else
{
PortraitPaymentScreen(
payment,
introText,
isTablet,
onCancel,
onShare,
onCopy,
onSelfCollect,
cancelLabel,
)
}
}
}
}
}
@Composable
private fun PaymentQrContent(
payment: Payment,
modifier: Modifier = Modifier,
)
{
if (payment.claimed)
{
AnimatedQrCodeFrame(modifier) {
Column(
modifier =
Modifier
.padding(24.dp)
.verticalScroll(rememberScrollState())
.semantics { liveRegion = LiveRegionMode.Polite },
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
// The QR frame stays light in both themes.
Text(
stringResource(R.string.checkout_order_claimed),
style = MaterialTheme.typography.titleMedium,
color = Color(0xFF1C1B1F),
textAlign = TextAlign.Center,
)
Text(
stringResource(R.string.checkout_waiting_customer_payment),
style = MaterialTheme.typography.titleMedium,
color = Color(0xFF1C1B1F),
textAlign = TextAlign.Center,
)
}
}
}
else
{
val payUri = payment.talerPayUri
if (payUri == null)
{
Box(modifier, contentAlignment = Alignment.Center) {
CircularProgressIndicator(Modifier.size(48.dp))
}
}
else
{
AnimatedQrCodeComposable(
link = payUri,
logoPainter = painterResource(R.drawable.ic_taler_logo_qr),
modifier = modifier,
)
}
}
}
@Composable
private fun PaymentBrandHeading(compact: Boolean = false)
{
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
stringResource(R.string.payment_pay_with),
style = if (compact) MaterialTheme.typography.titleMedium else MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
)
Image(
painter = painterResource(R.drawable.ic_taler_logo_brand_on_light),
contentDescription = stringResource(R.string.payment_brand_name),
modifier =
Modifier.size(
width = if (compact) 72.dp else 80.dp,
height = if (compact) 32.dp else 36.dp,
),
colorFilter =
if (MaterialTheme.colorScheme.surface.luminance() < 0.5f)
{
ColorFilter.tint(Color.White)
}
else
{
null
},
)
}
}
@Composable
private fun PaymentDetailRow(
label: String,
value: String,
amount: Boolean = false,
compact: Boolean = false,
)
{
if (compact &&
LocalDensity.current.fontScale > 1.2f)
{
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(label, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
value,
style = if (amount) MaterialTheme.typography.headlineSmall else MaterialTheme.typography.bodyMedium,
fontWeight = if (amount) FontWeight.Bold else FontWeight.Normal,
)
}
return
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top,
) {
Text(
label,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.width(if (compact) 68.dp else 100.dp),
)
Text(
value,
style = if (amount) MaterialTheme.typography.headlineSmall else MaterialTheme.typography.bodyMedium,
fontWeight = if (amount) FontWeight.Bold else FontWeight.Normal,
modifier = Modifier.weight(1f),
)
}
}
@Composable
private fun PaymentDeadline(payment: Payment)
{
payment.payDeadline?.takeUnless { it == Timestamp.never() }?.let { deadline ->
val locale = LocalConfiguration.current.locales[0]
val formatted =
remember(deadline, locale) {
DateFormat
.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale)
.format(Date(deadline.ms))
}
Text(
stringResource(R.string.checkout_payment_possible_until, formatted),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun PaymentActions(
payment: Payment,
onCancel: () -> Unit,
onShare: (String) -> Unit,
onCopy: (String) -> Unit,
onSelfCollect: (() -> Unit)?,
cancelLabel: String,
)
{
Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
payment.talerPayUri?.let { payUri ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = { onShare(payUri) }, modifier = Modifier.weight(1f).heightIn(min = 48.dp)) {
Text(stringResource(R.string.share))
}
OutlinedButton(
onClick = { onCopy(payUri) },
modifier = Modifier.weight(1f).heightIn(min = 48.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.primary),
) {
Text(stringResource(R.string.copy))
}
}
}
onSelfCollect?.let { collect ->
Button(onClick = collect, modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp)) {
Text(stringResource(R.string.checkout_collect_receipt_on_device))
}
}
OutlinedButton(
onClick = onCancel,
modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp),
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.primary),
) {
Text(cancelLabel)
}
}
}
@Composable
private fun PortraitPaymentScreen(
payment: Payment,
introText: String,
isTablet: Boolean,
onCancel: () -> Unit,
onShare: (String) -> Unit,
onCopy: (String) -> Unit,
onSelfCollect: (() -> Unit)?,
cancelLabel: String,
)
{
BoxWithConstraints(Modifier.fillMaxSize()) {
val horizontalPadding = if (isTablet) 48.dp else 24.dp
val qrSize =
if (isTablet)
{
minOf(maxWidth * 0.56f, maxHeight * 0.44f, 420.dp)
}
else
{
minOf(maxWidth - horizontalPadding * 2, 320.dp)
}
Column(
modifier =
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = horizontalPadding, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
if (!payment.selfCollect) PaymentBrandHeading()
Text(introText, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center)
Text(
text =
if (payment.selfCollect)
{
stringResource(R.string.checkout_receipt_paid_in_full)
}
else
{
payment.order.total.toString()
},
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
)
PaymentQrContent(payment, Modifier.size(qrSize))
PaymentDeadline(payment)
if (payment.selfCollect)
{
Text(stringResource(R.string.checkout_receipt_scan_hint), textAlign = TextAlign.Center)
}
payment.orderId?.let { id ->
if (isTablet)
{
PaymentDetailRow(stringResource(R.string.checkout_payment_detail_order), "#$id")
}
else
{
Text(stringResource(R.string.payment_order_id, id), textAlign = TextAlign.Center)
}
}
if (payment.summary.isNotBlank())
{
if (isTablet)
{
PaymentDetailRow(stringResource(R.string.checkout_payment_detail_summary), payment.summary)
}
else
{
Text(
payment.summary,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
PaymentActions(payment, onCancel, onShare, onCopy, onSelfCollect, cancelLabel)
}
}
}
@Composable
private fun LandscapePaymentScreen(
payment: Payment,
introText: String,
isTablet: Boolean,
onCancel: () -> Unit,
onShare: (String) -> Unit,
onCopy: (String) -> Unit,
onSelfCollect: (() -> Unit)?,
cancelLabel: String,
)
{
BoxWithConstraints(Modifier.fillMaxSize()) {
val bodyHeight =
minOf(
(maxHeight - 128.dp * LocalDensity.current.fontScale).coerceAtLeast(0.dp),
460.dp,
)
Column(
modifier =
Modifier
.fillMaxSize()
.padding(
horizontal = if (isTablet) 48.dp else 24.dp,
vertical = 16.dp,
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(if (isTablet) 24.dp else 12.dp, Alignment.CenterVertically),
) {
if (isTablet)
{
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
) {
if (!payment.selfCollect) PaymentBrandHeading()
Text(introText, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center)
}
}
Row(
modifier =
if (isTablet)
{
Modifier.fillMaxWidth().height(bodyHeight)
}
else
{
Modifier.weight(1f).fillMaxWidth()
},
horizontalArrangement = Arrangement.spacedBy(if (isTablet) 48.dp else 32.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(if (isTablet) 1f else 0.85f).fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
) {
BoxWithConstraints(
Modifier.weight(1f, fill = false).fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
val qrSize = minOf(maxWidth, maxHeight, if (isTablet) 420.dp else 360.dp)
PaymentQrContent(payment, Modifier.size(qrSize))
}
PaymentDeadline(payment)
}
Column(
modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(if (isTablet) 20.dp else 12.dp),
) {
if (!isTablet)
{
if (!payment.selfCollect) PaymentBrandHeading(compact = true)
Text(introText, style = MaterialTheme.typography.bodyMedium)
}
payment.orderId?.let { id ->
PaymentDetailRow(
stringResource(R.string.checkout_payment_detail_order),
"#$id",
compact = !isTablet,
)
}
if (payment.summary.isNotBlank())
{
PaymentDetailRow(
stringResource(R.string.checkout_payment_detail_summary),
payment.summary,
compact = !isTablet,
)
}
if (payment.selfCollect)
{
Text(
stringResource(R.string.checkout_receipt_paid_in_full),
style = MaterialTheme.typography.headlineSmall,
)
Text(stringResource(R.string.checkout_receipt_scan_hint))
}
else
{
PaymentDetailRow(
stringResource(R.string.amount_entry_label),
payment.order.total.toString(),
amount = true,
compact = !isTablet,
)
}
if (isTablet)
{
payment.merchantName?.takeIf { it.isNotBlank() }?.let { name ->
PaymentDetailRow(stringResource(R.string.checkout_payment_detail_merchant), name)
}
}
PaymentActions(payment, onCancel, onShare, onCopy, onSelfCollect, cancelLabel)
}
}
}
}
}
@Composable
internal fun PaymentScreenContent(payment: Payment)
{
ProcessPaymentScreen(
payment = payment,
deviceHasNfc = false,
showForceDeleteDialog = false,
onTryHttps = {},
onContinueAsIs = {},
onCancelInsecurePayment = {},
onCancel = {},
onForceDelete = {},
onDismissForceDelete = {},
onShare = {},
onCopy = {},
onSelfCollect = if (payment.selfCollect) ({}) else null,
cancelLabel =
stringResource(
if (payment.checkoutId != null) R.string.checkout_back_to_options else R.string.payment_cancel,
),
)
}