feat(wallet): iOS-style send input, lightning address QR, on-chain Send icon
Send screen: - Replace OutlinedTextField with tall BasicTextField in a dark card (min 120dp, top-aligned placeholder, nsec guard on all paste paths) - Scan QR · Paste · Gallery action row beneath the field (iOS parity) - Accent-colored Next button with proper disabled state - Spark hint now includes CLINK offer alongside Bitcoin address Receive screen: - Lightning address row: compact bolt icon + address + QR + copy icon replacing the two-button Copy/Share card; QR icon navigates to the existing LightningAddressQR page On-chain send: - Replace ArrowUpward direction indicator and button icons with the AutoMirrored Send (paper airplane) icon on both Amount and Confirm screens; remove redundant inline back buttons (TopAppBar handles back)
This commit is contained in:
@@ -71,6 +71,7 @@ import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VpnKey
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material.icons.filled.ArrowDownward
|
||||
import androidx.compose.material.icons.filled.ArrowUpward
|
||||
@@ -516,6 +517,7 @@ fun WalletScreen(
|
||||
onAmountChange = { viewModel.setReceiveAmount(it) },
|
||||
onGenerate = { sats, note, expirySecs -> viewModel.generateInvoice(sats, note, expirySecs) },
|
||||
onLoadDepositAddress = { viewModel.loadDepositAddress() },
|
||||
onShowAddressQR = { viewModel.navigateTo(WalletPage.LightningAddressQR) },
|
||||
modifier = Modifier.padding(padding)
|
||||
)
|
||||
is WalletPage.ReceiveInvoice -> {
|
||||
@@ -1778,46 +1780,60 @@ private fun SendInputContent(
|
||||
}
|
||||
}
|
||||
|
||||
val accent = WispThemeColors.zapColor
|
||||
val fieldShape = RoundedCornerShape(14.dp)
|
||||
val fieldBg = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||
val hint = if (walletMode == WalletMode.SPARK) {
|
||||
stringResource(R.string.wallet_send_input_hint_onchain)
|
||||
} else {
|
||||
stringResource(R.string.wallet_lightning_address_invoice)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.padding(horizontal = 20.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.wallet_send),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(20.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = input,
|
||||
onValueChange = onInputChange,
|
||||
label = { Text(stringResource(R.string.wallet_lightning_address_invoice)) },
|
||||
placeholder = {
|
||||
Text(
|
||||
if (walletMode == WalletMode.SPARK)
|
||||
stringResource(R.string.placeholder_send_spark)
|
||||
else
|
||||
stringResource(R.string.placeholder_user_domain)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = false,
|
||||
maxLines = 4,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
clipboardManager.getText()?.text?.let { onInputChange(it) }
|
||||
}) {
|
||||
Icon(Icons.Default.ContentPaste, contentDescription = stringResource(R.string.wallet_paste))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 120.dp)
|
||||
.background(fieldBg, fieldShape)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
val entryStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
BasicTextField(
|
||||
value = input,
|
||||
onValueChange = { new ->
|
||||
if (!com.darkwisp.app.ui.component.NsecPasteGuard.blockIfNsec(input, new)) onInputChange(new)
|
||||
},
|
||||
textStyle = entryStyle,
|
||||
cursorBrush = androidx.compose.ui.graphics.SolidColor(accent),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
decorationBox = { inner ->
|
||||
if (input.isEmpty()) {
|
||||
Text(hint, style = entryStyle.copy(color = MaterialTheme.colorScheme.onSurfaceVariant))
|
||||
}
|
||||
inner()
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (error != null) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@@ -1828,51 +1844,79 @@ private fun SendInputContent(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
// Scan QR / Import from Gallery buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(fieldBg, fieldShape)
|
||||
) {
|
||||
OutlinedButton(
|
||||
SendInputAction(
|
||||
icon = Icons.Default.QrCode,
|
||||
label = stringResource(R.string.wallet_scan_qr),
|
||||
onClick = onScanQR,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.QrCode,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.wallet_scan_qr))
|
||||
}
|
||||
OutlinedButton(
|
||||
)
|
||||
VerticalDivider(modifier = Modifier.height(24.dp))
|
||||
SendInputAction(
|
||||
icon = Icons.Default.ContentPaste,
|
||||
label = stringResource(R.string.wallet_paste),
|
||||
onClick = {
|
||||
clipboardManager.getText()?.text?.let { new ->
|
||||
if (!com.darkwisp.app.ui.component.NsecPasteGuard.blockIfNsec(input, new)) onInputChange(new)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
VerticalDivider(modifier = Modifier.height(24.dp))
|
||||
SendInputAction(
|
||||
icon = Icons.Default.Image,
|
||||
label = stringResource(R.string.wallet_gallery),
|
||||
onClick = { galleryLauncher.launch("image/*") },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Image,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.wallet_gallery))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Button(
|
||||
onClick = onNext,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = input.isNotBlank()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp),
|
||||
shape = fieldShape,
|
||||
enabled = input.isNotBlank(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accent,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = fieldBg,
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.wallet_next))
|
||||
Text(stringResource(R.string.wallet_next), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendInputAction(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = WispThemeColors.zapColor)
|
||||
) {
|
||||
Icon(icon, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text(label, fontWeight = FontWeight.Medium, maxLines = 1)
|
||||
}
|
||||
}
|
||||
|
||||
// --- QR Scanner ---
|
||||
|
||||
@Composable
|
||||
@@ -2262,6 +2306,7 @@ private fun ReceiveAmountContent(
|
||||
onAmountChange: (String) -> Unit,
|
||||
onGenerate: (Long, String, Int) -> Unit,
|
||||
onLoadDepositAddress: () -> Unit,
|
||||
onShowAddressQR: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val receiveCtx = LocalContext.current
|
||||
@@ -2564,7 +2609,7 @@ private fun ReceiveAmountContent(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
LightningAddressReceiveRow(address = lightningAddress)
|
||||
LightningAddressReceiveRow(address = lightningAddress, onShowQR = onShowAddressQR)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2574,54 +2619,64 @@ private fun ReceiveAmountContent(
|
||||
|
||||
// Lightning address displayed below the invoice form
|
||||
@Composable
|
||||
private fun LightningAddressReceiveRow(address: String) {
|
||||
private fun LightningAddressReceiveRow(
|
||||
address: String,
|
||||
onShowQR: () -> Unit
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val actionShape = RoundedCornerShape(14.dp)
|
||||
val actionBg = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(actionBg, actionShape)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
stringResource(R.string.wallet_receive_via_address),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 12.dp)
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.weight(1f))
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
|
||||
RoundedCornerShape(14.dp)
|
||||
)
|
||||
.padding(start = 16.dp, end = 6.dp, top = 6.dp, bottom = 6.dp)
|
||||
) {
|
||||
TextButton(
|
||||
onClick = { clipboardManager.setText(AnnotatedString(address)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = WispThemeColors.zapColor)
|
||||
) {
|
||||
Icon(Icons.Default.ContentCopy, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.wallet_copy_address), fontWeight = FontWeight.Medium, style = MaterialTheme.typography.labelMedium)
|
||||
Icon(
|
||||
Icons.Outlined.Bolt,
|
||||
contentDescription = null,
|
||||
tint = WispThemeColors.zapColor,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Text(
|
||||
address,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = onShowQR) {
|
||||
Icon(
|
||||
Icons.Default.QrCode,
|
||||
contentDescription = stringResource(R.string.wallet_receive_address_tab),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
VerticalDivider(modifier = Modifier.height(20.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, address)
|
||||
}
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.wallet_share_address)))
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = WispThemeColors.zapColor)
|
||||
) {
|
||||
Icon(Icons.Default.Share, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.wallet_share), fontWeight = FontWeight.Medium, style = MaterialTheme.typography.labelMedium)
|
||||
IconButton(onClick = { clipboardManager.setText(AnnotatedString(address)) }) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = stringResource(R.string.cd_copy_invoice),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2775,10 +2830,7 @@ private fun OnchainSendAmountContent(
|
||||
) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.btn_back))
|
||||
}
|
||||
Icon(Icons.Default.ArrowUpward, contentDescription = null, tint = accent, modifier = Modifier.size(20.dp))
|
||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, tint = accent, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.wallet_onchain_send_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
@@ -2885,7 +2937,7 @@ private fun OnchainSendAmountContent(
|
||||
shape = fieldShape,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = accent, contentColor = Color.White)
|
||||
) {
|
||||
Icon(Icons.Default.ArrowUpward, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.wallet_onchain_continue), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
@@ -2975,10 +3027,7 @@ private fun OnchainSendConfirmContent(
|
||||
) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.btn_back))
|
||||
}
|
||||
Icon(Icons.Default.ArrowUpward, contentDescription = null, tint = accent, modifier = Modifier.size(20.dp))
|
||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, tint = accent, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.wallet_onchain_send_title), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
@@ -3050,7 +3099,7 @@ private fun OnchainSendConfirmContent(
|
||||
shape = fieldShape,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = accent, contentColor = Color.White)
|
||||
) {
|
||||
Icon(Icons.Default.ArrowUpward, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.wallet_onchain_send_confirm), fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
@@ -3437,7 +3486,7 @@ private fun TransactionHistoryContent(
|
||||
// both render. The list is deduped by (paymentHash, type)
|
||||
// upstream, so these keys are unique.
|
||||
items(transactions, key = { "${it.paymentHash}|${it.type}" }) { tx ->
|
||||
TransactionRow(tx, profileLookup, displayMode)
|
||||
TransactionRow(tx, profileLookup, displayMode, expandable = true)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
@@ -3474,20 +3523,25 @@ private fun TransactionHistoryContent(
|
||||
private fun TransactionRow(
|
||||
tx: WalletTransaction,
|
||||
profileLookup: (String) -> com.darkwisp.app.nostr.ProfileData?,
|
||||
displayMode: WalletBalanceDisplayMode = WalletBalanceDisplayMode.SATS
|
||||
displayMode: WalletBalanceDisplayMode = WalletBalanceDisplayMode.SATS,
|
||||
expandable: Boolean = false
|
||||
) {
|
||||
val isIncoming = tx.type == "incoming"
|
||||
val amountSats = tx.amountMsats / 1000
|
||||
val profile = tx.counterpartyPubkey?.let { profileLookup(it) }
|
||||
val ctx = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val fiatMode by FiatPreferences.get(ctx).fiatMode.collectAsState()
|
||||
val fiatCurrency by FiatPreferences.get(ctx).currency.collectAsState()
|
||||
val isHidden = displayMode == WalletBalanceDisplayMode.HIDDEN
|
||||
val isWalletFiat = displayMode == WalletBalanceDisplayMode.FIAT
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(if (expandable) Modifier.clickable { expanded = !expanded } else Modifier)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -3540,11 +3594,26 @@ private fun TransactionRow(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
formatRelativeTime(tx.settledAt ?: tx.createdAt),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (tx.pending) {
|
||||
Text(
|
||||
stringResource(R.string.wallet_tx_pending),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = WispThemeColors.zapColor
|
||||
)
|
||||
Text(
|
||||
" · ",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Text(
|
||||
formatRelativeTime(tx.settledAt ?: tx.createdAt),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Amount + fee. In HIDDEN mode every number is masked so a
|
||||
@@ -3622,6 +3691,122 @@ private fun TransactionRow(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (expandable) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(
|
||||
if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (expandable) {
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
TransactionDetailPanel(
|
||||
tx = tx,
|
||||
onCopy = { clipboardManager.setText(AnnotatedString(it)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TransactionDetailPanel(
|
||||
tx: WalletTransaction,
|
||||
onCopy: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val sats = tx.amountMsats / 1000
|
||||
val feeSats = tx.feeMsats / 1000
|
||||
val fullDate = remember(tx.settledAt, tx.createdAt) {
|
||||
val secs = tx.settledAt ?: tx.createdAt
|
||||
java.text.SimpleDateFormat("MMM d, yyyy · h:mm a", java.util.Locale.getDefault())
|
||||
.format(java.util.Date(secs * 1000))
|
||||
}
|
||||
val note = tx.description?.takeIf {
|
||||
it.isNotBlank() && it != "null" && !it.trimStart().startsWith("{")
|
||||
}
|
||||
val idLabel = if (tx.isOnchain) "Transaction ID" else "Payment hash"
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f),
|
||||
RoundedCornerShape(12.dp)
|
||||
)
|
||||
.padding(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
TxDetailRow("Status", if (tx.pending) "Pending" else "Completed")
|
||||
TxDetailRow("Type", if (tx.isOnchain) "On-chain" else "Lightning")
|
||||
TxDetailRow("Amount", "%,d sats".format(sats))
|
||||
if (tx.feeMsats > 0) TxDetailRow("Network fee", "%,d sats".format(feeSats))
|
||||
TxDetailRow("Date", fullDate)
|
||||
if (note != null) TxDetailRow("Note", note)
|
||||
TxDetailRow(idLabel, tx.paymentHash, mono = true, onCopy = { onCopy(tx.paymentHash) })
|
||||
if (tx.isOnchain) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
context.startActivity(
|
||||
android.content.Intent(
|
||||
android.content.Intent.ACTION_VIEW,
|
||||
android.net.Uri.parse("https://mempool.space/tx/${tx.paymentHash}")
|
||||
)
|
||||
)
|
||||
},
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = WispThemeColors.zapColor)
|
||||
) {
|
||||
Icon(Icons.Default.Share, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Text("View on mempool.space", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TxDetailRow(
|
||||
label: String,
|
||||
value: String,
|
||||
mono: Boolean = false,
|
||||
onCopy: (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.width(110.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
value,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = if (mono) FontFamily.Monospace else null,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (onCopy != null) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = stringResource(R.string.action_copy),
|
||||
tint = WispThemeColors.zapColor,
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clickable(onClick = onCopy)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -812,6 +812,7 @@
|
||||
<string name="wallet_onchain_view_mempool">View on mempool.space</string>
|
||||
<!-- Spark send placeholder (accepts Lightning + Bitcoin addresses) -->
|
||||
<string name="placeholder_send_spark">Lightning invoice or Bitcoin address</string>
|
||||
<string name="wallet_send_input_hint_onchain">Lightning or Bitcoin address, invoice, or CLINK offer</string>
|
||||
<!-- Generic actions -->
|
||||
<string name="retry">Retry</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
@@ -901,6 +902,8 @@
|
||||
<string name="section_invite_codes">Invite codes</string>
|
||||
<string name="action_generate_invite">Generate invite code</string>
|
||||
<string name="label_latest_invite_code">Latest invite code</string>
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="wallet_tx_pending">Pending</string>
|
||||
<string name="action_copy_code">Copy code</string>
|
||||
<string name="action_copy_invite_link">Copy invite link</string>
|
||||
<string name="title_assign_role">Assign role</string>
|
||||
|
||||
Reference in New Issue
Block a user