refactor: extract PaymentSuccessContent
This commit is contained in:
@@ -92,7 +92,8 @@ sealed class SendState {
|
||||
val amountSats : Long,
|
||||
val feeSats : Long? = null,
|
||||
val paymentHash: String,
|
||||
val pendingRecord: PaymentRecord? = null
|
||||
val pendingRecord: PaymentRecord? = null,
|
||||
val lnurl : String? = null
|
||||
) : SendState()
|
||||
data class Error(val message: String) : SendState()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.PersonAdd
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||
import com.bitcointxoko.gudariwallet.data.db.PaymentAddressType
|
||||
import com.bitcointxoko.gudariwallet.ui.contacts.CreateContactSheet
|
||||
import com.bitcointxoko.gudariwallet.ui.receive.AmountDisplay
|
||||
|
||||
/**
|
||||
* Shared success screen used by both Send (PaymentSent) and Receive (PaymentReceived).
|
||||
*
|
||||
* @param title Headline string, e.g. strings.paymentSent / strings.receive.paymentReceived
|
||||
* @param amountSats Amount in sats.
|
||||
* @param fiatLabel Optional fiat conversion string (from [fiatLabel] util).
|
||||
* @param subLine Optional secondary line below amount: fee text (send) or memo (receive).
|
||||
* @param detailsEnabled Whether the Details button is clickable.
|
||||
* @param onDetails Navigate to payment detail.
|
||||
* @param secondaryLabel Label of the reset TextButton ("Send Another" / "Done").
|
||||
* @param onSecondary Reset screen state.
|
||||
* @param lnurl Pre-fill address for the save-contact sheet. Pass null to hide the button.
|
||||
* @param onSaveContact Forward to vm.saveContactFromSend — omit (null) to hide save-contact UI.
|
||||
*/
|
||||
@Composable
|
||||
fun PaymentSuccessContent(
|
||||
title : String,
|
||||
amountSats : Long,
|
||||
fiatLabel : String?,
|
||||
subLine : String? = null,
|
||||
detailsEnabled : Boolean = true,
|
||||
onDetails : () -> Unit,
|
||||
secondaryLabel : String,
|
||||
onSecondary : () -> Unit,
|
||||
lnurl : String? = null,
|
||||
onSaveContact : ((name: String, addressType: PaymentAddressType?, address: String?) -> Unit)? = null,
|
||||
) {
|
||||
var showSaveSheet by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(72.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(text = title, style = MaterialTheme.typography.headlineSmall)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AmountDisplay(amountSats = amountSats, fiatLabel = fiatLabel)
|
||||
if (!subLine.isNullOrBlank()) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = subLine,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
// ── Save contact affordance (send-side only) ─────────────────────────
|
||||
if (lnurl != null && onSaveContact != null) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
OutlinedButton(
|
||||
onClick = { showSaveSheet = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PersonAdd,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("Save contact")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Button(
|
||||
onClick = onDetails,
|
||||
enabled = detailsEnabled,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text(LocalAppStrings.current.details) }
|
||||
Spacer(Modifier.height(32.dp))
|
||||
TextButton(
|
||||
onClick = onSecondary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text(secondaryLabel) }
|
||||
}
|
||||
|
||||
// ── Bottom sheet ─────────────────────────────────────────────────────────
|
||||
if (showSaveSheet && lnurl != null && onSaveContact != null) {
|
||||
// Detect whether the string is a Lightning Address (user@domain) or LNURL
|
||||
val isLnAddress = lnurl.contains('@') && !lnurl.startsWith("lnurl", ignoreCase = true)
|
||||
val initialType = if (isLnAddress)
|
||||
PaymentAddressType.LIGHTNING_ADDRESS
|
||||
else
|
||||
PaymentAddressType.LNURL
|
||||
|
||||
CreateContactSheet(
|
||||
initialAddress = lnurl,
|
||||
initialAddressType = initialType,
|
||||
onSave = { name, addressType, address ->
|
||||
onSaveContact(name, addressType, address)
|
||||
showSaveSheet = false
|
||||
},
|
||||
onDismiss = { showSaveSheet = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||
import com.bitcointxoko.gudariwallet.ui.ReceiveState
|
||||
import com.bitcointxoko.gudariwallet.ui.WalletViewModel
|
||||
import com.bitcointxoko.gudariwallet.ui.common.PaymentSuccessContent
|
||||
import com.bitcointxoko.gudariwallet.ui.common.QrDisplayCard
|
||||
import com.bitcointxoko.gudariwallet.ui.common.rememberBrightnessController
|
||||
import com.bitcointxoko.gudariwallet.ui.common.UnitWheelPicker
|
||||
@@ -156,16 +157,21 @@ fun ReceiveScreen(
|
||||
}
|
||||
|
||||
is ReceiveState.PaymentReceived -> {
|
||||
ReceiveSuccessContent(
|
||||
PaymentSuccessContent(
|
||||
title = strings.receive.paymentReceived,
|
||||
amountSats = state.amountSats,
|
||||
memo = state.memo,
|
||||
fiatRate = fiatRate,
|
||||
fiatCurrency = fiatCurrency,
|
||||
onDone = { vm.resetReceiveState() },
|
||||
onDetails = { onNavigateToPaymentDetail(state.checkingId) }
|
||||
fiatLabel = fiatLabel(state.amountSats, fiatRate, fiatCurrency),
|
||||
subLine = state.memo?.takeIf { it.isNotBlank() },
|
||||
onDetails = { onNavigateToPaymentDetail(state.checkingId) },
|
||||
secondaryLabel = strings.done,
|
||||
onSecondary = { vm.resetReceiveState() },
|
||||
lnurl = null, // sender identity not available
|
||||
onSaveContact = null,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
is ReceiveState.Error -> {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(
|
||||
@@ -723,50 +729,3 @@ private fun ReceiveInvoiceContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── ReceiveSuccessContent ─────────────────────────────────────────────────────
|
||||
|
||||
@Composable
|
||||
private fun ReceiveSuccessContent(
|
||||
amountSats : Long,
|
||||
memo : String?,
|
||||
fiatRate : Double?,
|
||||
fiatCurrency : String?,
|
||||
onDone : () -> Unit,
|
||||
onDetails : () -> Unit
|
||||
) {
|
||||
val strings = LocalAppStrings.current
|
||||
val fiatLabel = fiatLabel(amountSats, fiatRate, fiatCurrency)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(72.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = strings.receive.paymentReceived, // ← "Payment Received!"
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AmountDisplay(amountSats = amountSats, fiatLabel = fiatLabel)
|
||||
if (!memo.isNullOrBlank()) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(memo, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Button(onClick = onDetails, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(strings.details) // ← "Details"
|
||||
}
|
||||
Spacer(Modifier.height(32.dp))
|
||||
TextButton(onClick = onDone, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(strings.done) // ← "Done"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.bitcointxoko.gudariwallet.ui.BalanceState
|
||||
import com.bitcointxoko.gudariwallet.ui.NfcOfferState
|
||||
import com.bitcointxoko.gudariwallet.ui.SendState
|
||||
import com.bitcointxoko.gudariwallet.ui.WalletViewModel
|
||||
import com.bitcointxoko.gudariwallet.ui.common.PaymentSuccessContent
|
||||
import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel
|
||||
import com.bitcointxoko.gudariwallet.ui.receive.AmountDisplay
|
||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||
@@ -286,56 +287,31 @@ fun SendScreen(
|
||||
} else null
|
||||
val feeText = when {
|
||||
state.feeSats == null || state.feeSats == 0L ->
|
||||
strings.noFees // ← "No fees"
|
||||
strings.noFees
|
||||
else -> {
|
||||
val ppmSuffix = if (ppm != null) " (${ppm} ppm)" else ""
|
||||
strings.routingFee(state.feeSats, ppmSuffix) // ← "Routing fee: X sats (Y ppm)"
|
||||
strings.routingFee(state.feeSats, ppmSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(72.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
text = strings.paymentSent, // ← "Payment sent!"
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
AmountDisplay(amountSats = state.amountSats, fiatLabel = fiatLabel)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = feeText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
PaymentSuccessContent(
|
||||
title = strings.paymentSent,
|
||||
amountSats = state.amountSats,
|
||||
fiatLabel = fiatLabel,
|
||||
subLine = feeText,
|
||||
detailsEnabled = state.pendingRecord != null,
|
||||
onDetails = {
|
||||
val record = state.pendingRecord
|
||||
if (record != null) {
|
||||
onViewDetails(state.paymentHash, record)
|
||||
}
|
||||
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"
|
||||
}
|
||||
secondaryLabel = strings.sendAnother,
|
||||
onSecondary = { vm.resetSendState(); input = "" },
|
||||
lnurl = state.lnurl, // null for plain Bolt11
|
||||
onSaveContact = vm::saveContactFromSend
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
is SendState.Error -> {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(
|
||||
|
||||
@@ -134,20 +134,21 @@ class SendViewModel(
|
||||
pubkey : String?,
|
||||
alias : String?,
|
||||
strings : SendStrings,
|
||||
lnurl : String? = null
|
||||
) {
|
||||
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
||||
|
||||
when {
|
||||
// ── Settled immediately
|
||||
detail?.paid == true -> {
|
||||
emitPaymentSent(paymentHash, amountSats, detail.details?.feeSat, bolt11, memo, pubkey, alias)
|
||||
emitPaymentSent(paymentHash, amountSats, detail.details?.feeSat, bolt11, memo, pubkey, alias, lnurl)
|
||||
}
|
||||
|
||||
// ── Hold invoice — poll
|
||||
detail?.details?.status == "pending" || detail?.details?.pending == true -> {
|
||||
when (val result = poller.poll(paymentHash)) {
|
||||
is PaymentPoller.PollResult.Settled ->
|
||||
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias)
|
||||
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias, lnurl)
|
||||
|
||||
is PaymentPoller.PollResult.Failed ->
|
||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||
@@ -162,7 +163,7 @@ class SendViewModel(
|
||||
Timber.w("SEND [DETAIL NULL] polling as fallback")
|
||||
when (val result = poller.poll(paymentHash)) {
|
||||
is PaymentPoller.PollResult.Settled ->
|
||||
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias)
|
||||
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias, lnurl)
|
||||
else ->
|
||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||
}
|
||||
@@ -183,12 +184,14 @@ class SendViewModel(
|
||||
memo : String?,
|
||||
pubkey : String?,
|
||||
alias : String?,
|
||||
lnurl : String? = null
|
||||
) {
|
||||
_sendState.value = SendState.PaymentSent(
|
||||
amountSats = amountSats,
|
||||
feeSats = feeSats,
|
||||
paymentHash = paymentHash,
|
||||
pendingRecord = PaymentRecord.fromPayment(paymentHash, amountSats, feeSats, bolt11, memo, pubkey, alias)
|
||||
pendingRecord = PaymentRecord.fromPayment(paymentHash, amountSats, feeSats, bolt11, memo, pubkey, alias),
|
||||
lnurl = lnurl
|
||||
)
|
||||
_events.tryEmit(SendEvent.PaymentCompleted)
|
||||
}
|
||||
@@ -224,6 +227,7 @@ class SendViewModel(
|
||||
pubkey = null,
|
||||
alias = null,
|
||||
strings = strings,
|
||||
lnurl = lnurl
|
||||
)
|
||||
} else {
|
||||
_sendState.value = SendState.Error(
|
||||
@@ -323,6 +327,7 @@ class SendViewModel(
|
||||
pubkey = state.payee,
|
||||
alias = state.nodeAlias,
|
||||
strings = strings,
|
||||
lnurl = state.lnurl
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user