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
This commit is contained in:
@@ -87,7 +87,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
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.unit.sp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.vitorpamplona.amethyst.R
|
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.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo
|
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.LoadAddressableNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
@@ -346,20 +347,14 @@ fun PayReaction(
|
|||||||
iconSizeModifier: Modifier = Size20Modifier,
|
iconSizeModifier: Modifier = Size20Modifier,
|
||||||
) {
|
) {
|
||||||
val authorPubkey = baseNote.author?.pubkeyHex ?: return
|
val authorPubkey = baseNote.author?.pubkeyHex ?: return
|
||||||
val address =
|
val address = remember(authorPubkey) { PaymentTargetsEvent.createAddress(authorPubkey) }
|
||||||
remember(authorPubkey) {
|
val context = LocalContext.current
|
||||||
PaymentTargetsEvent.createAddress(authorPubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadAddressableNote(address, accountViewModel) { note ->
|
LoadAddressableNote(address, accountViewModel) { note ->
|
||||||
val targets =
|
val targets = remember(note) { (note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList() }
|
||||||
remember(note) {
|
|
||||||
(note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList()
|
|
||||||
}
|
|
||||||
if (targets.isEmpty()) return@LoadAddressableNote
|
|
||||||
|
|
||||||
val uri = LocalUriHandler.current
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
ClickableBox(
|
ClickableBox(
|
||||||
modifier = iconSizeModifier,
|
modifier = iconSizeModifier,
|
||||||
@@ -379,14 +374,27 @@ fun PayReaction(
|
|||||||
onDismiss = { expanded = false },
|
onDismiss = { expanded = false },
|
||||||
) {
|
) {
|
||||||
M3ActionSection {
|
M3ActionSection {
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
M3ActionRow(
|
||||||
|
icon = Icons.Outlined.AccountBalanceWallet,
|
||||||
|
text = stringRes(R.string.no_payment_targets_message),
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
targets.forEach { target ->
|
targets.forEach { target ->
|
||||||
M3ActionRow(
|
M3ActionRow(
|
||||||
icon = Icons.Outlined.AccountBalanceWallet,
|
icon = Icons.Outlined.AccountBalanceWallet,
|
||||||
text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}",
|
text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}",
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
runCatching {
|
try {
|
||||||
uri.openUri("payto://${target.type}/${target.authority}")
|
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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -395,6 +403,15 @@ fun PayReaction(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errorMessage?.let { msg ->
|
||||||
|
ErrorMessageDialog(
|
||||||
|
title = stringRes(R.string.error_dialog_payment_error),
|
||||||
|
textContent = msg,
|
||||||
|
onDismiss = { errorMessage = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
+30
-6
@@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -32,13 +33,15 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
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.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
import com.vitorpamplona.amethyst.ui.components.M3ActionDialog
|
||||||
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
import com.vitorpamplona.amethyst.ui.components.M3ActionRow
|
||||||
import com.vitorpamplona.amethyst.ui.components.M3ActionSection
|
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.note.LoadAddressableNote
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -61,16 +64,15 @@ fun PaymentButton(
|
|||||||
remember(note) {
|
remember(note) {
|
||||||
(note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList()
|
(note?.event as? PaymentTargetsEvent)?.paymentTargets() ?: emptyList()
|
||||||
}
|
}
|
||||||
if (targets.isNotEmpty()) {
|
|
||||||
PaymentButtonWithTargets(targets)
|
PaymentButtonWithTargets(targets)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PaymentButtonWithTargets(targets: List<PaymentTarget>) {
|
fun PaymentButtonWithTargets(targets: List<PaymentTarget>) {
|
||||||
val uri = LocalUriHandler.current
|
val context = LocalContext.current
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -92,14 +94,27 @@ fun PaymentButtonWithTargets(targets: List<PaymentTarget>) {
|
|||||||
onDismiss = { expanded = false },
|
onDismiss = { expanded = false },
|
||||||
) {
|
) {
|
||||||
M3ActionSection {
|
M3ActionSection {
|
||||||
|
if (targets.isEmpty()) {
|
||||||
|
M3ActionRow(
|
||||||
|
icon = Icons.Outlined.AccountBalanceWallet,
|
||||||
|
text = stringRes(R.string.no_payment_targets_message),
|
||||||
|
enabled = false,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
targets.forEach { target ->
|
targets.forEach { target ->
|
||||||
M3ActionRow(
|
M3ActionRow(
|
||||||
icon = Icons.Outlined.AccountBalanceWallet,
|
icon = Icons.Outlined.AccountBalanceWallet,
|
||||||
text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}",
|
text = "${target.type.replaceFirstChar(Char::titlecase)}: ${target.authority}",
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
runCatching {
|
try {
|
||||||
uri.openUri("payto://${target.type}/${target.authority}")
|
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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -107,4 +122,13 @@ fun PaymentButtonWithTargets(targets: List<PaymentTarget>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage?.let { msg ->
|
||||||
|
ErrorMessageDialog(
|
||||||
|
title = stringRes(R.string.error_dialog_payment_error),
|
||||||
|
textContent = msg,
|
||||||
|
onDismiss = { errorMessage = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -583,6 +583,8 @@
|
|||||||
<string name="payment_target_type">Network type (e.g. bitcoin)</string>
|
<string name="payment_target_type">Network type (e.g. bitcoin)</string>
|
||||||
<string name="payment_target_authority">Address</string>
|
<string name="payment_target_authority">Address</string>
|
||||||
<string name="delete_payment_target">Delete payment target</string>
|
<string name="delete_payment_target">Delete payment target</string>
|
||||||
|
<string name="no_payment_app_found">No app found to handle this payment. Please install a compatible wallet.</string>
|
||||||
|
<string name="error_dialog_payment_error">Unable to open payment</string>
|
||||||
|
|
||||||
<string name="uploading_state_ready">Not Started</string>
|
<string name="uploading_state_ready">Not Started</string>
|
||||||
<string name="uploading_state_compressing">Compressing</string>
|
<string name="uploading_state_compressing">Compressing</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user