From cfde0755cd03ac7e4413c5c37aa1f63d3fd52aab Mon Sep 17 00:00:00 2001 From: rasputin Date: Fri, 5 Jun 2026 17:17:19 +0200 Subject: [PATCH] feat: add sending by nfc --- .../gudariwallet/ui/WalletScreen.kt | 1 + .../gudariwallet/ui/send/SendScreen.kt | 62 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt index ad1b100..8c79ab0 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/WalletScreen.kt @@ -296,6 +296,7 @@ fun WalletScreen( composable(TabItem.Send.route) { SendScreen( vm = vm, + nfcVm = nfcVm, onViewDetails = { checkingId -> historyVm.refresh() navController.navigate("payment_detail/$checkingId") { diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt index a855321..3ec8fb7 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/send/SendScreen.kt @@ -7,6 +7,7 @@ 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.material.icons.filled.Nfc import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -21,8 +22,10 @@ import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.nfc.NfcViewModel import com.bitcointxoko.gudariwallet.util.SendInputDetector import com.bitcointxoko.gudariwallet.util.feePpm import com.bitcointxoko.gudariwallet.util.formatFiat @@ -30,21 +33,38 @@ import com.bitcointxoko.gudariwallet.util.satsToFiat import kotlinx.coroutines.launch @Composable -fun SendScreen(vm: WalletViewModel, onViewDetails: (paymentHash: String) -> Unit = {}) { - 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() - val balanceState by vm.balanceState.collectAsStateWithLifecycle() - val fiatRate = (balanceState as? BalanceState.Success)?.let { s -> +fun SendScreen( + vm : WalletViewModel, + nfcVm : NfcViewModel, + onViewDetails: (paymentHash: String) -> Unit = {} +) { + 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() + val balanceState by vm.balanceState.collectAsStateWithLifecycle() + val fiatRate = (balanceState as? BalanceState.Success)?.let { s -> s.fiatAmount?.let { if (s.sats > 0) s.sats.toDouble() / it else null } } - val fiatCurrency = (balanceState as? BalanceState.Success)?.fiatCurrency + val fiatCurrency = (balanceState as? BalanceState.Success)?.fiatCurrency + + // ── Collect NFC offer at top level — never inside a conditional ─────────── + val nfcOffer by nfcVm.nfcOfferState.collectAsStateWithLifecycle() DisposableEffect(Unit) { onDispose { vm.resetSendState() } } + // ── Auto-trigger scan when NFC offer arrives ────────────────────────────── + LaunchedEffect(nfcOffer) { + val offer = nfcOffer + if (offer is NfcOfferState.Detected && sendState is SendState.Idle) { + input = offer.raw + vm.scan(offer.raw) + nfcVm.dismissNfcOffer() + } + } + Column( modifier = Modifier .fillMaxSize() @@ -55,13 +75,31 @@ fun SendScreen(vm: WalletViewModel, onViewDetails: (paymentHash: String) -> Unit ) { Spacer(Modifier.height(32.dp)) - // ── Header row (matches ReceiveIdleContent) ────────────────────────── + // ── Header row ──────────────────────────────────────────────────────── Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Text("Send", 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 = "Tap NFC device to receive invoice", + tint = if (nfcOffer is NfcOfferState.Detected) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + } + } } if (sendState !is SendState.PaymentSent) { @@ -237,4 +275,4 @@ fun SendScreen(vm: WalletViewModel, onViewDetails: (paymentHash: String) -> Unit } } } -} \ No newline at end of file +}