PaymentTiles.kt

/*
 * This file is part of GNU Taler
 * (C) 2026 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.compose

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
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 net.taler.merchantpos.R

enum class PaymentTileStyle
{
    Primary,
    PrimaryContainer,
    Secondary,
    SecondaryContainer,
    Tertiary,
    TertiaryContainer,
    Surface,
}

private data class PaymentTileColors(
    val container: Color,
    val content: Color,
)

private fun PaymentTileStyle.colors(scheme: ColorScheme): PaymentTileColors =
    with(scheme) {
        when (this@colors)
        {
            PaymentTileStyle.Primary -> PaymentTileColors(primary, onPrimary)
            PaymentTileStyle.PrimaryContainer -> PaymentTileColors(primaryContainer, onPrimaryContainer)
            PaymentTileStyle.Secondary -> PaymentTileColors(secondary, onSecondary)
            PaymentTileStyle.SecondaryContainer -> PaymentTileColors(secondaryContainer, onSecondaryContainer)
            PaymentTileStyle.Tertiary -> PaymentTileColors(tertiary, onTertiary)
            PaymentTileStyle.TertiaryContainer -> PaymentTileColors(tertiaryContainer, onTertiaryContainer)
            PaymentTileStyle.Surface -> PaymentTileColors(surface, onSurface)
        }
    }

/** Omit [onClick] to display a non-interactive header. */
@Composable
fun TalerPaymentTile(
    title: String,
    detail: String?,
    modifier: Modifier = Modifier,
    style: PaymentTileStyle = PaymentTileStyle.SecondaryContainer,
    amount: String? = null,
    enabled: Boolean = true,
    onClick: (() -> Unit)? = null,
)
{
    PaymentTile(title, detail, modifier, style, amount, enabled, onClick) {
        val logoModifier = Modifier.size(width = 56.dp, height = 26.dp)
        val contentColor = LocalContentColor.current
        // Dark themes also have light tiles: choose the logo from the tile's foreground,
        // not the system night mode. Disabled logos follow the disabled foreground.
        if (!enabled ||
            contentColor.luminance() > 0.5f)
        {
            Icon(
                painter = painterResource(R.drawable.ic_taler_logo_full),
                contentDescription = null,
                modifier = logoModifier,
                tint = if (enabled) Color.White else contentColor,
            )
        }
        else
        {
            Image(
                painter = painterResource(R.drawable.ic_taler_logo_brand_on_light),
                contentDescription = null,
                modifier = logoModifier,
            )
        }
    }
}

/** Omit [onClick] to display a non-interactive header. */
@Composable
fun PaymentMethodTile(
    icon: ImageVector,
    title: String,
    detail: String?,
    modifier: Modifier = Modifier,
    style: PaymentTileStyle = PaymentTileStyle.PrimaryContainer,
    amount: String? = null,
    enabled: Boolean = true,
    onClick: (() -> Unit)? = null,
)
{
    PaymentTile(title, detail, modifier, style, amount, enabled, onClick) {
        Icon(imageVector = icon, contentDescription = null, modifier = Modifier.size(32.dp))
    }
}

@Composable
private fun PaymentTile(
    title: String,
    detail: String?,
    modifier: Modifier,
    style: PaymentTileStyle,
    amount: String?,
    enabled: Boolean,
    onClick: (() -> Unit)?,
    icon: @Composable () -> Unit,
)
{
    val scheme = MaterialTheme.colorScheme
    val colors =
        if (enabled)
        {
            style.colors(scheme)
        }
        else
        {
            PaymentTileColors(scheme.surfaceVariant, scheme.onSurfaceVariant)
        }
    val border =
        BorderStroke(
            1.dp,
            if (!enabled ||
                style == PaymentTileStyle.Surface)
            {
                scheme.outlineVariant
            }
            else
            {
                colors.content.copy(alpha = 0.35f)
            },
        )
    val shape = RoundedCornerShape(16.dp)
    val tileModifier = modifier.fillMaxWidth().heightIn(min = 72.dp)
    val fontScale = LocalDensity.current.fontScale
    val content: @Composable () -> Unit = {
        BoxWithConstraints(Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
            // Keep long amounts out of the title's width on phones and with large text.
            val trailingAmount = amount != null && maxWidth >= 400.dp && fontScale <= 1.3f
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(16.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                icon()
                Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
                    Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
                    detail?.let { Text(it, style = MaterialTheme.typography.bodyMedium) }
                    if (amount != null &&
                        !trailingAmount)
                    {
                        Text(amount, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
                    }
                }
                if (trailingAmount)
                {
                    Text(
                        text = amount.orEmpty(),
                        modifier = Modifier.weight(0.6f),
                        style = MaterialTheme.typography.titleMedium,
                        fontWeight = FontWeight.SemiBold,
                        textAlign = TextAlign.End,
                    )
                }
            }
        }
    }
    if (onClick == null)
    {
        Surface(
            modifier = tileModifier,
            shape = shape,
            color = colors.container,
            contentColor = colors.content,
            border = border,
            content = content,
        )
    }
    else
    {
        Surface(
            onClick = onClick,
            enabled = enabled,
            modifier = tileModifier.semantics { role = Role.Button },
            shape = shape,
            color = colors.container,
            contentColor = colors.content,
            border = border,
            content = content,
        )
    }
}