package com.bitcointxoko.gudariwallet.ui import android.content.ClipboardManager import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape 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 import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.bitcointxoko.gudariwallet.MainActivity import com.bitcointxoko.gudariwallet.ui.history.HistoryScreen import com.bitcointxoko.gudariwallet.ui.history.HistoryViewModel import com.bitcointxoko.gudariwallet.ui.history.HistoryViewModelFactory import com.bitcointxoko.gudariwallet.ui.history.PaymentDetailScreen import com.bitcointxoko.gudariwallet.ui.nfc.NfcViewModel import com.bitcointxoko.gudariwallet.ui.receive.ReceiveScreen import com.bitcointxoko.gudariwallet.ui.send.SendScreen import com.bitcointxoko.gudariwallet.util.SendInputType import kotlinx.coroutines.delay // ── Tab destinations ────────────────────────────────────────────────────────── sealed class TabItem(val route: String, val label: String, val icon: ImageVector) { data object Receive : TabItem("receive", "Receive", Icons.Filled.QrCode) data object Send : TabItem("send", "Send", Icons.AutoMirrored.Filled.Send) } private const val ROUTE_HOME = "home" private const val ROUTE_SCANNER = "scanner" private const val ROUTE_HISTORY = "history" private const val ROUTE_PAYMENT_DETAIL = "payment_detail/{checkingId}" @Composable fun WalletScreen( vm : WalletViewModel, nfcVm : NfcViewModel, // ← new pendingPaymentHash: MutableState, pendingPaymentUri : MutableState ) { val navController = rememberNavController() val context = LocalContext.current val historyFactory = remember { HistoryViewModelFactory( repo = vm.repo, aliasRepo = vm.aliasRepo, paymentCache = vm.paymentCache, fiatCurrency = vm.selectedCurrency, fiatSatsPerUnit = vm.fiatSatsPerUnit ) } val historyVm: HistoryViewModel = viewModel(factory = historyFactory) val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route val showBottomBar = currentRoute != ROUTE_PAYMENT_DETAIL && currentRoute != ROUTE_SCANNER && currentRoute != ROUTE_HISTORY val isFullScreenDestination = currentRoute == ROUTE_HISTORY || currentRoute == ROUTE_PAYMENT_DETAIL || currentRoute == ROUTE_SCANNER // ── Notification tap ────────────────────────────────────────────────────── val hash = pendingPaymentHash.value LaunchedEffect(hash) { if (hash != null) { navController.navigate("payment_detail/$hash") { launchSingleTop = true } pendingPaymentHash.value = null } } val activity = context as MainActivity LaunchedEffect(Unit) { activity.windowFocusEvents.collect { val cm = context.getSystemService(ClipboardManager::class.java) val text = cm?.primaryClip?.getItemAt(0) ?.coerceToText(context)?.toString() vm.checkClipboard(text) } } LaunchedEffect(Unit) { vm.navigateToReceive.collect { Log.d("WalletScreen", "LNURL-WITHDRAW [NAV RECEIVED] switching to Receive tab") navController.navigate(TabItem.Receive.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } } val intentUri = pendingPaymentUri.value LaunchedEffect(intentUri) { if (intentUri != null) { Log.d("WalletScreen", "INTENT [URI] deep link received: $intentUri") pendingPaymentUri.value = null vm.scan(intentUri) navController.navigate(TabItem.Send.route) { launchSingleTop = true } } } // ── Clipboard offer ─────────────────────────────────────────────────────── val clipboardOffer by vm.clipboardOfferState.collectAsStateWithLifecycle() LaunchedEffect(clipboardOffer) { if (clipboardOffer is ClipboardOfferState.Detected) { delay(8_000) vm.dismissClipboardOffer() } } // ── NFC offer (tag read while app is foregrounded) ──────────────────────── val nfcOffer by nfcVm.nfcOfferState.collectAsStateWithLifecycle() LaunchedEffect(nfcOffer) { if (nfcOffer is NfcOfferState.Detected) { delay(8_000) nfcVm.dismissNfcOffer() } } Scaffold( bottomBar = { if (showBottomBar) { // ── Floating island nav bar: Receive | [Scan] | Send ────────── Box( modifier = Modifier .fillMaxWidth() .navigationBarsPadding() .padding(horizontal = 24.dp) .padding(bottom = 16.dp), contentAlignment = Alignment.Center ) { Surface( shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.surfaceContainer, tonalElevation = 6.dp, shadowElevation = 6.dp, modifier = Modifier.fillMaxWidth() ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { // Receive NavigationBarItem( selected = currentRoute == TabItem.Receive.route, icon = { Icon(TabItem.Receive.icon, contentDescription = TabItem.Receive.label) }, label = { Text(TabItem.Receive.label) }, modifier = Modifier.weight(1f), onClick = { if (currentRoute != TabItem.Receive.route) { navController.navigate(TabItem.Receive.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } } ) // Scan — centre, elevated, not a nav destination Box( modifier = Modifier.weight(1f), contentAlignment = Alignment.Center ) { FilledIconButton( onClick = { navController.navigate(ROUTE_SCANNER) }, modifier = Modifier.size(52.dp), shape = CircleShape ) { Icon( imageVector = Icons.Filled.QrCodeScanner, contentDescription = "Scan", modifier = Modifier.size(26.dp) ) } } // Send NavigationBarItem( selected = currentRoute == TabItem.Send.route, icon = { Icon(TabItem.Send.icon, contentDescription = TabItem.Send.label) }, label = { Text(TabItem.Send.label) }, modifier = Modifier.weight(1f), onClick = { if (currentRoute != TabItem.Send.route) { navController.navigate(TabItem.Send.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = true } } } ) } } } } } ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() .padding(bottom = innerPadding.calculateBottomPadding()) ) { NavHost( navController = navController, startDestination = ROUTE_HOME, modifier = if (isFullScreenDestination) Modifier.fillMaxSize() else Modifier .fillMaxSize() .statusBarsPadding() ) { composable(ROUTE_HOME) { HomeTab( vm = vm, onHistoryClick = { navController.navigate(ROUTE_HISTORY) { launchSingleTop = true } } ) } composable(TabItem.Receive.route) { ReceiveScreen( vm = vm, nfcVm = nfcVm, // ← new onNavigateToPaymentDetail = { checkingId -> navController.navigate("payment_detail/$checkingId") } ) } composable(TabItem.Send.route) { SendScreen( vm = vm, onViewDetails = { checkingId -> historyVm.refresh() navController.navigate("payment_detail/$checkingId") { launchSingleTop = true } } ) } // Scanner — full screen, no bottom chrome composable(ROUTE_SCANNER) { QrScannerScreen( onScanned = { scanned -> navController.popBackStack() vm.scan(scanned) navController.navigate(TabItem.Send.route) { launchSingleTop = true } }, onDismiss = { navController.popBackStack() } ) } // History — full screen, no bottom chrome, close → home composable( ROUTE_HISTORY, enterTransition = { fadeIn(animationSpec = tween(150)) }, popExitTransition = { fadeOut(animationSpec = tween(150)) } ) { HistoryScreen( vm = historyVm, onClose = { navController.popBackStack(ROUTE_HOME, inclusive = false) }, onPaymentClick = { payment -> navController.navigate("payment_detail/${payment.checkingId}") } ) } // Payment detail — back → history (natural back stack) composable( route = ROUTE_PAYMENT_DETAIL, arguments = listOf(navArgument("checkingId") { type = NavType.StringType }) ) { backStackEntry -> val checkingId = backStackEntry.arguments?.getString("checkingId") ?: "" val historyState by historyVm.filteredState.collectAsStateWithLifecycle() val fallbackState by historyVm.state.collectAsStateWithLifecycle() val payment = remember(historyState, fallbackState, checkingId) { (historyState as? HistoryState.Success) ?.payments?.firstOrNull { it.checkingId == checkingId } ?: (fallbackState as? HistoryState.Success) ?.payments?.firstOrNull { it.checkingId == checkingId } } if (payment != null) { PaymentDetailScreen( payment = payment, vm = historyVm, onBack = { navController.popBackStack() } ) } else { LaunchedEffect(checkingId) { historyVm.refresh() } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { CircularProgressIndicator() } } } } // ── Clipboard banner — floats over content, slides in from top ──── AnimatedVisibility( visible = clipboardOffer is ClipboardOfferState.Detected, enter = slideInVertically(initialOffsetY = { -it }), exit = slideOutVertically(targetOffsetY = { -it }), modifier = Modifier .align(Alignment.TopCenter) .zIndex(2f) // above NFC banner .fillMaxWidth() .statusBarsPadding() ) { if (clipboardOffer is ClipboardOfferState.Detected) { val offer = clipboardOffer as ClipboardOfferState.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.primaryContainer, modifier = Modifier .fillMaxWidth() .clickable { vm.dismissClipboardOffer() vm.scan(offer.raw) navController.navigate(TabItem.Send.route) { launchSingleTop = true } } ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Default.ContentPaste, contentDescription = null) Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { Text( "$label detected in clipboard", style = MaterialTheme.typography.bodyMedium ) Text( subLabel, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) ) } IconButton(onClick = { vm.dismissClipboardOffer() }) { Icon(Icons.Default.Close, contentDescription = "Dismiss") } } } } } // ── NFC banner — floats over content, slides in from top ────────── // Shown when an NFC tag is tapped while the app is foregrounded. // Mirrors the clipboard banner pattern exactly. AnimatedVisibility( visible = nfcOffer is NfcOfferState.Detected, enter = slideInVertically(initialOffsetY = { -it }), exit = slideOutVertically(targetOffsetY = { -it }), modifier = Modifier .align(Alignment.TopCenter) .zIndex(1f) // below clipboard banner if both show .fillMaxWidth() .statusBarsPadding() ) { 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, modifier = Modifier .fillMaxWidth() .clickable { nfcVm.dismissNfcOffer() vm.scan(offer.raw) navController.navigate(TabItem.Send.route) { launchSingleTop = true } } ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Default.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 = { nfcVm.dismissNfcOffer() }) { Icon(Icons.Default.Close, contentDescription = "Dismiss") } } } } } } } }