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 cafe.adriel.lyricist.Lyricist import com.bitcointxoko.gudariwallet.MainActivity import com.bitcointxoko.gudariwallet.i18n.AppStrings import com.bitcointxoko.gudariwallet.LocalAppStrings 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.ui.send.SendStrings import com.bitcointxoko.gudariwallet.util.SendInputType import kotlinx.coroutines.delay // ── Tab destinations ────────────────────────────────────────────────────────── // Note: label and icon are now resolved at the call site via LocalAppStrings, // so the sealed class carries only the stable route string and icon vector. sealed class TabItem(val route: String, val icon: ImageVector) { data object Receive : TabItem("receive", Icons.Filled.QrCode) data object Send : TabItem("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, pendingPaymentHash: MutableState, pendingPaymentUri : MutableState, lyricist : Lyricist ) { val strings = LocalAppStrings.current val sendStrings = remember(strings) { SendStrings( lnurlAuthNotSupported = strings.sendVmLnurlAuthNotSupported, unrecognisedInput = strings.sendVmUnrecognisedInput, paymentFailed = strings.sendVmPaymentFailed, couldNotFetchInvoice = strings.sendVmCouldNotFetchInvoice, couldNotDecodeInvoice = strings.sendVmCouldNotDecodeInvoice, onChainNotSupported = strings.sendVmOnChainNotSupported, channelRequestNotSupported = strings.sendVmChannelRequestNotSupported, couldNotResolveAddress = strings.sendVmCouldNotResolveAddress, emptyResponse = strings.sendVmEmptyResponse, unsupportedType = strings.sendVmUnsupportedType, paymentFailedRescan = strings.sendVmPaymentFailedRescan, authFailed = strings.sendVmAuthFailed, ) } 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, sendStrings) 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) { 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 = strings.tabReceive) }, // ← localized label = { Text(strings.tabReceive) }, // ← localized 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) { popUpTo(TabItem.Send.route) { saveState = true inclusive = false } } }, modifier = Modifier.size(52.dp), shape = CircleShape ) { Icon( imageVector = Icons.Filled.QrCodeScanner, contentDescription = strings.scanContentDescription, // ← localized modifier = Modifier.size(26.dp) ) } } // Send NavigationBarItem( selected = currentRoute == TabItem.Send.route, icon = { Icon(TabItem.Send.icon, contentDescription = strings.tabSend) }, // ← localized label = { Text(strings.tabSend) }, // ← localized 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 } }, lyricist = lyricist ) } composable(TabItem.Receive.route) { ReceiveScreen( vm = vm, nfcVm = nfcVm, onNavigateToPaymentDetail = { checkingId -> navController.navigate("payment_detail/$checkingId") } ) } composable(TabItem.Send.route) { SendScreen( vm = vm, nfcVm = nfcVm, onViewDetails = { checkingId, record -> historyVm.primePayment(record) navController.navigate("payment_detail/$checkingId") { launchSingleTop = true } }, sendStrings ) } // Scanner — full screen, no bottom chrome composable(ROUTE_SCANNER) { QrScannerScreen( onScanned = { scanned -> vm.scan(scanned, sendStrings) val wentBack = navController.popBackStack(TabItem.Send.route, inclusive = false) if (!wentBack) { navController.navigate(TabItem.Send.route) { popUpTo(navController.graph.findStartDestination().id) { saveState = true } launchSingleTop = true restoreState = false } } }, 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) .fillMaxWidth() .statusBarsPadding() ) { if (clipboardOffer is ClipboardOfferState.Detected) { val offer = clipboardOffer as ClipboardOfferState.Detected val label = when (offer.inputType) { is SendInputType.Bolt11 -> strings.clipboardInvoiceDetected // ← localized is SendInputType.Lnurl -> strings.clipboardLnurlDetected // ← localized is SendInputType.LightningAddress -> strings.clipboardAddressDetected // ← localized else -> strings.clipboardPaymentDetected // ← localized } val subLabel = when (offer.inputType) { is SendInputType.Lnurl -> strings.clipboardTapToOpen // ← localized else -> strings.clipboardTapToPay // ← localized } Surface( color = MaterialTheme.colorScheme.primaryContainer, modifier = Modifier .fillMaxWidth() .clickable { vm.dismissClipboardOffer() vm.scan(offer.raw, sendStrings) 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, 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 = strings.dismissContentDescription) // ← localized } } } } } // ── NFC banner — floats over content, 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() ) { if (nfcOffer is NfcOfferState.Detected) { val offer = nfcOffer as NfcOfferState.Detected val label = when (offer.inputType) { is SendInputType.Bolt11 -> strings.nfcInvoiceDetected // ← localized is SendInputType.Lnurl -> strings.nfcLnurlDetected // ← localized is SendInputType.LightningAddress -> strings.nfcAddressDetected // ← localized else -> strings.nfcPaymentDetected // ← localized } val subLabel = when (offer.inputType) { is SendInputType.Lnurl -> strings.nfcTapToOpen // ← localized else -> strings.nfcTapToPay // ← localized } Surface( color = MaterialTheme.colorScheme.secondaryContainer, modifier = Modifier .fillMaxWidth() .clickable { vm.scan(offer.raw, sendStrings) nfcVm.dismissNfcOffer() 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, 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 = strings.dismissContentDescription) // ← localized } } } } } } } }