diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index eef31d2..19ba699 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -6,6 +6,7 @@
Tink
amboss
hkdf
+ inflight
lnbc
lnbits
lnurlp
diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt
index 22c89f1..8ac3b86 100644
--- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt
+++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HistoryScreen.kt
@@ -2,7 +2,6 @@ package com.bitcointxoko.gudariwallet.ui
import android.content.ClipData
import android.content.Intent
-import android.util.Log
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@@ -32,7 +31,7 @@ import androidx.core.net.toUri
import com.bitcointxoko.gudariwallet.api.PaymentRecord
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
import com.bitcointxoko.gudariwallet.util.feePpm
-import com.bitcointxoko.gudariwallet.util.formatFiatForSats // ← new import
+import com.bitcointxoko.gudariwallet.util.formatFiatForSats
import kotlinx.coroutines.launch
// ── List screen ──────────────────────────────────────────────────────────────
@@ -156,6 +155,21 @@ fun HistoryScreen(
}
}
+// ── Payment color helpers ─────────────────────────────────────────────────────
+
+private fun isPendingStatus(status: String): Boolean =
+ status.lowercase() in setOf("pending", "in_flight", "inflight")
+
+@Composable
+private fun paymentAmountColor(status: String, isOutgoing: Boolean): Color {
+ val semantic = semanticColors()
+ return when {
+ isPendingStatus(status) -> MaterialTheme.colorScheme.onSurfaceVariant // muted
+ isOutgoing -> MaterialTheme.colorScheme.onSurface // neutral
+ else -> semantic.success // green
+ }
+}
+
// ── Single row ───────────────────────────────────────────────────────────────
@Composable
@@ -168,16 +182,8 @@ private fun PaymentRow(
val semantic = semanticColors()
// ── Pending detection ────────────────────────────────────────────────────
- val isPending = payment.status.lowercase() in setOf("pending", "in_flight", "inflight")
-
- // bitcoin.design: green for received, neutral for sent, muted for pending.
- val amountColor = when {
- isPending -> MaterialTheme.colorScheme.onSurfaceVariant
- payment.isOutgoing -> MaterialTheme.colorScheme.onSurface
- else -> semantic.success
- }
-
- val amountPrefix = if (payment.isOutgoing) "−" else "+"
+ val isPending = isPendingStatus(payment.status)
+ val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
else Icons.Default.ArrowDownward
@@ -235,29 +241,15 @@ private fun PaymentRow(
}
}
Spacer(Modifier.width(12.dp))
-
- // Right: sats (primary) + fiat (secondary) — right-aligned
- Column(horizontalAlignment = Alignment.End) {
- Text(
- text = "$amountPrefix${payment.amountSat} sats",
- style = MaterialTheme.typography.bodyMedium.copy(
- fontFamily = FontFamily.Monospace
- ),
- color = amountColor
- )
- if (fiatLine != null) {
- Spacer(Modifier.height(1.dp))
- Text(
- text = fiatLine,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
- )
- }
- }
+ AmountWithFiatColumn(
+ amountSat = payment.amountSat,
+ isOutgoing = payment.isOutgoing,
+ amountColor = amountColor,
+ fiatLine = fiatLine
+ )
}
}
-
// ── Detail screen ────────────────────────────────────────────────────────────
@OptIn(ExperimentalMaterial3Api::class)
@@ -354,13 +346,8 @@ private fun PaymentDetailContent(
fiatSatsPerUnit : Double?, // ← new
modifier : Modifier = Modifier
) {
- val isPending = payment.status.lowercase() in setOf("pending", "in_flight", "inflight")
- val semantic = semanticColors()
- val amountColor = when {
- isPending -> MaterialTheme.colorScheme.onSurfaceVariant // muted
- payment.isOutgoing -> MaterialTheme.colorScheme.onSurface // neutral
- else -> semantic.success // green
- }
+ val isPending = isPendingStatus(payment.status)
+ val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
val context = LocalContext.current
val clipboard = LocalClipboard.current
@@ -407,24 +394,14 @@ private fun PaymentDetailContent(
modifier = Modifier.size(36.dp)
)
Spacer(Modifier.height(8.dp))
-
- // Primary: sats
- Text(
- text = "${if (payment.isOutgoing) "−" else "+"}${payment.amountSat} sats",
- style = MaterialTheme.typography.headlineLarge,
- color = amountColor
+ AmountWithFiatColumn(
+ amountSat = payment.amountSat,
+ isOutgoing = payment.isOutgoing,
+ amountColor = amountColor,
+ fiatLine = heroFiat,
+ style = MaterialTheme.typography.headlineLarge,
+ horizontalAlignment = Alignment.CenterHorizontally
)
-
- // Secondary: fiat equivalent
- if (heroFiat != null) {
- Spacer(Modifier.height(4.dp))
- Text(
- text = "≈ $heroFiat",
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
- )
- }
-
// Fee line (with fiat if available)
if (payment.feeSat > 0) {
val ppm = feePpm(
@@ -433,7 +410,7 @@ private fun PaymentDetailContent(
)
val feeLabel = buildString {
append("Fee: ${payment.feeSat} sats")
- if (feeFiat != null) append(" (≈ $feeFiat)")
+ if (feeFiat != null) append(" ($feeFiat)")
if (ppm != null) append(" · $ppm ppm")
}
Spacer(Modifier.height(4.dp))
@@ -520,13 +497,7 @@ private fun PaymentDetailContent(
}
)
}
- }
- }
-
- // ── BOLT11 invoice ───────────────────────────────────────────────────
- if (!payment.bolt11.isNullOrBlank()) {
- item {
- DetailSection(title = "Invoice") {
+ if (!payment.bolt11.isNullOrBlank()) {
DetailRow(
label = "BOLT11",
value = payment.bolt11,
@@ -542,8 +513,6 @@ private fun PaymentDetailContent(
}
// ── Reusable section wrapper ─────────────────────────────────────────────────
-// (unchanged — kept for completeness)
-
@Composable
private fun DetailSection(
title : String,
@@ -563,8 +532,6 @@ private fun DetailSection(
}
// ── Single label/value row ───────────────────────────────────────────────────
-// (unchanged)
-
@Composable
private fun DetailRow(
label : String,
@@ -625,8 +592,6 @@ private fun DetailRow(
}
// ── Status chip ──────────────────────────────────────────────────────────────
-// (unchanged)
-
@Composable
private fun StatusChip(status: String) {
val semantic = semanticColors()
@@ -675,3 +640,30 @@ private fun formatTimestamp(time: String): String {
.format(odt)
}.getOrDefault(time)
}
+
+@Composable
+private fun AmountWithFiatColumn(
+ amountSat : Long,
+ isOutgoing : Boolean,
+ amountColor : Color,
+ fiatLine : String?,
+ style : androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium,
+ horizontalAlignment: Alignment.Horizontal = Alignment.End
+) {
+ val prefix = if (isOutgoing) "−" else "+"
+ Column(horizontalAlignment = horizontalAlignment) {
+ Text(
+ text = "$prefix$amountSat sats",
+ style = style.copy(fontFamily = FontFamily.Monospace),
+ color = amountColor
+ )
+ if (fiatLine != null) {
+ Spacer(Modifier.height(if (horizontalAlignment == Alignment.CenterHorizontally) 4.dp else 1.dp))
+ Text(
+ text = fiatLine,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt
index fd8d996..e0b86c8 100644
--- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt
+++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/HomeTab.kt
@@ -3,6 +3,7 @@ package com.bitcointxoko.gudariwallet.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+import androidx.compose.foundation.combinedClickable
import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
@@ -10,14 +11,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
-import androidx.compose.ui.draw.alpha
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.filled.ArrowDropDown
@@ -29,7 +27,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.platform.LocalFocusManager
import com.bitcointxoko.gudariwallet.util.formatFiat
-import kotlinx.coroutines.delay
+import androidx.compose.ui.text.font.FontFamily
@@ -43,18 +41,6 @@ fun HomeTab(
val balanceHidden by vm.balanceHidden.collectAsState()
- var eyeVisible by remember { mutableStateOf(false) }
- LaunchedEffect(balanceHidden) {
- eyeVisible = true
- delay(1_500)
- eyeVisible = false
- }
- val eyeAlpha by animateFloatAsState(
- targetValue = if (eyeVisible) 1f else 0f,
- animationSpec = tween(durationMillis = 400),
- label = "eyeAlpha"
- )
-
val isRefreshing = balanceState is BalanceState.Loading
|| (balanceState as? BalanceState.Success)?.isRefreshing == true
@@ -96,9 +82,7 @@ fun HomeTab(
}
}
Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxWidth()
+ verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (balanceHidden)
@@ -109,14 +93,15 @@ fun HomeTab(
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.size(20.dp)
- .alpha(eyeAlpha)
)
Spacer(Modifier.width(8.dp))
Text(
- text = if (balanceHidden) "••••••" else "${s.sats}",
- style = MaterialTheme.typography.displayLarge,
+ text = if (balanceHidden) "••••••" else formatSats(s.sats),
+ style = MaterialTheme.typography.displayLarge.copy(
+ fontFamily = FontFamily.Monospace
+ ),
color = if (balanceHidden)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f)
else
@@ -124,74 +109,93 @@ fun HomeTab(
maxLines = 1,
softWrap = false,
modifier = Modifier
- .weight(1f, fill = false)
- .clickable(
- onClick = { vm.toggleBalanceVisibility() },
- onClickLabel = if (balanceHidden) "Reveal balance" else "Hide balance",
+ .widthIn(max = 280.dp)
+ .combinedClickable(
+ onClick = { if (!balanceHidden) vm.toggleBalanceVisibility() },
+ onClickLabel = if (balanceHidden) null else "Hide balance",
+ onLongClick = { if (balanceHidden) vm.toggleBalanceVisibility() },
+ onLongClickLabel = if (balanceHidden) "Reveal balance" else null,
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
)
-
Spacer(Modifier.width(8.dp))
- Spacer(Modifier.size(20.dp))
+ Text(
+ text = "sats",
+ style = MaterialTheme.typography.titleMedium.copy(
+ fontFamily = FontFamily.Monospace
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Spacer(Modifier.width(8.dp))
}
- Text(
- text = "sats",
- style = MaterialTheme.typography.titleMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
Spacer(Modifier.height(2.dp))
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- if (s.fiatAmount != null && s.fiatCurrency != null) {
- Text(
- text = if (balanceHidden) "≈ ••••"
- else "≈ ${formatFiat(s.fiatAmount, s.fiatCurrency)}",
- style = MaterialTheme.typography.bodyMedium,
- color = if (balanceHidden)
- MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f)
- else
- MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
- maxLines = 1,
- softWrap = false
- )
- Spacer(Modifier.width(2.dp))
- }
+ val currency = selectedCurrency
+ if (currency != null) {
+ // ── Fiat amount + tappable currency code ──────────────────────────
Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier
- .clickable(
- onClick = { showCurrencyPicker = true },
- onClickLabel = "Change currency",
- indication = null,
- interactionSource = remember { MutableInteractionSource() }
- )
- .padding(start = 0.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
) {
- Text(
- text = selectedCurrency ?: "—",
- style = MaterialTheme.typography.bodyMedium,
- color = if (selectedCurrency != null)
- MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
- else
- MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
- )
- // Triangle only shown when null — signals "tap to set a currency"
- if (selectedCurrency == null) {
+ if (s.fiatAmount != null && s.fiatCurrency != null) {
+ Text(
+ text = if (balanceHidden) "••••"
+ else formatFiat(s.fiatAmount, s.fiatCurrency),
+ style = MaterialTheme.typography.bodyMedium.copy(
+ fontFamily = FontFamily.Monospace
+ ),
+ color = if (balanceHidden)
+ MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f)
+ else
+ MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
+ maxLines = 1,
+ softWrap = false
+ )
+ Spacer(Modifier.width(4.dp))
+ }
+ // Currency code — always shown when a currency is selected, tappable to change
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clickable(
+ onClick = { showCurrencyPicker = true },
+ onClickLabel = "Change currency",
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ )
+ .padding(horizontal = 4.dp, vertical = 4.dp)
+ ) {
+ Text(
+ text = currency,
+ style = MaterialTheme.typography.bodyMedium.copy(
+ fontFamily = FontFamily.Monospace
+ ),
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f)
+ )
Spacer(Modifier.width(2.dp))
Icon(
imageVector = Icons.Filled.ArrowDropDown,
contentDescription = "Change currency",
- tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
+ tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
modifier = Modifier.size(14.dp)
)
}
}
+ } else {
+ // ── No currency selected — ghost prompt ───────────────────────────
+ TextButton(
+ onClick = { showCurrencyPicker = true },
+ contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
+ ) {
+ Text(
+ text = "+ Show fiat",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)
+ )
+ }
}
+
Spacer(Modifier.height(8.dp))
if (!s.isRefreshing) {
@@ -231,8 +235,10 @@ fun HomeTab(
is BalanceState.Error -> {
if (s.staleSats != null) {
Text(
- text = if (balanceHidden) "••••••" else "${s.staleSats}",
- style = MaterialTheme.typography.displayLarge,
+ text = if (balanceHidden) "••••••" else formatSats(s.staleSats),
+ style = MaterialTheme.typography.displayLarge.copy(
+ fontFamily = FontFamily.Monospace
+ ),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
)
Text(
@@ -264,6 +270,9 @@ fun HomeTab(
}
}
+private fun formatSats(sats: Long): String =
+ "%,d".format(sats).replace(',', ' ')
+
private fun formatLastUpdated(epochMs: Long): String {
val ageMs = System.currentTimeMillis() - epochMs
return when {