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.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<String?>(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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+37
-13
@@ -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<PaymentTarget>) {
|
||||
val uri = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
FilledTonalButton(
|
||||
modifier =
|
||||
@@ -92,19 +94,41 @@ fun PaymentButtonWithTargets(targets: List<PaymentTarget>) {
|
||||
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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,6 +583,8 @@
|
||||
<string name="payment_target_type">Network type (e.g. bitcoin)</string>
|
||||
<string name="payment_target_authority">Address</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_compressing">Compressing</string>
|
||||
|
||||
Reference in New Issue
Block a user