From ec28d4eb4c4e9a4ebe64e82efc001841fed4766e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 20 May 2026 16:35:47 +0000 Subject: [PATCH] feat: redesign payment targets modal with QR, copy and pay buttons Each target now renders in a single row with the payment method and address on the left and three trailing icon buttons: QR (opens a dialog showing the raw address as a QR code), Copy (clipboard + toast), and Pay (existing payto:// intent). Adds QrCode2 to MaterialSymbols. https://claude.ai/code/session_01JtJuvSYMusKiDMWo7N8Aj8 --- .../loggedIn/profile/header/PaymentButton.kt | 143 ++++++++++++++++-- .../commons/icons/symbols/MaterialSymbols.kt | 1 + 2 files changed, 128 insertions(+), 16 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt index 173bc6423d..b95bb1f5df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/PaymentButton.kt @@ -21,19 +21,36 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -42,13 +59,14 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.M3ActionDialog -import com.vitorpamplona.amethyst.ui.components.M3ActionRow import com.vitorpamplona.amethyst.ui.components.M3ActionSection import com.vitorpamplona.amethyst.ui.components.util.setText import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.ZeroPadding import com.vitorpamplona.quartz.experimental.nipA3.PaymentTarget import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent @@ -86,6 +104,7 @@ fun PaymentButtonWithTargets(targets: List) { val scope = rememberCoroutineScope() var expanded by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } + var qrContent by remember { mutableStateOf(null) } FilledTonalButton( modifier = @@ -108,36 +127,41 @@ fun PaymentButtonWithTargets(targets: List) { ) { M3ActionSection { targets.forEach { target -> - M3ActionRow( - icon = MaterialSymbols.AccountBalanceWallet, - text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}", - onClick = { - expanded = false + PaymentTargetRow( + target = target, + onShowQr = { qrContent = target.authority }, + onCopy = { + scope.launch { + clipboardManager.setText(target.authority) + Toast + .makeText( + context, + stringRes(context, R.string.copied_to_clipboard), + Toast.LENGTH_SHORT, + ).show() + } + }, + onPay = { try { val intent = Intent(Intent.ACTION_VIEW, "payto://${target.type}/${target.authority}".toUri()) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK context.startActivity(intent) + expanded = false } catch (e: Exception) { if (e is kotlinx.coroutines.CancellationException) throw e errorMessage = stringRes(context, R.string.no_payment_app_found) } }, ) - M3ActionRow( - icon = MaterialSymbols.ContentCopy, - text = stringRes(R.string.copy_to_clipboard), - onClick = { - expanded = false - scope.launch { - clipboardManager.setText(target.authority) - } - }, - ) } } } } + qrContent?.let { value -> + PaymentTargetQrDialog(value = value, onDismiss = { qrContent = null }) + } + errorMessage?.let { msg -> ErrorMessageDialog( title = stringRes(R.string.error_dialog_payment_error), @@ -146,3 +170,90 @@ fun PaymentButtonWithTargets(targets: List) { ) } } + +@Composable +private fun PaymentTargetRow( + target: PaymentTarget, + onShowQr: () -> Unit, + onCopy: () -> Unit, + onPay: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = target.type.replaceFirstChar(Char::titlecase), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = target.authority, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + Spacer(modifier = Modifier.width(8.dp)) + IconButton(onClick = onShowQr) { + Icon( + symbol = MaterialSymbols.QrCode2, + contentDescription = stringRes(R.string.show_qr), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onCopy) { + Icon( + symbol = MaterialSymbols.ContentCopy, + contentDescription = stringRes(R.string.copy_to_clipboard), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onPay) { + Icon( + symbol = MaterialSymbols.Bolt, + contentDescription = stringRes(R.string.payment_targets), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun PaymentTargetQrDialog( + value: String, + onDismiss: () -> Unit, +) { + Dialog(onDismissRequest = onDismiss) { + Surface( + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + QrCodeDrawer( + contents = value, + modifier = Modifier.size(260.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt index 1530c9af57..db0d05359a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/symbols/MaterialSymbols.kt @@ -175,6 +175,7 @@ object MaterialSymbols { val Public = MaterialSymbol("\uE80B") val PublicOff = MaterialSymbol("\uF1CA") val PushPin = MaterialSymbol("\uF10D") + val QrCode2 = MaterialSymbol("\uEF6B") val RadioButtonChecked = MaterialSymbol("\uE837") val RadioButtonUnchecked = MaterialSymbol("\uE836") val Recommend = MaterialSymbol("\uE9D2")