mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
refactor(desktop): use dialogs for wallet actions, center home content
Send, Receive, and Connect are now AlertDialogs instead of full-screen sub-pages. The wallet home content is centered with widthIn(max=360.dp) for a polished desktop look. Dialogs are the native desktop pattern (consistent with zap dialog, bookmark dialog, tor settings dialog). - ConnectWalletDialog: NWC URI input with paste + validation - SendDialog: BOLT11 invoice input with paste + progress - ReceiveDialog: amount/description input, transitions to show generated invoice with copy button - Home content: centered balance card + action buttons + connection info 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
420a10df50
commit
a15ce33807
+373
-503
@@ -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<String?>(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<Long?>(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<String?>(null) }
|
||||
|
||||
// Receive state
|
||||
var receiveAmount by remember { mutableStateOf("") }
|
||||
var receiveDescription by remember { mutableStateOf("") }
|
||||
var generatedInvoice by remember { mutableStateOf<String?>(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<String?>(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<String?>(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)
|
||||
|
||||
Reference in New Issue
Block a user