Files
gudari/app/src/main/java/com/bitcointxoko/gudariwallet/ui/SendScreen.kt
T
2026-06-01 03:09:19 +02:00

218 lines
8.6 KiB
Kotlin

package com.bitcointxoko.gudariwallet.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitcointxoko.gudariwallet.ui.send.Bolt11ConfirmCard
import com.bitcointxoko.gudariwallet.ui.send.LnurlInvoiceConfirmCard
import com.bitcointxoko.gudariwallet.ui.send.LnurlPayForm
import com.bitcointxoko.gudariwallet.util.SendInputDetector
import com.bitcointxoko.gudariwallet.util.feePpm
import kotlinx.coroutines.launch
@Composable
fun SendScreen(vm: WalletViewModel) {
val sendState: SendState by vm.sendState.collectAsStateWithLifecycle()
var input by remember { mutableStateOf("") }
val context = LocalContext.current
val activity = context as FragmentActivity
val clipboard = LocalClipboard.current
val scope = rememberCoroutineScope()
// Read rate directly from VM cache — same source HomeTab uses.
// Both are null when no currency is selected; fiat display is suppressed.
val fiatRate = vm.fiatRate // Double?
val fiatCurrency = vm.fiatCurrency // String?
DisposableEffect(Unit) { onDispose { vm.resetSendState() } }
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
Spacer(Modifier.height(16.dp))
Text("Send", style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.height(24.dp))
if (sendState !is SendState.PaymentSent) {
OutlinedTextField(
value = input,
onValueChange = { input = SendInputDetector.normalize(it) },
label = { Text("Invoice, LNURL, or Lightning Address") },
placeholder = { Text("lnbc… / lnurl1… / user@domain.com") },
modifier = Modifier.fillMaxWidth(),
enabled = sendState is SendState.Idle || sendState is SendState.Error,
singleLine = false,
maxLines = 4,
textStyle = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Go
),
keyboardActions = KeyboardActions(
onGo = { if (input.isNotBlank()) vm.scan(input) }
),
)
Spacer(Modifier.height(8.dp))
}
when (val state = sendState) {
is SendState.Idle -> {
Button(
onClick = {
scope.launch {
val text = clipboard.getClipEntry()
?.clipData
?.getItemAt(0)
?.text
?.toString()
if (!text.isNullOrBlank()) {
input = SendInputDetector.normalize(text)
vm.scan(text)
}
}
},
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(8.dp))
Text("Paste")
}
Spacer(Modifier.height(8.dp))
OutlinedButton(
onClick = { if (input.isNotBlank()) vm.scan(input) },
enabled = input.isNotBlank(),
modifier = Modifier.fillMaxWidth()
) { Text("Continue") }
}
is SendState.Scanning -> {
Spacer(Modifier.height(16.dp))
CircularProgressIndicator()
Spacer(Modifier.height(8.dp))
Text("Resolving…")
}
is SendState.Bolt11Decoded -> {
Bolt11ConfirmCard(
state = state,
activity = activity,
vm = vm,
fiatRate = fiatRate,
fiatCurrency = fiatCurrency
)
}
is SendState.LnurlReady -> {
LnurlPayForm(
state = state,
vm = vm,
fiatRate = fiatRate,
fiatCurrency = fiatCurrency
)
}
is SendState.LnurlInvoiceReady -> {
LnurlInvoiceConfirmCard(
state = state,
activity = activity,
vm = vm,
fiatRate = fiatRate,
fiatCurrency = fiatCurrency
)
}
is SendState.Paying -> {
Spacer(Modifier.height(16.dp))
CircularProgressIndicator()
Spacer(Modifier.height(8.dp))
Text("Sending payment…")
}
is SendState.PaymentSent -> {
Spacer(Modifier.height(32.dp))
Text(
text = "",
style = MaterialTheme.typography.displayLarge,
color = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.height(8.dp))
Text("Payment sent!", style = MaterialTheme.typography.headlineSmall)
Spacer(Modifier.height(4.dp))
Text(
text = "${state.amountSats} sats",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(4.dp))
val feeText = when {
state.feeSats == null || state.feeSats == 0L -> "No fees"
else -> {
val ppm = feePpm(
amountMsat = state.amountSats * 1_000L,
feeMsat = state.feeSats * 1_000L
)
val ppmSuffix = if (ppm != null) " (${ppm} ppm)" else ""
"Routing fee: ${state.feeSats} sat${if (state.feeSats == 1L) "" else "s"}$ppmSuffix"
}
}
Text(
text = feeText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(32.dp))
Button(
onClick = { vm.resetSendState(); input = "" },
modifier = Modifier.fillMaxWidth()
) { Text("Send Another") }
}
is SendState.Error -> {
Spacer(Modifier.height(8.dp))
Text(
text = state.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
Spacer(Modifier.height(12.dp))
Button(
onClick = { if (input.isNotBlank()) vm.scan(input) },
enabled = input.isNotBlank(),
modifier = Modifier.fillMaxWidth()
) { Text("Try Again") }
Spacer(Modifier.height(8.dp))
OutlinedButton(
onClick = { vm.resetSendState(); input = "" },
modifier = Modifier.fillMaxWidth()
) { Text("Clear") }
}
}
}
}