feat: add nfc step 1

This commit is contained in:
2026-06-05 01:47:39 +02:00
parent 8e53a927a0
commit cc50ff08df
7 changed files with 340 additions and 9 deletions
@@ -25,6 +25,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.Nfc
import androidx.compose.material.icons.filled.QrCode
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material3.CircularProgressIndicator
@@ -160,6 +161,36 @@ fun WalletScreen(
}
}
// ── NFC reader mode — register on composition, unregister on disposal ────────
val nfcAdapter = remember {
android.nfc.NfcAdapter.getDefaultAdapter(context)
}
DisposableEffect(Unit) {
val activity = context as MainActivity
nfcAdapter?.enableReaderMode(
activity,
{ tag -> vm.onNfcTagDiscovered(tag) }, // runs on NFC binder thread
android.nfc.NfcAdapter.FLAG_READER_NFC_A or
android.nfc.NfcAdapter.FLAG_READER_NFC_B or
android.nfc.NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK.inv(), // keep NDEF
null
)
onDispose {
nfcAdapter?.disableReaderMode(activity)
}
}
val nfcOffer by vm.nfcOfferState.collectAsStateWithLifecycle()
// Auto-dismiss NFC banner after 8 seconds (same as clipboard)
LaunchedEffect(nfcOffer) {
if (nfcOffer is NfcOfferState.Detected) {
delay(8_000)
vm.dismissNfcOffer()
}
}
Scaffold(
bottomBar = {
if (showBottomBar) {
@@ -414,6 +445,70 @@ fun WalletScreen(
}
}
}
// ── NFC banner — floats below clipboard banner, slides in from top ────────────
AnimatedVisibility(
visible = nfcOffer is NfcOfferState.Detected,
enter = slideInVertically(initialOffsetY = { -it }),
exit = slideOutVertically(targetOffsetY = { -it }),
modifier = Modifier
.align(Alignment.TopCenter)
.zIndex(1f)
.fillMaxWidth()
.statusBarsPadding()
// push down if clipboard banner is also showing
.padding(top = if (clipboardOffer is ClipboardOfferState.Detected) 64.dp else 0.dp)
) {
if (nfcOffer is NfcOfferState.Detected) {
val offer = nfcOffer as NfcOfferState.Detected
val label = when (offer.inputType) {
is SendInputType.Bolt11 -> "Invoice"
is SendInputType.Lnurl -> "LNURL"
is SendInputType.LightningAddress -> "Lightning Address"
else -> "Payment"
}
val subLabel = when (offer.inputType) {
is SendInputType.Lnurl -> "Tap to open"
else -> "Tap to pay"
}
Surface(
color = MaterialTheme.colorScheme.secondaryContainer, // distinct from clipboard's primaryContainer
modifier = Modifier
.fillMaxWidth()
.clickable {
vm.dismissNfcOffer()
vm.scan(offer.raw)
// scan() already emits navigateToReceive for LNURL-withdraw,
// so we only need to navigate to Send for everything else.
// The existing navigateToReceive LaunchedEffect handles BoltCard.
navController.navigate(TabItem.Send.route) {
launchSingleTop = true
}
}
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Filled.Nfc, contentDescription = null)
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text(
"$label detected via NFC",
style = MaterialTheme.typography.bodyMedium
)
Text(
subLabel,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f)
)
}
IconButton(onClick = { vm.dismissNfcOffer() }) {
Icon(Icons.Default.Close, contentDescription = "Dismiss")
}
}
}
}
}
}
}
}