Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e11cecbeb | |||
| e57414de17 |
@@ -92,7 +92,8 @@ sealed class SendState {
|
|||||||
val amountSats : Long,
|
val amountSats : Long,
|
||||||
val feeSats : Long? = null,
|
val feeSats : Long? = null,
|
||||||
val paymentHash: String,
|
val paymentHash: String,
|
||||||
val pendingRecord: PaymentRecord? = null
|
val pendingRecord: PaymentRecord? = null,
|
||||||
|
val lnurl : String? = null
|
||||||
) : SendState()
|
) : SendState()
|
||||||
data class Error(val message: String) : SendState()
|
data class Error(val message: String) : SendState()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -537,19 +537,30 @@ fun WalletScreen(
|
|||||||
composable(
|
composable(
|
||||||
route = ROUTE_CONTACT_DETAIL,
|
route = ROUTE_CONTACT_DETAIL,
|
||||||
arguments = listOf(navArgument("contactId") { type = NavType.StringType }),
|
arguments = listOf(navArgument("contactId") { type = NavType.StringType }),
|
||||||
enterTransition = { fadeIn(animationSpec = tween(150)) },
|
enterTransition = { fadeIn(animationSpec = tween(150)) },
|
||||||
popExitTransition = { fadeOut(animationSpec = tween(150)) }
|
popExitTransition = { fadeOut(animationSpec = tween(150)) }
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
val contactId = backStackEntry.arguments?.getString("contactId") ?: ""
|
val contactId = backStackEntry.arguments?.getString("contactId") ?: ""
|
||||||
val detailVm: ContactDetailViewModel = viewModel(
|
val detailVm: ContactDetailViewModel = viewModel(
|
||||||
key = contactId,
|
key = contactId,
|
||||||
factory = ContactDetailViewModel.Factory(contactId, vm.contactRepo)
|
factory = ContactDetailViewModel.Factory(contactId, vm.contactRepo)
|
||||||
)
|
)
|
||||||
ContactDetailScreen(
|
ContactDetailScreen(
|
||||||
viewModel = detailVm,
|
viewModel = detailVm,
|
||||||
onBack = { navController.popBackStack() }
|
onBack = { navController.popBackStack() },
|
||||||
|
onPayAddress = { address ->
|
||||||
|
vm.prefillSend(address)
|
||||||
|
navController.navigate(TabItem.Send.route) {
|
||||||
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = false // don't restore stale SendScreen state
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Clipboard banner — floats over content, slides in from top ────
|
// ── Clipboard banner — floats over content, slides in from top ────
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
|
|||||||
import com.bitcointxoko.gudariwallet.ui.send.SendEvent
|
import com.bitcointxoko.gudariwallet.ui.send.SendEvent
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
|
import com.bitcointxoko.gudariwallet.ui.send.SendStrings
|
||||||
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -116,6 +117,16 @@ class WalletViewModel(
|
|||||||
|
|
||||||
val sendEvents: SharedFlow<SendEvent> = sendVm.events
|
val sendEvents: SharedFlow<SendEvent> = sendVm.events
|
||||||
|
|
||||||
|
// ── Contact tap-to-pay prefill ────────────────────────────────────────────
|
||||||
|
private val _pendingSend = MutableStateFlow<String?>(null)
|
||||||
|
val pendingSend: StateFlow<String?> get() = _pendingSend
|
||||||
|
|
||||||
|
/** Called by ContactDetailScreen before navigating to the Send tab. */
|
||||||
|
fun prefillSend(address: String) { _pendingSend.value = address }
|
||||||
|
|
||||||
|
/** Called by SendScreen once it has consumed the prefilled address. */
|
||||||
|
fun consumePendingSend() { _pendingSend.value = null }
|
||||||
|
|
||||||
fun handleWithdrawScanned(raw: LnurlScanResponse, lnurl: String) {
|
fun handleWithdrawScanned(raw: LnurlScanResponse, lnurl: String) {
|
||||||
receiveVm.handleWithdrawResponse(raw, lnurl)
|
receiveVm.handleWithdrawResponse(raw, lnurl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.bitcointxoko.gudariwallet.ui.contacts
|
package com.bitcointxoko.gudariwallet.ui.contacts
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -37,7 +38,8 @@ import com.bitcointxoko.gudariwallet.ui.common.DetailSection
|
|||||||
@Composable
|
@Composable
|
||||||
fun ContactDetailScreen(
|
fun ContactDetailScreen(
|
||||||
viewModel: ContactDetailViewModel,
|
viewModel: ContactDetailViewModel,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit,
|
||||||
|
onPayAddress: (address: String) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val strings = LocalAppStrings.current
|
val strings = LocalAppStrings.current
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
@@ -127,6 +129,7 @@ fun ContactDetailScreen(
|
|||||||
isEditing = isEditing,
|
isEditing = isEditing,
|
||||||
onRemoveAddress = { addressId -> pendingAddressDelete = addressId },
|
onRemoveAddress = { addressId -> pendingAddressDelete = addressId },
|
||||||
onSetDefault = { viewModel.setDefaultAddress(it) },
|
onSetDefault = { viewModel.setDefaultAddress(it) },
|
||||||
|
onPayAddress = onPayAddress,
|
||||||
onEditAlias = { viewModel.openEditAliasSheet() },
|
onEditAlias = { viewModel.openEditAliasSheet() },
|
||||||
onDeleteContact = { viewModel.requestDelete() },
|
onDeleteContact = { viewModel.requestDelete() },
|
||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
@@ -209,6 +212,7 @@ private fun ContactDetailContent(
|
|||||||
isEditing: Boolean,
|
isEditing: Boolean,
|
||||||
onRemoveAddress: (addressId: String) -> Unit,
|
onRemoveAddress: (addressId: String) -> Unit,
|
||||||
onSetDefault: (PaymentAddressEntity) -> Unit,
|
onSetDefault: (PaymentAddressEntity) -> Unit,
|
||||||
|
onPayAddress: (address: String) -> Unit,
|
||||||
onEditAlias: () -> Unit,
|
onEditAlias: () -> Unit,
|
||||||
onDeleteContact: () -> Unit,
|
onDeleteContact: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -276,6 +280,7 @@ private fun ContactDetailContent(
|
|||||||
AddressRow(
|
AddressRow(
|
||||||
address = addr,
|
address = addr,
|
||||||
isEditing = isEditing,
|
isEditing = isEditing,
|
||||||
|
onPay = { onPayAddress(addr.address) },
|
||||||
onRemove = { onRemoveAddress(addr.id) },
|
onRemove = { onRemoveAddress(addr.id) },
|
||||||
onSetDefault = { onSetDefault(addr) }
|
onSetDefault = { onSetDefault(addr) }
|
||||||
)
|
)
|
||||||
@@ -349,6 +354,7 @@ private fun ContactDetailContent(
|
|||||||
private fun AddressRow(
|
private fun AddressRow(
|
||||||
address: PaymentAddressEntity,
|
address: PaymentAddressEntity,
|
||||||
isEditing: Boolean,
|
isEditing: Boolean,
|
||||||
|
onPay: () -> Unit,
|
||||||
onRemove: () -> Unit,
|
onRemove: () -> Unit,
|
||||||
onSetDefault: () -> Unit,
|
onSetDefault: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -365,6 +371,10 @@ private fun AddressRow(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
enabled = !isEditing,
|
||||||
|
onClickLabel = "Pay with this address"
|
||||||
|
) { onPay() }
|
||||||
.padding(vertical = 12.dp),
|
.padding(vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.ui.ReceiveState
|
import com.bitcointxoko.gudariwallet.ui.ReceiveState
|
||||||
import com.bitcointxoko.gudariwallet.ui.WalletViewModel
|
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.QrDisplayCard
|
||||||
import com.bitcointxoko.gudariwallet.ui.common.rememberBrightnessController
|
import com.bitcointxoko.gudariwallet.ui.common.rememberBrightnessController
|
||||||
import com.bitcointxoko.gudariwallet.ui.common.UnitWheelPicker
|
import com.bitcointxoko.gudariwallet.ui.common.UnitWheelPicker
|
||||||
@@ -156,16 +157,21 @@ fun ReceiveScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is ReceiveState.PaymentReceived -> {
|
is ReceiveState.PaymentReceived -> {
|
||||||
ReceiveSuccessContent(
|
PaymentSuccessContent(
|
||||||
amountSats = state.amountSats,
|
title = strings.receive.paymentReceived,
|
||||||
memo = state.memo,
|
amountSats = state.amountSats,
|
||||||
fiatRate = fiatRate,
|
fiatLabel = fiatLabel(state.amountSats, fiatRate, fiatCurrency),
|
||||||
fiatCurrency = fiatCurrency,
|
subLine = state.memo?.takeIf { it.isNotBlank() },
|
||||||
onDone = { vm.resetReceiveState() },
|
onDetails = { onNavigateToPaymentDetail(state.checkingId) },
|
||||||
onDetails = { onNavigateToPaymentDetail(state.checkingId) }
|
secondaryLabel = strings.done,
|
||||||
|
onSecondary = { vm.resetReceiveState() },
|
||||||
|
lnurl = null, // sender identity not available
|
||||||
|
onSaveContact = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
is ReceiveState.Error -> {
|
is ReceiveState.Error -> {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Column(
|
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.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.common.PaymentSuccessContent
|
||||||
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.ui.receive.AmountDisplay
|
||||||
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
import com.bitcointxoko.gudariwallet.util.SendInputDetector
|
||||||
@@ -59,6 +60,17 @@ fun SendScreen(
|
|||||||
}
|
}
|
||||||
val fiatCurrency = (balanceState as? BalanceState.Success)?.fiatCurrency
|
val fiatCurrency = (balanceState as? BalanceState.Success)?.fiatCurrency
|
||||||
|
|
||||||
|
// ── Consume address prefilled from Contacts tap-to-pay ───────────────────
|
||||||
|
val pendingSend by vm.pendingSend.collectAsStateWithLifecycle()
|
||||||
|
LaunchedEffect(pendingSend) {
|
||||||
|
val address = pendingSend
|
||||||
|
if (!address.isNullOrBlank() && sendState is SendState.Idle) {
|
||||||
|
input = SendInputDetector.normalize(address)
|
||||||
|
vm.scan(address, sendStrings)
|
||||||
|
vm.consumePendingSend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Collect NFC offer at top level — never inside a conditional ───────────
|
// ── Collect NFC offer at top level — never inside a conditional ───────────
|
||||||
val nfcOffer by nfcVm.nfcOfferState.collectAsStateWithLifecycle()
|
val nfcOffer by nfcVm.nfcOfferState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -275,56 +287,31 @@ fun SendScreen(
|
|||||||
} else null
|
} else null
|
||||||
val feeText = when {
|
val feeText = when {
|
||||||
state.feeSats == null || state.feeSats == 0L ->
|
state.feeSats == null || state.feeSats == 0L ->
|
||||||
strings.noFees // ← "No fees"
|
strings.noFees
|
||||||
else -> {
|
else -> {
|
||||||
val ppmSuffix = if (ppm != null) " (${ppm} ppm)" 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(
|
PaymentSuccessContent(
|
||||||
modifier = Modifier.fillMaxSize(),
|
title = strings.paymentSent,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
amountSats = state.amountSats,
|
||||||
verticalArrangement = Arrangement.Center
|
fiatLabel = fiatLabel,
|
||||||
) {
|
subLine = feeText,
|
||||||
Icon(
|
detailsEnabled = state.pendingRecord != null,
|
||||||
imageVector = Icons.Filled.CheckCircle,
|
onDetails = {
|
||||||
contentDescription = null,
|
val record = state.pendingRecord
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
if (record != null) onViewDetails(state.paymentHash, record)
|
||||||
modifier = Modifier.size(72.dp)
|
},
|
||||||
)
|
secondaryLabel = strings.sendAnother,
|
||||||
Spacer(Modifier.height(16.dp))
|
onSecondary = { vm.resetSendState(); input = "" },
|
||||||
Text(
|
lnurl = state.lnurl, // null for plain Bolt11
|
||||||
text = strings.paymentSent, // ← "Payment sent!"
|
onSaveContact = vm::saveContactFromSend
|
||||||
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 = {
|
|
||||||
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 -> {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -134,20 +134,21 @@ class SendViewModel(
|
|||||||
pubkey : String?,
|
pubkey : String?,
|
||||||
alias : String?,
|
alias : String?,
|
||||||
strings : SendStrings,
|
strings : SendStrings,
|
||||||
|
lnurl : String? = null
|
||||||
) {
|
) {
|
||||||
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
val detail = runCatching { repo.getPaymentDetail(paymentHash) }.getOrNull()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
// ── Settled immediately
|
// ── Settled immediately
|
||||||
detail?.paid == true -> {
|
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
|
// ── Hold invoice — poll
|
||||||
detail?.details?.status == "pending" || detail?.details?.pending == true -> {
|
detail?.details?.status == "pending" || detail?.details?.pending == true -> {
|
||||||
when (val result = poller.poll(paymentHash)) {
|
when (val result = poller.poll(paymentHash)) {
|
||||||
is PaymentPoller.PollResult.Settled ->
|
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 ->
|
is PaymentPoller.PollResult.Failed ->
|
||||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||||
@@ -162,7 +163,7 @@ class SendViewModel(
|
|||||||
Timber.w("SEND [DETAIL NULL] polling as fallback")
|
Timber.w("SEND [DETAIL NULL] polling as fallback")
|
||||||
when (val result = poller.poll(paymentHash)) {
|
when (val result = poller.poll(paymentHash)) {
|
||||||
is PaymentPoller.PollResult.Settled ->
|
is PaymentPoller.PollResult.Settled ->
|
||||||
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias)
|
emitPaymentSent(paymentHash, amountSats, result.feeSats, bolt11, memo, pubkey, alias, lnurl)
|
||||||
else ->
|
else ->
|
||||||
_sendState.value = SendState.Error(strings.paymentFailed)
|
_sendState.value = SendState.Error(strings.paymentFailed)
|
||||||
}
|
}
|
||||||
@@ -183,12 +184,14 @@ class SendViewModel(
|
|||||||
memo : String?,
|
memo : String?,
|
||||||
pubkey : String?,
|
pubkey : String?,
|
||||||
alias : String?,
|
alias : String?,
|
||||||
|
lnurl : String? = null
|
||||||
) {
|
) {
|
||||||
_sendState.value = SendState.PaymentSent(
|
_sendState.value = SendState.PaymentSent(
|
||||||
amountSats = amountSats,
|
amountSats = amountSats,
|
||||||
feeSats = feeSats,
|
feeSats = feeSats,
|
||||||
paymentHash = paymentHash,
|
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)
|
_events.tryEmit(SendEvent.PaymentCompleted)
|
||||||
}
|
}
|
||||||
@@ -224,6 +227,7 @@ class SendViewModel(
|
|||||||
pubkey = null,
|
pubkey = null,
|
||||||
alias = null,
|
alias = null,
|
||||||
strings = strings,
|
strings = strings,
|
||||||
|
lnurl = lnurl
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
_sendState.value = SendState.Error(
|
_sendState.value = SendState.Error(
|
||||||
@@ -323,6 +327,7 @@ class SendViewModel(
|
|||||||
pubkey = state.payee,
|
pubkey = state.payee,
|
||||||
alias = state.nodeAlias,
|
alias = state.nodeAlias,
|
||||||
strings = strings,
|
strings = strings,
|
||||||
|
lnurl = state.lnurl
|
||||||
)
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user