From c803c06607a6ccd96299e4504b464736f4111a6e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 19:54:29 +0000 Subject: [PATCH] feat: show error dialog when payto:// URI fails to open - Use Intent(ACTION_VIEW) + startActivity instead of LocalUriHandler so ActivityNotFoundException is catchable - Show ErrorMessageDialog with title/message when no app handles the URI, matching the pattern used by zap payment errors - Always show the wallet icon (profile button + reactions row), showing a disabled "no targets" row in the dialog when the author has none set - Add no_payment_app_found and error_dialog_payment_error string resources https://claude.ai/code/session_018G4g1cChqjeNEM5TztD3FE --- .../amethyst/ui/note/ReactionsRow.kt | 55 ++++++++++++------- .../loggedIn/profile/header/PaymentButton.kt | 50 ++++++++++++----- amethyst/src/main/res/values/strings.xml | 2 + 3 files changed, 75 insertions(+), 32 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 01b0d5a98..a332565d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -87,7 +87,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextAlign @@ -99,6 +98,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties +import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.R @@ -132,6 +132,7 @@ import com.vitorpamplona.amethyst.ui.components.toasts.multiline.UserBasedErrorM import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo +import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -346,20 +347,14 @@ fun PayReaction( iconSizeModifier: Modifier = Size20Modifier, ) { val authorPubkey = baseNote.author?.pubkeyHex ?: return - val address = - remember(authorPubkey) { - PaymentTargetsEvent.createAddress(authorPubkey) - } + val address = remember(authorPubkey) { PaymentTargetsEvent.createAddress(authorPubkey) } + val context = LocalContext.current LoadAddressableNote(address, accountViewModel) { note -> - val targets = - remember(note) { - (note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList() - } - if (targets.isEmpty()) return@LoadAddressableNote + val targets = remember(note) { (note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList() } - val uri = LocalUriHandler.current var expanded by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } ClickableBox( modifier = iconSizeModifier, @@ -379,21 +374,43 @@ fun PayReaction( onDismiss = { expanded = false }, ) { M3ActionSection { - targets.forEach { target -> + if (targets.isEmpty()) { M3ActionRow( icon = Icons.Outlined.AccountBalanceWallet, - text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}", - onClick = { - expanded = false - runCatching { - uri.openUri("payto://${target.type}/${target.authority}") - } - }, + text = stringRes(R.string.no_payment_targets_message), + enabled = false, + onClick = {}, ) + } else { + targets.forEach { target -> + M3ActionRow( + icon = Icons.Outlined.AccountBalanceWallet, + text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}", + onClick = { + expanded = false + 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) + } catch (e: Exception) { + if (e is kotlinx.coroutines.CancellationException) throw e + errorMessage = stringRes(context, R.string.no_payment_app_found) + } + }, + ) + } } } } } + + errorMessage?.let { msg -> + ErrorMessageDialog( + title = stringRes(R.string.error_dialog_payment_error), + textContent = msg, + onDismiss = { errorMessage = null }, + ) + } } } 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 d01d76beb..84e3a2cf4 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 @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header +import android.content.Intent import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons @@ -32,13 +33,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User 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.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -61,16 +64,15 @@ fun PaymentButton( remember(note) { (note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList() } - if (targets.isNotEmpty()) { - PaymentButtonWithTargets(targets) - } + PaymentButtonWithTargets(targets) } } @Composable fun PaymentButtonWithTargets(targets: List) { - val uri = LocalUriHandler.current + val context = LocalContext.current var expanded by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } FilledTonalButton( modifier = @@ -92,19 +94,41 @@ fun PaymentButtonWithTargets(targets: List) { onDismiss = { expanded = false }, ) { M3ActionSection { - targets.forEach { target -> + if (targets.isEmpty()) { M3ActionRow( icon = Icons.Outlined.AccountBalanceWallet, - text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}", - onClick = { - expanded = false - runCatching { - uri.openUri("payto://${target.type}/${target.authority}") - } - }, + text = stringRes(R.string.no_payment_targets_message), + enabled = false, + onClick = {}, ) + } else { + targets.forEach { target -> + M3ActionRow( + icon = Icons.Outlined.AccountBalanceWallet, + text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}", + onClick = { + expanded = false + 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) + } catch (e: Exception) { + if (e is kotlinx.coroutines.CancellationException) throw e + errorMessage = stringRes(context, R.string.no_payment_app_found) + } + }, + ) + } } } } } + + errorMessage?.let { msg -> + ErrorMessageDialog( + title = stringRes(R.string.error_dialog_payment_error), + textContent = msg, + onDismiss = { errorMessage = null }, + ) + } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 20601bae8..3c28d66d2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -583,6 +583,8 @@ Network type (e.g. bitcoin) Address Delete payment target + No app found to handle this payment. Please install a compatible wallet. + Unable to open payment Not Started Compressing