218 lines
8.6 KiB
Kotlin
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") }
|
|
}
|
|
}
|
|
}
|
|
} |