diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/services/lnurl/LightningAddressResolver.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/services/lnurl/LightningAddressResolver.kt index d6b7768e16..bde4e94d31 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/services/lnurl/LightningAddressResolver.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/services/lnurl/LightningAddressResolver.kt @@ -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 diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/wallet/WalletColumnScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/wallet/WalletColumnScreen.kt index c721a776b7..be6237263d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/wallet/WalletColumnScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/wallet/WalletColumnScreen.kt @@ -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(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