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 2de8f2fcc4..3fc2e28b6d 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 @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.desktop.ui.wallet import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -30,8 +31,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -73,13 +76,6 @@ import java.awt.datatransfer.StringSelection import java.text.NumberFormat import java.util.Locale -enum class WalletScreen { - HOME, - CONNECT, - SEND, - RECEIVE, -} - @Composable fun WalletColumnScreen( account: AccountState.LoggedIn, @@ -92,28 +88,16 @@ fun WalletColumnScreen( ) { val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - var currentScreen by remember { mutableStateOf(WalletScreen.HOME) } - // NWC connection state - var nwcUri by remember { mutableStateOf("") } - var isConnecting by remember { mutableStateOf(false) } - var connectionError by remember { mutableStateOf(null) } + // Dialog visibility + var showConnectDialog by remember { mutableStateOf(false) } + var showSendDialog by remember { mutableStateOf(false) } + var showReceiveDialog by remember { mutableStateOf(false) } // Balance state var balanceSats by remember { mutableStateOf(null) } var isLoadingBalance by remember { mutableStateOf(false) } - // Send state - var sendInvoice by remember { mutableStateOf("") } - var isSending by remember { mutableStateOf(false) } - var sendResult by remember { mutableStateOf(null) } - - // Receive state - var receiveAmount by remember { mutableStateOf("") } - var receiveDescription by remember { mutableStateOf("") } - var generatedInvoice by remember { mutableStateOf(null) } - var isGenerating by remember { mutableStateOf(false) } - val paymentHandler = remember(relayManager, localCache) { NwcPaymentHandler(relayManager, localCache) @@ -128,24 +112,33 @@ fun WalletColumnScreen( balanceSats = result.balanceMsats / 1000 } - else -> { /* silently fail on auto-fetch */ } + else -> {} } isLoadingBalance = false } } - Column(modifier = Modifier.fillMaxSize()) { - when (currentScreen) { - WalletScreen.HOME -> { - WalletHomeContent( - nwcConnection = nwcConnection, - balanceSats = balanceSats, - isLoadingBalance = isLoadingBalance, - onConnect = { currentScreen = WalletScreen.CONNECT }, - onSend = { currentScreen = WalletScreen.SEND }, - onReceive = { currentScreen = WalletScreen.RECEIVE }, - onRefreshBalance = { - if (nwcConnection != null) { + Box(modifier = Modifier.fillMaxSize()) { + // Centered home content + Column( + modifier = + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier.widthIn(max = 360.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + if (nwcConnection == null) { + NoWalletContent(onConnect = { showConnectDialog = true }) + } else { + WalletBalanceCard( + balanceSats = balanceSats, + isLoading = isLoadingBalance, + onRefresh = { isLoadingBalance = true scope.launch { when (val result = paymentHandler.getBalance(nwcConnection)) { @@ -163,242 +156,146 @@ fun WalletColumnScreen( } isLoadingBalance = false } + }, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { showSendDialog = true }, + modifier = Modifier.weight(1f), + ) { + Icon(symbol = MaterialSymbols.ArrowUpward, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text("Send") } - }, - onDisconnect = { + OutlinedButton( + onClick = { showReceiveDialog = true }, + modifier = Modifier.weight(1f), + ) { + Icon(symbol = MaterialSymbols.ArrowDownward, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(modifier = Modifier.width(4.dp)) + Text("Receive") + } + } + + HorizontalDivider() + + Text( + text = "Connected Wallet", + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = "Relay: ${nwcConnection.relayUri}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = "Wallet: ${nwcConnection.pubKeyHex.take(8)}...${nwcConnection.pubKeyHex.takeLast(8)}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + TextButton(onClick = { accountManager.clearNwcConnection() - scope.launch { - snackbarHostState.showSnackbar("Wallet disconnected") - } - }, - ) - } - - WalletScreen.CONNECT -> { - ConnectWalletContent( - nwcUri = nwcUri, - isConnecting = isConnecting, - error = connectionError, - onUriChanged = { - nwcUri = it - connectionError = null - }, - onPasteFromClipboard = { - val clipboard = Toolkit.getDefaultToolkit().systemClipboard - val text = - try { - clipboard.getData(DataFlavor.stringFlavor) as? String - } catch (_: Exception) { - null - } - if (text != null) { - nwcUri = text - } - }, - onConnect = { - val result = accountManager.setNwcConnection(nwcUri) - if (result.isSuccess) { - nwcUri = "" - currentScreen = WalletScreen.HOME - scope.launch { - snackbarHostState.showSnackbar("Wallet connected!") - } - } else { - connectionError = "Invalid NWC URI. Expected: nostr+walletconnect://..." - } - }, - onBack = { currentScreen = WalletScreen.HOME }, - ) - } - - WalletScreen.SEND -> { - SendContent( - invoice = sendInvoice, - isSending = isSending, - result = sendResult, - onInvoiceChanged = { - sendInvoice = it - sendResult = null - }, - onPaste = { - val clipboard = Toolkit.getDefaultToolkit().systemClipboard - val text = - try { - clipboard.getData(DataFlavor.stringFlavor) as? String - } catch (_: Exception) { - null - } - if (text != null) sendInvoice = text - }, - onSend = { - if (nwcConnection != null && sendInvoice.isNotBlank()) { - isSending = true - sendResult = null - scope.launch { - val result = - paymentHandler.payInvoice( - bolt11 = sendInvoice, - nwcConnection = nwcConnection, - ) - when (result) { - is NwcPaymentHandler.PaymentResult.Success -> { - sendResult = "Payment successful!" - sendInvoice = "" - } - - is NwcPaymentHandler.PaymentResult.Error -> { - sendResult = "Error: ${result.message}" - } - - is NwcPaymentHandler.PaymentResult.Timeout -> { - sendResult = "Payment timed out" - } - } - isSending = false - } - } - }, - hasWallet = nwcConnection != null, - onBack = { currentScreen = WalletScreen.HOME }, - ) - } - - WalletScreen.RECEIVE -> { - ReceiveContent( - amount = receiveAmount, - description = receiveDescription, - generatedInvoice = generatedInvoice, - isGenerating = isGenerating, - onAmountChanged = { receiveAmount = it }, - onDescriptionChanged = { receiveDescription = it }, - onGenerate = { - if (nwcConnection != null) { - val amountSats = receiveAmount.toLongOrNull() ?: 0L - if (amountSats > 0) { - isGenerating = true - scope.launch { - val amountMsats = amountSats * 1000 - val desc = receiveDescription.ifBlank { null } - when (val result = paymentHandler.makeInvoice(nwcConnection, amountMsats, desc)) { - is NwcPaymentHandler.InvoiceResult.Success -> { - generatedInvoice = result.invoice - } - - is NwcPaymentHandler.InvoiceResult.Error -> { - snackbarHostState.showSnackbar("Invoice error: ${result.message}") - } - - is NwcPaymentHandler.InvoiceResult.Timeout -> { - snackbarHostState.showSnackbar("Invoice request timed out") - } - } - isGenerating = false - } - } - } - }, - onCopyInvoice = { invoice -> - val clipboard = Toolkit.getDefaultToolkit().systemClipboard - clipboard.setContents(StringSelection(invoice), null) - scope.launch { - snackbarHostState.showSnackbar("Invoice copied to clipboard") - } - }, - hasWallet = nwcConnection != null, - onBack = { currentScreen = WalletScreen.HOME }, - ) + balanceSats = null + scope.launch { snackbarHostState.showSnackbar("Wallet disconnected") } + }) { + Text("Disconnect", color = MaterialTheme.colorScheme.error) + } + } } } - Spacer(modifier = Modifier.weight(1f)) - SnackbarHost(hostState = snackbarHostState) + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.align(Alignment.BottomCenter), + ) } -} -@Composable -private fun WalletHomeContent( - nwcConnection: Nip47URINorm?, - balanceSats: Long?, - isLoadingBalance: Boolean, - onConnect: () -> Unit, - onSend: () -> Unit, - onReceive: () -> Unit, - onRefreshBalance: () -> Unit, - onDisconnect: () -> Unit, -) { - Column( - modifier = - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - if (nwcConnection == null) { - // No wallet connected - NoWalletContent(onConnect = onConnect) - } else { - // Connected wallet - WalletBalanceCard( - balanceSats = balanceSats, - isLoading = isLoadingBalance, - walletRelay = nwcConnection.relayUri.toString(), - onRefresh = onRefreshBalance, - ) + // -- Dialogs -- - // Quick actions - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button( - onClick = onSend, - modifier = Modifier.weight(1f), - ) { - Icon(symbol = MaterialSymbols.ArrowUpward, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text("Send") + if (showConnectDialog) { + ConnectWalletDialog( + onDismiss = { showConnectDialog = false }, + onConnect = { uri -> + val result = accountManager.setNwcConnection(uri) + if (result.isSuccess) { + showConnectDialog = false + scope.launch { snackbarHostState.showSnackbar("Wallet connected!") } } - OutlinedButton( - onClick = onReceive, - modifier = Modifier.weight(1f), - ) { - Icon(symbol = MaterialSymbols.ArrowDownward, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text("Receive") + result.isSuccess + }, + ) + } + + 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") + } + } } - } + }, + ) + } - HorizontalDivider() + if (showReceiveDialog && nwcConnection != null) { + ReceiveDialog( + onDismiss = { showReceiveDialog = false }, + onGenerate = { amountSats, description -> + scope.launch { + val result = + paymentHandler.makeInvoice( + nwcConnection = nwcConnection, + amountMsats = amountSats * 1000, + description = description.ifBlank { null }, + ) + when (result) { + is NwcPaymentHandler.InvoiceResult.Success -> { + result.invoice + } - // Connection info - Text( - text = "Connected Wallet", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = "Relay: ${nwcConnection.relayUri}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = "Wallet: ${nwcConnection.pubKeyHex.take(8)}...${nwcConnection.pubKeyHex.takeLast(8)}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + is NwcPaymentHandler.InvoiceResult.Error -> { + snackbarHostState.showSnackbar("Error: ${result.message}") + null + } - TextButton(onClick = onDisconnect) { - Text("Disconnect", color = MaterialTheme.colorScheme.error) - } - } + is NwcPaymentHandler.InvoiceResult.Timeout -> { + snackbarHostState.showSnackbar("Invoice request timed out") + null + } + } + } + }, + paymentHandler = paymentHandler, + nwcConnection = nwcConnection, + snackbarHostState = snackbarHostState, + ) } } @Composable private fun NoWalletContent(onConnect: () -> Unit) { Column( - modifier = Modifier.fillMaxWidth().padding(vertical = 32.dp), + modifier = Modifier.fillMaxWidth().padding(vertical = 48.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), ) { @@ -429,15 +326,11 @@ private fun NoWalletContent(onConnect: () -> Unit) { private fun WalletBalanceCard( balanceSats: Long?, isLoading: Boolean, - walletRelay: String, onRefresh: () -> Unit, ) { Card( modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - ), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer), ) { Column( modifier = Modifier.padding(20.dp), @@ -480,262 +373,239 @@ private fun WalletBalanceCard( } } -@Composable -private fun ConnectWalletContent( - nwcUri: String, - isConnecting: Boolean, - error: String?, - onUriChanged: (String) -> Unit, - onPasteFromClipboard: () -> Unit, - onConnect: () -> Unit, - onBack: () -> Unit, -) { - Column( - modifier = - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - TextButton(onClick = onBack) { - Icon(symbol = MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text("Back") - } - - Text( - text = "Connect Wallet", - style = MaterialTheme.typography.titleLarge, - ) - - Text( - text = "Paste your Nostr Wallet Connect URI to connect a Lightning wallet.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - OutlinedTextField( - value = nwcUri, - onValueChange = onUriChanged, - label = { Text("NWC URI") }, - placeholder = { Text("nostr+walletconnect://...") }, - modifier = Modifier.fillMaxWidth(), - singleLine = false, - maxLines = 4, - isError = error != null, - supportingText = error?.let { { Text(it) } }, - ) - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedButton(onClick = onPasteFromClipboard) { - Text("Paste from Clipboard") - } - } - - Button( - onClick = onConnect, - enabled = nwcUri.isNotBlank() && !isConnecting, - modifier = Modifier.fillMaxWidth(), - ) { - if (isConnecting) { - CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) - Spacer(modifier = Modifier.width(8.dp)) - } - Text("Connect") - } - - HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - - Text( - text = "Supported wallets:", - style = MaterialTheme.typography.titleSmall, - ) - Text( - text = "Alby Hub, Phoenix, Coinos, LNbits, Zeus, Mutiny, Strike", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = "Get an NWC connection URI from your wallet's settings.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } -} +// -- Dialogs -- @Composable -private fun SendContent( - invoice: String, - isSending: Boolean, - result: String?, - onInvoiceChanged: (String) -> Unit, - onPaste: () -> Unit, - onSend: () -> Unit, - hasWallet: Boolean, - onBack: () -> Unit, +private fun ConnectWalletDialog( + onDismiss: () -> Unit, + onConnect: (String) -> Boolean, ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - TextButton(onClick = onBack) { - Icon(symbol = MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text("Back") - } + var nwcUri by remember { mutableStateOf("") } + var error by remember { mutableStateOf(null) } - Text( - text = "Send Payment", - style = MaterialTheme.typography.titleLarge, - ) - - if (!hasWallet) { - Text( - text = "Connect a wallet first to send payments.", - color = MaterialTheme.colorScheme.error, - ) - return - } - - OutlinedTextField( - value = invoice, - onValueChange = onInvoiceChanged, - label = { Text("BOLT11 Invoice") }, - placeholder = { Text("lnbc...") }, - modifier = Modifier.fillMaxWidth(), - singleLine = false, - maxLines = 6, - ) - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedButton(onClick = onPaste) { - Text("Paste") - } - } - - Button( - onClick = onSend, - enabled = invoice.isNotBlank() && !isSending, - modifier = Modifier.fillMaxWidth(), - ) { - 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") - } - } - - if (result != null) { - val isError = result.startsWith("Error") || result.contains("timed out") - Text( - text = result, - color = if (isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - ) - } - } -} - -@Composable -private fun ReceiveContent( - amount: String, - description: String, - generatedInvoice: String?, - isGenerating: Boolean, - onAmountChanged: (String) -> Unit, - onDescriptionChanged: (String) -> Unit, - onGenerate: () -> Unit, - onCopyInvoice: (String) -> Unit, - hasWallet: Boolean, - onBack: () -> Unit, -) { - Column( - modifier = - Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - TextButton(onClick = onBack) { - Icon(symbol = MaterialSymbols.AutoMirrored.ArrowBack, contentDescription = null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(4.dp)) - Text("Back") - } - - Text( - text = "Receive Payment", - style = MaterialTheme.typography.titleLarge, - ) - - if (!hasWallet) { - Text( - text = "Connect a wallet first to receive payments.", - color = MaterialTheme.colorScheme.error, - ) - return - } - - OutlinedTextField( - value = amount, - onValueChange = { new -> if (new.all { it.isDigit() }) onAmountChanged(new) }, - label = { Text("Amount (sats)") }, - placeholder = { Text("1000") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - - OutlinedTextField( - value = description, - onValueChange = onDescriptionChanged, - label = { Text("Description (optional)") }, - placeholder = { Text("What's this for?") }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - ) - - Button( - onClick = onGenerate, - enabled = amount.isNotBlank() && !isGenerating, - modifier = Modifier.fillMaxWidth(), - ) { - if (isGenerating) { - CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) - Spacer(modifier = Modifier.width(8.dp)) - } - Text("Create Invoice") - } - - if (generatedInvoice != null) { - HorizontalDivider() - Text( - text = "Invoice Created", - style = MaterialTheme.typography.titleSmall, - ) - Card(modifier = Modifier.fillMaxWidth()) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Connect Wallet") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text( - text = generatedInvoice, - modifier = Modifier.padding(12.dp), - style = MaterialTheme.typography.bodySmall, + "Paste your Nostr Wallet Connect URI.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + OutlinedTextField( + value = nwcUri, + onValueChange = { + nwcUri = it + error = null + }, + label = { Text("NWC URI") }, + placeholder = { Text("nostr+walletconnect://...") }, + modifier = Modifier.fillMaxWidth(), + singleLine = false, maxLines = 4, + isError = error != null, + supportingText = error?.let { { Text(it) } }, + ) + OutlinedButton(onClick = { + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + val text = + try { + clipboard.getData(DataFlavor.stringFlavor) as? String + } catch (_: Exception) { + null + } + if (text != null) nwcUri = text + }) { + Text("Paste from Clipboard") + } + Text( + "Supported: Alby Hub, Phoenix, Coinos, LNbits, Zeus", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } + }, + confirmButton = { Button( - onClick = { onCopyInvoice(generatedInvoice) }, - modifier = Modifier.fillMaxWidth(), + onClick = { + if (!onConnect(nwcUri)) { + error = "Invalid NWC URI. Expected: nostr+walletconnect://..." + } + }, + enabled = nwcUri.isNotBlank(), ) { - Text("Copy Invoice") + Text("Connect") } - } - } + }, + dismissButton = { + TextButton(onClick = onDismiss) { Text("Cancel") } + }, + ) +} + +@Composable +private fun SendDialog( + onDismiss: () -> Unit, + onSend: (String) -> Unit, +) { + var invoice by remember { mutableStateOf("") } + var isSending by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = { if (!isSending) onDismiss() }, + title = { Text("Send Payment") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + OutlinedTextField( + value = invoice, + onValueChange = { invoice = it }, + label = { Text("BOLT11 Invoice") }, + placeholder = { Text("lnbc...") }, + modifier = Modifier.fillMaxWidth(), + singleLine = false, + maxLines = 6, + ) + OutlinedButton(onClick = { + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + val text = + try { + clipboard.getData(DataFlavor.stringFlavor) as? String + } catch (_: Exception) { + null + } + if (text != null) invoice = text + }) { + 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") + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss, enabled = !isSending) { Text("Cancel") } + }, + ) +} + +@Composable +private fun ReceiveDialog( + onDismiss: () -> Unit, + onGenerate: (Long, String) -> Unit, + paymentHandler: NwcPaymentHandler, + nwcConnection: Nip47URINorm, + snackbarHostState: SnackbarHostState, +) { + var amount by remember { mutableStateOf("") } + var description by remember { mutableStateOf("") } + var generatedInvoice by remember { mutableStateOf(null) } + var isGenerating by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + AlertDialog( + onDismissRequest = { if (!isGenerating) onDismiss() }, + title = { Text(if (generatedInvoice != null) "Invoice Created" else "Receive Payment") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + if (generatedInvoice != null) { + Card(modifier = Modifier.fillMaxWidth()) { + Text( + text = generatedInvoice!!, + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall, + maxLines = 6, + ) + } + } else { + OutlinedTextField( + value = amount, + onValueChange = { new -> if (new.all { it.isDigit() }) amount = new }, + label = { Text("Amount (sats)") }, + placeholder = { Text("1000") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + OutlinedTextField( + value = description, + onValueChange = { description = it }, + label = { Text("Description (optional)") }, + placeholder = { Text("What's this for?") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + } + } + }, + confirmButton = { + if (generatedInvoice != null) { + Button(onClick = { + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + clipboard.setContents(StringSelection(generatedInvoice), null) + scope.launch { snackbarHostState.showSnackbar("Invoice copied!") } + }) { + Text("Copy Invoice") + } + } else { + Button( + onClick = { + val amountSats = amount.toLongOrNull() ?: 0L + if (amountSats > 0) { + isGenerating = true + scope.launch { + val result = + paymentHandler.makeInvoice( + nwcConnection = nwcConnection, + amountMsats = amountSats * 1000, + description = description.ifBlank { null }, + ) + when (result) { + is NwcPaymentHandler.InvoiceResult.Success -> { + generatedInvoice = result.invoice + } + + is NwcPaymentHandler.InvoiceResult.Error -> { + snackbarHostState.showSnackbar("Error: ${result.message}") + } + + is NwcPaymentHandler.InvoiceResult.Timeout -> { + snackbarHostState.showSnackbar("Invoice request timed out") + } + } + isGenerating = false + } + } + }, + enabled = amount.isNotBlank() && !isGenerating, + ) { + if (isGenerating) { + CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary) + Spacer(modifier = Modifier.width(8.dp)) + } + Text("Create Invoice") + } + } + }, + dismissButton = { + TextButton(onClick = onDismiss, enabled = !isGenerating) { + Text(if (generatedInvoice != null) "Close" else "Cancel") + } + }, + ) } private fun formatSats(sats: Long): String = NumberFormat.getNumberInstance(Locale.getDefault()).format(sats)