fix: send ui

This commit is contained in:
2026-06-06 14:17:42 +02:00
parent c80edafbd4
commit 0f360067d1
5 changed files with 120 additions and 71 deletions
@@ -126,6 +126,11 @@ val EnHomeStrings = AppStrings(
resolving = "Resolving…", resolving = "Resolving…",
sendingPayment = "Sending payment…", sendingPayment = "Sending payment…",
// ── SendScreen — paying ───────────────────────────────────────────────────
paymentTakingLong = "This is taking longer than usual",
paymentTakingLongDetail = "Your payment is still on its way. This may be a hold invoice that settles when the recipient is ready. You'll get a notification when it completes — you can safely leave this screen.",
dismissPayment = "Dismiss",
// ── SendScreen — payment sent ───────────────────────────────────────────── // ── SendScreen — payment sent ─────────────────────────────────────────────
paymentSent = "Payment sent!", paymentSent = "Payment sent!",
amountSats = { sats -> "$sats sats" }, amountSats = { sats -> "$sats sats" },
@@ -126,6 +126,11 @@ val EsHomeStrings = AppStrings(
resolving = "Resolviendo…", resolving = "Resolviendo…",
sendingPayment = "Enviando pago…", sendingPayment = "Enviando pago…",
// ── SendScreen — paying ───────────────────────────────────────────────────
paymentTakingLong = "Esto está tardando más de lo habitual",
paymentTakingLongDetail = "Tu pago sigue en camino. Es posible que sea una factura hold que se liquidará cuando el destinatario esté listo. Recibirás una notificación cuando se complete — puedes salir de esta pantalla con seguridad.",
dismissPayment = "Salir",
// ── SendScreen — payment sent ───────────────────────────────────────────── // ── SendScreen — payment sent ─────────────────────────────────────────────
paymentSent = "¡Pago enviado!", paymentSent = "¡Pago enviado!",
amountSats = { sats -> "$sats sats" }, amountSats = { sats -> "$sats sats" },
@@ -126,6 +126,12 @@ val EuStrings = AppStrings(
resolving = "Ebazten…", resolving = "Ebazten…",
sendingPayment = "Ordainketa bidaltzen…", sendingPayment = "Ordainketa bidaltzen…",
// ── SendScreen — paying ───────────────────────────────────────────────────
paymentTakingLong = "Ohikoa baino gehiago ari da irauten",
paymentTakingLongDetail = "Zure ordainketa bidean dago oraindik. Baliteke atxikitako faktura bat izatea, hartzaileak prest dagoenean kitatzekoa. Jakinarazpen bat jasoko duzu burutzen denean — pantaila hau segurtasunez utzi dezakezu.",
dismissPayment = "Utzi",
// ── SendScreen — payment sent ───────────────────────────────────────────── // ── SendScreen — payment sent ─────────────────────────────────────────────
paymentSent = "Ordainketa bidalita!", paymentSent = "Ordainketa bidalita!",
amountSats = { sats -> "$sats sat" }, amountSats = { sats -> "$sats sat" },
@@ -135,6 +135,11 @@ data class AppStrings(
val resolving : String, val resolving : String,
val sendingPayment : String, val sendingPayment : String,
// ── SendScreen — paying ───────────────────────────────────────────────────
val paymentTakingLong : String,
val paymentTakingLongDetail: String,
val dismissPayment : String,
// ── SendScreen — payment sent ───────────────────────────────────────────── // ── SendScreen — payment sent ─────────────────────────────────────────────
val paymentSent : String, val paymentSent : String,
val amountSats : (sats: Long) -> String, val amountSats : (sats: Long) -> String,
@@ -6,8 +6,10 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ContentPaste import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.Nfc import androidx.compose.material.icons.filled.Nfc
import androidx.compose.material.icons.outlined.HourglassTop
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -28,10 +30,13 @@ import com.bitcointxoko.gudariwallet.ui.NfcOfferState
import com.bitcointxoko.gudariwallet.ui.SendState import com.bitcointxoko.gudariwallet.ui.SendState
import com.bitcointxoko.gudariwallet.ui.WalletViewModel import com.bitcointxoko.gudariwallet.ui.WalletViewModel
import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel
import com.bitcointxoko.gudariwallet.ui.receive.AmountDisplay
import com.bitcointxoko.gudariwallet.util.SendInputDetector import com.bitcointxoko.gudariwallet.util.SendInputDetector
import com.bitcointxoko.gudariwallet.util.feePpm import com.bitcointxoko.gudariwallet.util.feePpm
import com.bitcointxoko.gudariwallet.util.fiatLabel
import com.bitcointxoko.gudariwallet.util.formatFiat import com.bitcointxoko.gudariwallet.util.formatFiat
import com.bitcointxoko.gudariwallet.util.satsToFiat import com.bitcointxoko.gudariwallet.util.satsToFiat
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@@ -89,26 +94,10 @@ fun SendScreen(
text = strings.send, // ← "Send" text = strings.send, // ← "Send"
style = MaterialTheme.typography.headlineSmall style = MaterialTheme.typography.headlineSmall
) )
// NFC indicator — only visible in Idle/Error, lights up when offer pending
if (sendState is SendState.Idle || sendState is SendState.Error) {
FilledTonalIconButton(
onClick = { /* passive indicator */ },
modifier = Modifier.size(40.dp)
) {
Icon(
imageVector = Icons.Filled.Nfc,
contentDescription = strings.tapNfcHint, // ← "Tap NFC device to receive invoice"
tint = if (nfcOffer is NfcOfferState.Detected)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
}
} }
Spacer(Modifier.height(16.dp))
if (sendState !is SendState.PaymentSent) { if (sendState !is SendState.PaymentSent) {
OutlinedTextField( OutlinedTextField(
value = input, value = input,
@@ -206,42 +195,60 @@ fun SendScreen(
} }
is SendState.Paying -> { is SendState.Paying -> {
var slowPayment by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(10_000L)
slowPayment = true
}
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
CircularProgressIndicator() Column(
Spacer(Modifier.height(8.dp)) modifier = Modifier.fillMaxWidth(),
Text(strings.sendingPayment) // ← "Sending payment…" horizontalAlignment = Alignment.CenterHorizontally
) {
if (slowPayment) {
// Slow path — replace spinner with explanatory text
Icon(
imageVector = Icons.Outlined.HourglassTop,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(32.dp)
)
Spacer(Modifier.height(12.dp))
Text(
text = strings.paymentTakingLong, // "This is taking longer than usual"
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(8.dp))
Text(
text = strings.paymentTakingLongDetail,
// "Your payment is still on its way. This may be a hold invoice
// that settles when the recipient is ready. You'll get a
// notification when it completes — you can safely leave this screen."
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(20.dp))
OutlinedButton(
onClick = { vm.resetSendState(); input = "" },
modifier = Modifier.fillMaxWidth()
) {
Text(strings.dismissPayment) // "Dismiss"
}
} else {
// Fast path — normal spinner
CircularProgressIndicator()
Spacer(Modifier.height(8.dp))
Text(strings.sendingPayment) // "Sending payment…"
}
}
} }
is SendState.PaymentSent -> { is SendState.PaymentSent -> {
Spacer(Modifier.height(32.dp)) val fiatLabel = fiatLabel(state.amountSats, fiatRate, fiatCurrency)
Text(
text = "",
style = MaterialTheme.typography.displayLarge,
color = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.height(8.dp))
Text(
text = strings.paymentSent, // ← "Payment sent!"
style = MaterialTheme.typography.headlineSmall
)
Spacer(Modifier.height(4.dp))
Text(
text = strings.amountSats(state.amountSats), // ← "X sats"
color = MaterialTheme.colorScheme.onSurfaceVariant
)
val fiatAmount = if (fiatRate != null && fiatCurrency != null) {
satsToFiat(state.amountSats, fiatRate)
} else null
if (fiatAmount != null && fiatCurrency != null) {
Text(
text = strings.fiatEquivalent( // ← "≈ X.XX EUR"
formatFiat(fiatAmount, fiatCurrency)
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(Modifier.height(4.dp))
val ppm = if (state.feeSats != null && state.feeSats != 0L) { val ppm = if (state.feeSats != null && state.feeSats != 0L) {
feePpm( feePpm(
amountMsat = state.amountSats * 1_000L, amountMsat = state.amountSats * 1_000L,
@@ -256,27 +263,48 @@ fun SendScreen(
strings.routingFee(state.feeSats, ppmSuffix) // ← "Routing fee: X sats (Y ppm)" strings.routingFee(state.feeSats, ppmSuffix) // ← "Routing fee: X sats (Y ppm)"
} }
} }
Text(
text = feeText, Column(
style = MaterialTheme.typography.bodySmall, modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.onSurfaceVariant horizontalAlignment = Alignment.CenterHorizontally,
) verticalArrangement = Arrangement.Center
Spacer(Modifier.height(32.dp)) ) {
OutlinedButton( Icon(
onClick = { imageVector = Icons.Filled.CheckCircle,
val record = state.pendingRecord contentDescription = null,
if (record != null) { tint = MaterialTheme.colorScheme.primary,
onViewDetails(state.paymentHash, record) modifier = Modifier.size(72.dp)
} )
}, Spacer(Modifier.height(16.dp))
enabled = state.pendingRecord != null, Text(
modifier = Modifier.fillMaxWidth() text = strings.paymentSent, // ← "Payment sent!"
) { Text(strings.details) } // ← "Details" style = MaterialTheme.typography.headlineSmall
Spacer(Modifier.height(8.dp)) )
Button( Spacer(Modifier.height(8.dp))
onClick = { vm.resetSendState(); input = "" }, AmountDisplay(amountSats = state.amountSats, fiatLabel = fiatLabel)
modifier = Modifier.fillMaxWidth() Spacer(Modifier.height(4.dp))
) { Text(strings.sendAnother) } // ← "Send Another" Text(
text = feeText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(32.dp))
Button(
onClick = {
val record = state.pendingRecord
if (record != null) {
onViewDetails(state.paymentHash, record)
}
},
enabled = state.pendingRecord != null,
modifier = Modifier.fillMaxWidth()
) { Text(strings.details) } // ← "Details"
Spacer(Modifier.height(32.dp))
TextButton(
onClick = { vm.resetSendState(); input = "" },
modifier = Modifier.fillMaxWidth()
) { Text(strings.sendAnother) } // ← "Send Another"
}
} }
is SendState.Error -> { is SendState.Error -> {