mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
fix(desktop): improve send/receive dialogs and LNURL error surfacing
SendDialog: switch to Dialog+Card with X close, inline copiable error messages, button resets to "Pay Invoice" on error for retry. LightningAddressResolver: return error body from callback responses so server error messages (e.g. "Recipient wallet error") surface to user instead of generic "Failed to fetch invoice". Also check "message" field in addition to "reason" for error extraction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
00708d92d3
commit
4936d187fe
+6
-7
@@ -143,7 +143,7 @@ class LightningAddressResolver(
|
||||
milliSats = milliSats,
|
||||
message = message,
|
||||
zapRequest = if (allowsNostr) zapRequest else null,
|
||||
) ?: return@withContext Result.Error("Failed to fetch invoice from callback")
|
||||
) ?: return@withContext Result.Error("Failed to connect to payment server")
|
||||
|
||||
onProgress(0.7f)
|
||||
|
||||
@@ -157,7 +157,9 @@ class LightningAddressResolver(
|
||||
val pr = invoiceResponse?.get("pr")?.asText()?.ifBlank { null }
|
||||
|
||||
if (pr == null) {
|
||||
val reason = invoiceResponse?.get("reason")?.asText()?.ifBlank { null }
|
||||
val reason =
|
||||
invoiceResponse?.get("reason")?.asText()?.ifBlank { null }
|
||||
?: invoiceResponse?.get("message")?.asText()?.ifBlank { null }
|
||||
return@withContext Result.Error(reason ?: "No invoice in response")
|
||||
}
|
||||
|
||||
@@ -221,11 +223,8 @@ class LightningAddressResolver(
|
||||
|
||||
val request = Request.Builder().url(url).build()
|
||||
httpClient.newCall(request).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
response.body?.string()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// Return body even on error — caller extracts "reason" or "message" from JSON
|
||||
response.body?.string()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
+106
-47
@@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
@@ -253,25 +254,12 @@ fun WalletColumnScreen(
|
||||
if (showSendDialog && nwcConnection != null) {
|
||||
SendDialog(
|
||||
onDismiss = { showSendDialog = false },
|
||||
onSend = { invoice ->
|
||||
scope.launch {
|
||||
val result = paymentHandler.payInvoice(bolt11 = invoice, nwcConnection = nwcConnection)
|
||||
when (result) {
|
||||
is NwcPaymentHandler.PaymentResult.Success -> {
|
||||
showSendDialog = false
|
||||
snackbarHostState.showSnackbar("Payment successful!")
|
||||
}
|
||||
|
||||
is NwcPaymentHandler.PaymentResult.Error -> {
|
||||
snackbarHostState.showSnackbar("Error: ${result.message}")
|
||||
}
|
||||
|
||||
is NwcPaymentHandler.PaymentResult.Timeout -> {
|
||||
snackbarHostState.showSnackbar("Payment timed out")
|
||||
}
|
||||
}
|
||||
}
|
||||
onSuccess = {
|
||||
showSendDialog = false
|
||||
scope.launch { snackbarHostState.showSnackbar("Payment successful!") }
|
||||
},
|
||||
paymentHandler = paymentHandler,
|
||||
nwcConnection = nwcConnection,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -468,25 +456,54 @@ private fun ConnectWalletDialog(
|
||||
@Composable
|
||||
private fun SendDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSend: (String) -> Unit,
|
||||
onSuccess: () -> Unit,
|
||||
paymentHandler: NwcPaymentHandler,
|
||||
nwcConnection: Nip47URINorm,
|
||||
) {
|
||||
var invoice by remember { mutableStateOf("") }
|
||||
var isSending by remember { mutableStateOf(false) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Dialog(onDismissRequest = { if (!isSending) onDismiss() }) {
|
||||
Card(
|
||||
modifier = Modifier.width(480.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
"Send Payment",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = { if (!isSending) onDismiss() }) {
|
||||
Icon(
|
||||
MaterialSymbols.Close,
|
||||
contentDescription = "Close",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!isSending) onDismiss() },
|
||||
title = { Text("Send Payment") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
value = invoice,
|
||||
onValueChange = { invoice = it },
|
||||
onValueChange = {
|
||||
invoice = it
|
||||
errorMessage = null
|
||||
},
|
||||
label = { Text("BOLT11 Invoice") },
|
||||
placeholder = { Text("lnbc...") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = false,
|
||||
maxLines = 6,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
OutlinedButton(onClick = {
|
||||
val clipboard = Toolkit.getDefaultToolkit().systemClipboard
|
||||
val text =
|
||||
@@ -495,33 +512,75 @@ private fun SendDialog(
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
if (text != null) invoice = text
|
||||
if (text != null) {
|
||||
invoice = text
|
||||
errorMessage = null
|
||||
}
|
||||
}) {
|
||||
Text("Paste from Clipboard")
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
isSending = true
|
||||
onSend(invoice)
|
||||
},
|
||||
enabled = invoice.isNotBlank() && !isSending,
|
||||
) {
|
||||
if (isSending) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Sending...")
|
||||
} else {
|
||||
Text("Pay Invoice")
|
||||
|
||||
// Inline error — copiable
|
||||
if (errorMessage != null) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = errorMessage!!,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TextButton(onClick = onDismiss, enabled = !isSending) {
|
||||
Text("Cancel")
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
isSending = true
|
||||
errorMessage = null
|
||||
scope.launch {
|
||||
val result = paymentHandler.payInvoice(bolt11 = invoice, nwcConnection = nwcConnection)
|
||||
when (result) {
|
||||
is NwcPaymentHandler.PaymentResult.Success -> onSuccess()
|
||||
is NwcPaymentHandler.PaymentResult.Error -> {
|
||||
errorMessage = result.message
|
||||
isSending = false
|
||||
}
|
||||
is NwcPaymentHandler.PaymentResult.Timeout -> {
|
||||
errorMessage = "Payment timed out"
|
||||
isSending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = invoice.isNotBlank() && !isSending,
|
||||
) {
|
||||
if (isSending) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Sending...")
|
||||
} else {
|
||||
Text("Pay Invoice")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss, enabled = !isSending) { Text("Cancel") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user