From 961ca8428b523d25d1ccf0cf5a4ae4415240cafb Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Nov 2025 14:46:29 -0300 Subject: [PATCH 01/68] chore: ReceiveTab enum --- .../ui/screens/wallets/receive/ReceiveTab.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt new file mode 100644 index 000000000..000547c63 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt @@ -0,0 +1,19 @@ +package to.bitkit.ui.screens.wallets.receive + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import to.bitkit.R + +enum class ReceiveTab { + SAVINGS, // Pure onchain (BIP21 without Lightning) + AUTO, // Unified (BIP21 with Lightning or CJIT invoice) + SPENDING; // Pure Lightning (bolt11 or CJIT invoice) + + val uiText: String + @Composable + get() = when (this) { + SAVINGS -> stringResource(R.string.wallet__receive_tab_savings) + AUTO -> stringResource(R.string.wallet__receive_tab_auto) + SPENDING -> stringResource(R.string.wallet__receive_tab_spending) + } +} From 32ff9b420c3ee40f0a028187e90c1b3e6c0e985e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Nov 2025 14:48:59 -0300 Subject: [PATCH 02/68] chore: ReceiveInvoiceUtils --- .../wallets/receive/ReceiveInvoiceUtils.kt | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt new file mode 100644 index 000000000..d43e913c8 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -0,0 +1,132 @@ +package to.bitkit.ui.screens.wallets.receive + +import to.bitkit.R +import to.bitkit.models.ChannelDetails +import to.bitkit.models.NodeLifecycleState + +/** + * Strips the Lightning invoice parameter from a BIP21 URI, returning pure onchain address. + * + * Example: + * Input: "bitcoin:bc1q...?amount=0.001&lightning=lnbc..." + * Output: "bitcoin:bc1q...?amount=0.001" + * + * @param bip21 The full BIP21 URI + * @return BIP21 URI without lightning parameter + */ +fun stripLightningFromBip21(bip21: String): String { + if (bip21.isEmpty()) return bip21 + + // Remove lightning parameter and its value + // Pattern: &lightning=... or ?lightning=... + val lightningParamRegex = Regex("[?&]lightning=[^&]*") + var result = bip21.replace(lightningParamRegex, "") + + // If we removed the first param (started with ?), convert & to ? + if (result.contains("&") && !result.contains("?")) { + result = result.replaceFirst("&", "?") + } + + // Clean up trailing ? or & + result = result.trimEnd('?', '&') + + return result +} + +/** + * Returns the appropriate invoice/address for the selected tab. + * + * @param tab The selected receive tab + * @param bip21 Full BIP21 invoice (onchain + lightning) + * @param bolt11 Lightning invoice + * @param cjitInvoice CJIT invoice from Blocktank (if active) + * @param onchainAddress Pure Bitcoin address (fallback) + * @return The invoice string to display/encode in QR + */ +fun getInvoiceForTab( + tab: ReceiveTab, + bip21: String, + bolt11: String, + cjitInvoice: String?, + onchainAddress: String +): String { + return when (tab) { + ReceiveTab.SAVINGS -> { + // Pure onchain: strip lightning from BIP21 + val strippedBip21 = stripLightningFromBip21(bip21) + strippedBip21.ifEmpty { onchainAddress } + } + ReceiveTab.AUTO -> { + // Unified: prefer CJIT > full BIP21 + cjitInvoice?.takeIf { it.isNotEmpty() } + ?: bip21.ifEmpty { onchainAddress } + } + ReceiveTab.SPENDING -> { + // Lightning only: prefer CJIT > bolt11 + cjitInvoice?.takeIf { it.isNotEmpty() } + ?: bolt11.ifEmpty { onchainAddress } + } + } +} + +/** + * Returns the appropriate QR code logo resource for the selected tab. + * + * @param tab The selected receive tab + * @param hasCjit Whether a CJIT invoice is active + * @return Drawable resource ID for QR logo + */ +fun getQrLogoResource(tab: ReceiveTab, hasCjit: Boolean): Int { + return when (tab) { + ReceiveTab.SAVINGS -> R.drawable.ic_btc_circle + ReceiveTab.AUTO -> { + // Unified logo if CJIT or standard unified + if (hasCjit) R.drawable.ic_unified_circle + else R.drawable.ic_unified_circle + } + ReceiveTab.SPENDING -> R.drawable.ic_ln_circle + } +} + +/** + * Determines whether the Auto (unified) tab should be visible. + * + * Logic: + * - Node must be running + * - If geoblocked: only show if user has existing channels (grandfathered) + * - If not geoblocked: always show + * + * @param channels List of Lightning channels + * @param isGeoblocked Whether Lightning is geoblocked for this user + * @param nodeRunning Whether the Lightning node is running + * @return true if Auto tab should be visible + */ +fun shouldShowAutoTab( + channels: List, + isGeoblocked: Boolean, + nodeRunning: Boolean +): Boolean { + if (!nodeRunning) return false + + return if (isGeoblocked) { + // Geoblocked users can still use Auto if they have existing channels + channels.isNotEmpty() + } else { + // Not geoblocked: always show Auto tab + true + } +} + +/** + * Extension: Check if node lifecycle state is running. + */ +fun NodeLifecycleState.isRunning(): Boolean { + return this == NodeLifecycleState.Running +} + +/** + * Extension: Check if node lifecycle state is starting. + */ +fun NodeLifecycleState.isStarting(): Boolean { + return this == NodeLifecycleState.Starting +} From ac18980ca3f5f4291a63b087c2e388b230ba47a8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Nov 2025 14:49:13 -0300 Subject: [PATCH 03/68] chore: add temporary strings --- app/src/main/res/values/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 171d3437e..254d57044 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -964,6 +964,10 @@ Lightning invoice Optional note to payer Show QR Code + Show Details + Savings + Auto + Spending Want to receive <accent>Lightning</accent> funds? Receive on Spending Balance To set up your spending balance, a <accent>{networkFee}</accent> network fee and <accent>{serviceFee}</accent> service provider fee will be deducted. From dd3552038d563ff5fade8260a300c3df50616a60 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Nov 2025 14:50:56 -0300 Subject: [PATCH 04/68] feat: update CustomTabRowWithSpacing.kt to be generic --- .../activity/components/CustomTabRowWithSpacing.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index 4d69be23c..c8d01f179 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -25,11 +25,17 @@ import androidx.compose.ui.unit.dp import to.bitkit.ui.components.CaptionB import to.bitkit.ui.theme.Colors +interface TabItem { + val name: String + val uiText: String + @Composable get +} + @Composable -fun CustomTabRowWithSpacing( - tabs: List, +fun CustomTabRowWithSpacing( + tabs: List, currentTabIndex: Int, - onTabChange: (ActivityTab) -> Unit, + onTabChange: (T) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { From 866d3f911fd2595fd5cf6db59e3d4ed3ad2b2437 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Nov 2025 14:59:50 -0300 Subject: [PATCH 05/68] feat: update CustomTabRowWithSpacing.kt to be generic --- .../screens/wallets/activity/components/ActivityListFilter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt index 03fa3ec57..d78bbbb46 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListFilter.kt @@ -100,10 +100,10 @@ fun ActivityListFilter( } } -enum class ActivityTab { +enum class ActivityTab : TabItem { ALL, SENT, RECEIVED, OTHER; - val uiText: String + override val uiText: String @Composable get() = when (this) { ALL -> stringResource(R.string.wallet__activity_tabs__all) From 7bcffcdc8563d5fca65386af5f2431d779059228 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 27 Nov 2025 15:02:18 -0300 Subject: [PATCH 06/68] feat: implement tabs WIP --- .../wallets/receive/ReceiveInvoiceUtils.kt | 2 +- .../wallets/receive/ReceiveQrScreen.kt | 368 ++++++++++++++---- .../screens/wallets/receive/ReceiveSheet.kt | 1 + .../ui/screens/wallets/receive/ReceiveTab.kt | 5 +- 4 files changed, 293 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index d43e913c8..5856f1d01 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -1,7 +1,7 @@ package to.bitkit.ui.screens.wallets.receive +import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R -import to.bitkit.models.ChannelDetails import to.bitkit.models.NodeLifecycleState /** diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 8304d608c..207525408 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -13,8 +13,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -22,9 +20,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Switch import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -74,6 +72,7 @@ fun ReceiveQrScreen( cjitInvoice: MutableState, cjitActive: MutableState, walletState: MainUiState, + lightningState: to.bitkit.repositories.LightningState, onCjitToggle: (Boolean) -> Unit, onClickEditInvoice: () -> Unit, onClickReceiveOnSpending: () -> Unit, @@ -81,17 +80,62 @@ fun ReceiveQrScreen( ) { SetMaxBrightness() - val qrLogoImageRes by remember(walletState, cjitInvoice.value) { - val resId = when { - cjitInvoice.value?.isNotEmpty() == true -> R.drawable.ic_ln_circle - walletState.bolt11.isNotEmpty() && walletState.onchainAddress.isNotEmpty() -> R.drawable.ic_unified_circle - else -> R.drawable.ic_btc_circle + // Tab selection state + var selectedTab by remember { + mutableStateOf( + if (shouldShowAutoTab( + walletState.channels, + lightningState.shouldBlockLightningReceive, + walletState.nodeLifecycleState.isRunning() + ) + ) ReceiveTab.AUTO + else ReceiveTab.SAVINGS + ) + } + + // QR vs Details toggle state + var showDetails by remember { mutableStateOf(false) } + + // Dynamic tab visibility + val visibleTabs = remember(walletState, lightningState) { + buildList { + add(ReceiveTab.SAVINGS) // Always visible + if (shouldShowAutoTab( + walletState.channels, + lightningState.shouldBlockLightningReceive, + walletState.nodeLifecycleState.isRunning() + ) + ) { + add(ReceiveTab.AUTO) + } + if (walletState.nodeLifecycleState.isRunning()) { + add(ReceiveTab.SPENDING) + } + } + } + + // Auto-correct selected tab if it becomes hidden + LaunchedEffect(visibleTabs) { + if (selectedTab !in visibleTabs) { + selectedTab = visibleTabs.first() } - mutableIntStateOf(resId) } - val onchainAddress = walletState.onchainAddress - val uri = cjitInvoice.value ?: walletState.bip21 + // Current invoice for display + val currentInvoice = remember(selectedTab, walletState, cjitInvoice.value) { + getInvoiceForTab( + tab = selectedTab, + bip21 = walletState.bip21, + bolt11 = walletState.bolt11, + cjitInvoice = cjitInvoice.value, + onchainAddress = walletState.onchainAddress + ) + } + + // QR logo based on selected tab + val qrLogoRes = remember(selectedTab, cjitInvoice.value) { + getQrLogoResource(selectedTab, cjitInvoice.value != null) + } Column( modifier = modifier @@ -104,91 +148,240 @@ fun ReceiveQrScreen( Column( modifier = Modifier.padding(horizontal = 16.dp) ) { + Spacer(Modifier.height(16.dp)) + + // Tab row + to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing( + tabs = visibleTabs, + currentTabIndex = visibleTabs.indexOf(selectedTab), + onTabChange = { tab -> + selectedTab = tab + showDetails = false // Reset to QR when switching tabs + } + ) + Spacer(Modifier.height(24.dp)) + + // Content area (QR or Details) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.weight(1f) ) { - val pagerState = rememberPagerState(initialPage = 0) { 2 } - HorizontalPager( - state = pagerState, - pageSpacing = 20.dp, - verticalAlignment = Alignment.Top, - modifier = Modifier - .weight(1f) - .testTag("ReceiveSlider") - ) { - when (it) { - 0 -> ReceiveQrSlide( - uri = uri, - qrLogoPainter = painterResource(qrLogoImageRes), - modifier = Modifier.fillMaxWidth(), - onClickEditInvoice = onClickEditInvoice + if (showDetails) { + ReceiveDetailsView( + tab = selectedTab, + onchainAddress = walletState.onchainAddress, + bolt11 = walletState.bolt11, + cjitInvoice = cjitInvoice.value, + bip21 = walletState.bip21 + ) + } else { + ReceiveQrView( + uri = currentInvoice, + qrLogoPainter = painterResource(qrLogoRes), + onClickEditInvoice = onClickEditInvoice, + modifier = Modifier.fillMaxWidth() + ) + } + } + + Spacer(Modifier.height(24.dp)) + + // Toggle button + PrimaryButton( + text = stringResource( + if (showDetails) R.string.wallet__receive_show_qr + else R.string.wallet__receive_show_details + ), + onClick = { showDetails = !showDetails }, + fullWidth = true, + modifier = Modifier.testTag("ReceiveToggleButton") + ) + + Spacer(Modifier.height(16.dp)) + + // Node state indicator (simplified from lines 150-190) + ReceiveNodeStateIndicator( + nodeLifecycleState = walletState.nodeLifecycleState, + selectedTab = selectedTab, + cjitActive = cjitActive.value + ) + + Spacer(Modifier.height(24.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ReceiveQrView( + uri: String, + qrLogoPainter: Painter, + onClickEditInvoice: () -> Unit, + modifier: Modifier = Modifier +) { + val context = androidx.compose.ui.platform.LocalContext.current + val qrButtonTooltipState = rememberTooltipState() + val coroutineScope = rememberCoroutineScope() + var qrBitmap by remember { mutableStateOf(null) } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + ) { + QrCodeImage( + content = uri, + logoPainter = qrLogoPainter, + tipMessage = androidx.compose.ui.res.stringResource(R.string.wallet__receive_copied), + onBitmapGenerated = { bitmap -> qrBitmap = bitmap }, + testTag = "QRCode", + modifier = Modifier.weight(1f, fill = false) + ) + + Spacer(modifier = Modifier.height(16.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Top, + ) { + PrimaryButton( + text = androidx.compose.ui.res.stringResource(R.string.common__edit), + size = ButtonSize.Small, + onClick = onClickEditInvoice, + fullWidth = false, + color = Colors.White10, + icon = { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.ic_pencil_simple), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(18.dp) + ) + }, + modifier = Modifier.testTag("SpecifyInvoiceButton") + ) + Tooltip( + text = androidx.compose.ui.res.stringResource(R.string.wallet__receive_copied), + tooltipState = qrButtonTooltipState + ) { + PrimaryButton( + text = androidx.compose.ui.res.stringResource(R.string.common__copy), + size = ButtonSize.Small, + onClick = { + context.setClipboardText(uri) + coroutineScope.launch { qrButtonTooltipState.show() } + }, + fullWidth = false, + color = Colors.White10, + icon = { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.ic_copy), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(18.dp) ) + }, + modifier = Modifier.testTag("ReceiveCopyQR") + ) + } + PrimaryButton( + text = androidx.compose.ui.res.stringResource(R.string.common__share), + size = ButtonSize.Small, + onClick = { + qrBitmap?.let { bitmap -> + shareQrCode(context, bitmap, uri) + } ?: shareText(context, uri) + }, + fullWidth = false, + color = Colors.White10, + icon = { + Icon( + painter = androidx.compose.ui.res.painterResource(R.drawable.ic_share), + contentDescription = null, + tint = Colors.Brand, + modifier = Modifier.size(18.dp) + ) + }, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + } +} - 1 -> CopyValuesSlide( - onchainAddress = onchainAddress, - bolt11 = walletState.bolt11, - cjitInvoice = cjitInvoice.value, - receiveOnSpendingBalance = walletState.receiveOnSpendingBalance +@Composable +private fun ReceiveDetailsView( + tab: ReceiveTab, + onchainAddress: String, + bolt11: String, + cjitInvoice: String?, + bip21: String, + modifier: Modifier = Modifier +) { + Card( + colors = CardDefaults.cardColors(containerColor = Colors.White10), + shape = AppShapes.small, + modifier = modifier + ) { + Column { + when (tab) { + ReceiveTab.SAVINGS -> { + if (onchainAddress.isNotEmpty()) { + CopyAddressCard( + title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_bitcoin_invoice), + address = onchainAddress, + type = CopyAddressType.ONCHAIN, + testTag = "ReceiveOnchainAddress", ) } } - @Suppress("DEPRECATION") - HorizontalPagerIndicator( - pagerState = pagerState, - pageCount = pagerState.pageCount, - indicatorWidth = 8.dp, - spacing = 8.dp, - activeColor = Colors.White, - inactiveColor = Colors.White32, - modifier = Modifier - .align(Alignment.CenterHorizontally) - ) - } - Spacer(modifier = Modifier.height(24.dp)) - AnimatedVisibility(walletState.nodeLifecycleState.isRunning() && walletState.channels.isEmpty()) { - ReceiveLightningFunds( - cjitInvoice = cjitInvoice, - cjitActive = cjitActive, - onCjitToggle = onCjitToggle, - ) - } - AnimatedVisibility(walletState.nodeLifecycleState.isRunning() && walletState.channels.isNotEmpty()) { - Column { - AnimatedVisibility(!walletState.receiveOnSpendingBalance) { - Headline( - text = stringResource( - R.string.wallet__receive_text_lnfunds - ).withAccent(accentColor = Colors.Purple) + ReceiveTab.AUTO -> { + // Show both onchain AND lightning if available + if (onchainAddress.isNotEmpty()) { + CopyAddressCard( + title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_bitcoin_invoice), + address = onchainAddress, + type = CopyAddressType.ONCHAIN, + testTag = "ReceiveOnchainAddress", ) } - Row(verticalAlignment = Alignment.CenterVertically) { - BodyM(text = stringResource(R.string.wallet__receive_spending)) - Spacer(modifier = Modifier.weight(1f)) - AnimatedVisibility(!walletState.receiveOnSpendingBalance) { - Icon( - painter = painterResource(R.drawable.empty_state_arrow_horizontal), - contentDescription = null, - tint = Colors.White64, - modifier = Modifier - .rotate(17.33f) - .padding(start = 7.65.dp, end = 13.19.dp) - ) - } - Switch( - checked = walletState.receiveOnSpendingBalance, - onCheckedChange = { onClickReceiveOnSpending() }, - colors = AppSwitchDefaults.colorsPurple, - modifier = Modifier.testTag("ReceiveInstantlySwitch") + if (cjitInvoice != null || bolt11.isNotEmpty()) { + CopyAddressCard( + title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_lightning_invoice), + address = cjitInvoice ?: bolt11, + type = CopyAddressType.LIGHTNING, + testTag = "ReceiveLightningAddress", + ) + } + } + ReceiveTab.SPENDING -> { + if (cjitInvoice != null || bolt11.isNotEmpty()) { + CopyAddressCard( + title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_lightning_invoice), + address = cjitInvoice ?: bolt11, + type = CopyAddressType.LIGHTNING, + testTag = "ReceiveLightningAddress", ) } } } - AnimatedVisibility(walletState.nodeLifecycleState.isStarting()) { - BodyM(text = stringResource(R.string.wallet__receive_ldk_init)) - } - Spacer(modifier = Modifier.height(24.dp)) + } + } +} + +@Composable +private fun ReceiveNodeStateIndicator( + nodeLifecycleState: to.bitkit.models.NodeLifecycleState, + selectedTab: ReceiveTab, + cjitActive: Boolean +) { + when { + nodeLifecycleState.isStarting() -> { + BodyM(text = androidx.compose.ui.res.stringResource(R.string.wallet__receive_ldk_init)) + } + selectedTab == ReceiveTab.SPENDING && cjitActive -> { + BodyS( + text = "CJIT Active", + color = Colors.Purple + ) } } } @@ -453,6 +646,11 @@ private fun Preview() { walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, ), + lightningState = to.bitkit.repositories.LightningState( + nodeLifecycleState = NodeLifecycleState.Running, + shouldBlockLightningReceive = false, + isGeoBlocked = false + ), onCjitToggle = {}, onClickEditInvoice = {}, onClickReceiveOnSpending = {}, @@ -473,6 +671,11 @@ private fun PreviewNodeNotReady() { walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Starting, ), + lightningState = to.bitkit.repositories.LightningState( + nodeLifecycleState = NodeLifecycleState.Starting, + shouldBlockLightningReceive = false, + isGeoBlocked = false + ), onCjitToggle = {}, onClickEditInvoice = {}, onClickReceiveOnSpending = {}, @@ -493,6 +696,11 @@ private fun PreviewSmall() { walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, ), + lightningState = to.bitkit.repositories.LightningState( + nodeLifecycleState = NodeLifecycleState.Running, + shouldBlockLightningReceive = false, + isGeoBlocked = false + ), onCjitToggle = {}, onClickEditInvoice = {}, onClickReceiveOnSpending = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index c2dccc46c..5a981b3c2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -78,6 +78,7 @@ fun ReceiveSheet( cjitInvoice = cjitInvoice, cjitActive = showCreateCjit, walletState = walletState, + lightningState = lightningState, onCjitToggle = { isOn -> when { isOn && lightningState.shouldBlockLightningReceive -> { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt index 000547c63..087e30b13 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt @@ -3,13 +3,14 @@ package to.bitkit.ui.screens.wallets.receive import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import to.bitkit.R +import to.bitkit.ui.screens.wallets.activity.components.TabItem -enum class ReceiveTab { +enum class ReceiveTab : TabItem { SAVINGS, // Pure onchain (BIP21 without Lightning) AUTO, // Unified (BIP21 with Lightning or CJIT invoice) SPENDING; // Pure Lightning (bolt11 or CJIT invoice) - val uiText: String + override val uiText: String @Composable get() = when (this) { SAVINGS -> stringResource(R.string.wallet__receive_tab_savings) From f1033ad989ae5a5a2105440f7cf8cf7a7c4e884b Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 27 Nov 2025 21:04:06 -0300 Subject: [PATCH 07/68] feat: implement tab WIP --- .../wallets/receive/ReceiveInvoiceUtils.kt | 29 -------------- .../wallets/receive/ReceiveQrScreen.kt | 38 ++++++------------- 2 files changed, 12 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index 5856f1d01..090068e32 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -88,35 +88,6 @@ fun getQrLogoResource(tab: ReceiveTab, hasCjit: Boolean): Int { } } -/** - * Determines whether the Auto (unified) tab should be visible. - * - * Logic: - * - Node must be running - * - If geoblocked: only show if user has existing channels (grandfathered) - * - If not geoblocked: always show - * - * @param channels List of Lightning channels - * @param isGeoblocked Whether Lightning is geoblocked for this user - * @param nodeRunning Whether the Lightning node is running - * @return true if Auto tab should be visible - */ -fun shouldShowAutoTab( - channels: List, - isGeoblocked: Boolean, - nodeRunning: Boolean -): Boolean { - if (!nodeRunning) return false - - return if (isGeoblocked) { - // Geoblocked users can still use Auto if they have existing channels - channels.isNotEmpty() - } else { - // Not geoblocked: always show Auto tab - true - } -} - /** * Extension: Check if node lifecycle state is running. */ diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 207525408..ab5d6443a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.google.accompanist.pager.HorizontalPagerIndicator import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ext.setClipboardText @@ -83,13 +82,11 @@ fun ReceiveQrScreen( // Tab selection state var selectedTab by remember { mutableStateOf( - if (shouldShowAutoTab( - walletState.channels, - lightningState.shouldBlockLightningReceive, - walletState.nodeLifecycleState.isRunning() - ) - ) ReceiveTab.AUTO - else ReceiveTab.SAVINGS + if (walletState.channels.isNotEmpty()) { + ReceiveTab.AUTO + } else { + ReceiveTab.SAVINGS + } ) } @@ -100,17 +97,10 @@ fun ReceiveQrScreen( val visibleTabs = remember(walletState, lightningState) { buildList { add(ReceiveTab.SAVINGS) // Always visible - if (shouldShowAutoTab( - walletState.channels, - lightningState.shouldBlockLightningReceive, - walletState.nodeLifecycleState.isRunning() - ) - ) { + if (walletState.channels.isNotEmpty()) { add(ReceiveTab.AUTO) } - if (walletState.nodeLifecycleState.isRunning()) { - add(ReceiveTab.SPENDING) - } + add(ReceiveTab.SPENDING) } } @@ -200,9 +190,7 @@ fun ReceiveQrScreen( Spacer(Modifier.height(16.dp)) - // Node state indicator (simplified from lines 150-190) ReceiveNodeStateIndicator( - nodeLifecycleState = walletState.nodeLifecycleState, selectedTab = selectedTab, cjitActive = cjitActive.value ) @@ -218,7 +206,7 @@ private fun ReceiveQrView( uri: String, qrLogoPainter: Painter, onClickEditInvoice: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { val context = androidx.compose.ui.platform.LocalContext.current val qrButtonTooltipState = rememberTooltipState() @@ -314,7 +302,7 @@ private fun ReceiveDetailsView( bolt11: String, cjitInvoice: String?, bip21: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Card( colors = CardDefaults.cardColors(containerColor = Colors.White10), @@ -333,6 +321,7 @@ private fun ReceiveDetailsView( ) } } + ReceiveTab.AUTO -> { // Show both onchain AND lightning if available if (onchainAddress.isNotEmpty()) { @@ -352,6 +341,7 @@ private fun ReceiveDetailsView( ) } } + ReceiveTab.SPENDING -> { if (cjitInvoice != null || bolt11.isNotEmpty()) { CopyAddressCard( @@ -369,14 +359,10 @@ private fun ReceiveDetailsView( @Composable private fun ReceiveNodeStateIndicator( - nodeLifecycleState: to.bitkit.models.NodeLifecycleState, selectedTab: ReceiveTab, - cjitActive: Boolean + cjitActive: Boolean, ) { when { - nodeLifecycleState.isStarting() -> { - BodyM(text = androidx.compose.ui.res.stringResource(R.string.wallet__receive_ldk_init)) - } selectedTab == ReceiveTab.SPENDING && cjitActive -> { BodyS( text = "CJIT Active", From a9d153eb1b57e5aadd866a6e19859c1431d2dcbc Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 27 Nov 2025 21:08:08 -0300 Subject: [PATCH 08/68] chore: cleanup code --- .../screens/wallets/receive/ReceiveQrScreen.kt | 11 ++--------- .../ui/screens/wallets/receive/ReceiveSheet.kt | 18 ------------------ 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index ab5d6443a..e68f4789c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -54,6 +54,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.components.Tooltip import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing import to.bitkit.ui.shared.effects.SetMaxBrightness import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground @@ -72,9 +73,7 @@ fun ReceiveQrScreen( cjitActive: MutableState, walletState: MainUiState, lightningState: to.bitkit.repositories.LightningState, - onCjitToggle: (Boolean) -> Unit, onClickEditInvoice: () -> Unit, - onClickReceiveOnSpending: () -> Unit, modifier: Modifier = Modifier, ) { SetMaxBrightness() @@ -141,7 +140,7 @@ fun ReceiveQrScreen( Spacer(Modifier.height(16.dp)) // Tab row - to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing( + CustomTabRowWithSpacing( tabs = visibleTabs, currentTabIndex = visibleTabs.indexOf(selectedTab), onTabChange = { tab -> @@ -637,9 +636,7 @@ private fun Preview() { shouldBlockLightningReceive = false, isGeoBlocked = false ), - onCjitToggle = {}, onClickEditInvoice = {}, - onClickReceiveOnSpending = {}, modifier = Modifier.sheetHeight(), ) } @@ -662,9 +659,7 @@ private fun PreviewNodeNotReady() { shouldBlockLightningReceive = false, isGeoBlocked = false ), - onCjitToggle = {}, onClickEditInvoice = {}, - onClickReceiveOnSpending = {}, modifier = Modifier.sheetHeight(), ) } @@ -687,9 +682,7 @@ private fun PreviewSmall() { shouldBlockLightningReceive = false, isGeoBlocked = false ), - onCjitToggle = {}, onClickEditInvoice = {}, - onClickReceiveOnSpending = {}, modifier = Modifier.sheetHeight(), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index 5a981b3c2..ec5ddc934 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -79,25 +79,7 @@ fun ReceiveSheet( cjitActive = showCreateCjit, walletState = walletState, lightningState = lightningState, - onCjitToggle = { isOn -> - when { - isOn && lightningState.shouldBlockLightningReceive -> { - navController.navigate(ReceiveRoute.GeoBlock) - } - - !isOn -> { - showCreateCjit.value = false - cjitInvoice.value = null - } - - isOn && cjitInvoice.value == null -> { - showCreateCjit.value = true - navController.navigate(ReceiveRoute.Amount) - } - } - }, onClickEditInvoice = { navController.navigate(ReceiveRoute.EditInvoice) }, - onClickReceiveOnSpending = { wallet.toggleReceiveOnSpending() } ) } composableWithDefaultTransitions { From 771b979e2993e55f4d0d12aa241734e65e51e568 Mon Sep 17 00:00:00 2001 From: Joao Victor Sena Date: Thu, 27 Nov 2025 21:16:33 -0300 Subject: [PATCH 09/68] feat: tab color --- .../wallets/activity/components/CustomTabRowWithSpacing.kt | 4 +++- .../bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index c8d01f179..9d6fe678d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -35,6 +36,7 @@ interface TabItem { fun CustomTabRowWithSpacing( tabs: List, currentTabIndex: Int, + selectedColor: Color = Colors.Brand, onTabChange: (T) -> Unit, modifier: Modifier = Modifier, ) { @@ -73,7 +75,7 @@ fun CustomTabRowWithSpacing( ) val animatedColor by animateColorAsState( - targetValue = if (isSelected) Colors.Brand else Colors.White, + targetValue = if (isSelected) selectedColor else Colors.White, animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), label = "indicatorColor" ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index e68f4789c..ce125569e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -143,6 +143,11 @@ fun ReceiveQrScreen( CustomTabRowWithSpacing( tabs = visibleTabs, currentTabIndex = visibleTabs.indexOf(selectedTab), + selectedColor = when (selectedTab) { + ReceiveTab.SAVINGS -> Colors.Brand + ReceiveTab.AUTO -> Colors.White + ReceiveTab.SPENDING -> Colors.Purple + }, onTabChange = { tab -> selectedTab = tab showDetails = false // Reset to QR when switching tabs @@ -207,7 +212,7 @@ private fun ReceiveQrView( onClickEditInvoice: () -> Unit, modifier: Modifier = Modifier, ) { - val context = androidx.compose.ui.platform.LocalContext.current + val context = LocalContext.current val qrButtonTooltipState = rememberTooltipState() val coroutineScope = rememberCoroutineScope() var qrBitmap by remember { mutableStateOf(null) } From 1ee8d216eccef4b528694372e87c5d7c43f9a2a7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 06:58:10 -0300 Subject: [PATCH 10/68] chore: cleanup code --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index ce125569e..e8166b51e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -243,7 +243,7 @@ private fun ReceiveQrView( color = Colors.White10, icon = { Icon( - painter = androidx.compose.ui.res.painterResource(R.drawable.ic_pencil_simple), + painter = painterResource(R.drawable.ic_pencil_simple), contentDescription = null, tint = Colors.Brand, modifier = Modifier.size(18.dp) @@ -266,7 +266,7 @@ private fun ReceiveQrView( color = Colors.White10, icon = { Icon( - painter = androidx.compose.ui.res.painterResource(R.drawable.ic_copy), + painter = painterResource(R.drawable.ic_copy), contentDescription = null, tint = Colors.Brand, modifier = Modifier.size(18.dp) @@ -287,7 +287,7 @@ private fun ReceiveQrView( color = Colors.White10, icon = { Icon( - painter = androidx.compose.ui.res.painterResource(R.drawable.ic_share), + painter = painterResource(R.drawable.ic_share), contentDescription = null, tint = Colors.Brand, modifier = Modifier.size(18.dp) From dd0ef180c87fb94dd5cc4cee2e1ff80841b2b763 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 07:12:55 -0300 Subject: [PATCH 11/68] feat: copy address visibility --- .../wallets/receive/ReceiveQrScreen.kt | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index e8166b51e..e83d4184a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -193,13 +193,6 @@ fun ReceiveQrScreen( ) Spacer(Modifier.height(16.dp)) - - ReceiveNodeStateIndicator( - selectedTab = selectedTab, - cjitActive = cjitActive.value - ) - - Spacer(Modifier.height(24.dp)) } } } @@ -508,11 +501,11 @@ private fun ReceiveQrSlide( } @Composable -private fun CopyValuesSlide( +private fun CopyValuesBox( onchainAddress: String, bolt11: String, cjitInvoice: String?, - receiveOnSpendingBalance: Boolean, + selectedTab: ReceiveTab, ) { Card( colors = CardDefaults.cardColors(containerColor = Colors.White10), @@ -527,20 +520,23 @@ private fun CopyValuesSlide( testTag = "ReceiveOnchainAddress", ) } - if (bolt11.isNotEmpty() && receiveOnSpendingBalance) { - CopyAddressCard( - title = stringResource(R.string.wallet__receive_lightning_invoice), - address = bolt11, - type = CopyAddressType.LIGHTNING, - testTag = "ReceiveLightningAddress", - ) - } else if (cjitInvoice != null) { - CopyAddressCard( - title = stringResource(R.string.wallet__receive_lightning_invoice), - address = cjitInvoice, - type = CopyAddressType.LIGHTNING, - testTag = "ReceiveLightningAddress", - ) + + if (selectedTab == ReceiveTab.AUTO || selectedTab == ReceiveTab.SPENDING) { + if (bolt11.isNotEmpty()) { + CopyAddressCard( + title = stringResource(R.string.wallet__receive_lightning_invoice), + address = bolt11, + type = CopyAddressType.LIGHTNING, + testTag = "ReceiveLightningAddress", + ) + } else if (cjitInvoice != null) { + CopyAddressCard( + title = stringResource(R.string.wallet__receive_lightning_invoice), + address = cjitInvoice, + type = CopyAddressType.LIGHTNING, + testTag = "ReceiveLightningAddress", + ) + } } } } @@ -704,11 +700,11 @@ private fun PreviewSlide2() { .gradientBackground() .padding(16.dp) ) { - CopyValuesSlide( + CopyValuesBox( onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", cjitInvoice = null, - true + selectedTab = ReceiveTab.AUTO ) } } From 804dd1e40c1573291e73eab69fab20440a1220e3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 07:17:26 -0300 Subject: [PATCH 12/68] feat: buttons tab --- .../wallets/receive/ReceiveQrScreen.kt | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index e83d4184a..89e94a95f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -174,6 +174,7 @@ fun ReceiveQrScreen( uri = currentInvoice, qrLogoPainter = painterResource(qrLogoRes), onClickEditInvoice = onClickEditInvoice, + tab = selectedTab, modifier = Modifier.fillMaxWidth() ) } @@ -204,6 +205,7 @@ private fun ReceiveQrView( qrLogoPainter: Painter, onClickEditInvoice: () -> Unit, modifier: Modifier = Modifier, + tab: ReceiveTab, ) { val context = LocalContext.current val qrButtonTooltipState = rememberTooltipState() @@ -238,7 +240,11 @@ private fun ReceiveQrView( Icon( painter = painterResource(R.drawable.ic_pencil_simple), contentDescription = null, - tint = Colors.Brand, + tint = when (tab) { + ReceiveTab.SAVINGS -> Colors.Brand + ReceiveTab.AUTO -> Colors.Brand + ReceiveTab.SPENDING -> Colors.Purple + }, modifier = Modifier.size(18.dp) ) }, @@ -261,7 +267,11 @@ private fun ReceiveQrView( Icon( painter = painterResource(R.drawable.ic_copy), contentDescription = null, - tint = Colors.Brand, + tint = when (tab) { + ReceiveTab.SAVINGS -> Colors.Brand + ReceiveTab.AUTO -> Colors.Brand + ReceiveTab.SPENDING -> Colors.Purple + }, modifier = Modifier.size(18.dp) ) }, @@ -282,7 +292,11 @@ private fun ReceiveQrView( Icon( painter = painterResource(R.drawable.ic_share), contentDescription = null, - tint = Colors.Brand, + tint = when (tab) { + ReceiveTab.SAVINGS -> Colors.Brand + ReceiveTab.AUTO -> Colors.Brand + ReceiveTab.SPENDING -> Colors.Purple + }, modifier = Modifier.size(18.dp) ) }, From f93e271973b9cc563bd38cf81b6c9b7953bb2eea Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 07:26:05 -0300 Subject: [PATCH 13/68] feat: preview --- .../wallets/receive/ReceiveQrScreen.kt | 109 +++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 89e94a95f..70eb88d74 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.ext.truncate @@ -75,13 +76,14 @@ fun ReceiveQrScreen( lightningState: to.bitkit.repositories.LightningState, onClickEditInvoice: () -> Unit, modifier: Modifier = Modifier, + initialTab: ReceiveTab? = null, ) { SetMaxBrightness() // Tab selection state var selectedTab by remember { mutableStateOf( - if (walletState.channels.isNotEmpty()) { + initialTab ?: if (walletState.channels.isNotEmpty()) { ReceiveTab.AUTO } else { ReceiveTab.SAVINGS @@ -635,9 +637,108 @@ private fun CopyAddressCard( } } -@Preview(showSystemUi = true) +@Suppress("SpellCheckingInspection") +@Preview(showSystemUi = true, name = "Savings Mode") +@Composable +private fun PreviewSavingsMode() { + AppThemeSurface { + BottomSheetPreview { + ReceiveQrScreen( + cjitInvoice = remember { mutableStateOf(null) }, + cjitActive = remember { mutableStateOf(false) }, + walletState = MainUiState( + nodeLifecycleState = NodeLifecycleState.Running, + onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", + channels = emptyList() + ), + lightningState = to.bitkit.repositories.LightningState( + nodeLifecycleState = NodeLifecycleState.Running, + shouldBlockLightningReceive = false, + isGeoBlocked = false + ), + onClickEditInvoice = {}, + modifier = Modifier.sheetHeight(), + initialTab = ReceiveTab.SAVINGS + ) + } + } +} + +@Suppress("SpellCheckingInspection") +@Preview(showSystemUi = true, name = "Auto Mode") +@Composable +private fun PreviewAutoMode() { + // Mock channel for preview (AUTO tab requires non-empty channels list) + val mockChannel = ChannelDetails( + channelId = "0".repeat(64), + counterpartyNodeId = "0".repeat(66), + fundingTxo = null, + shortChannelId = null, + outboundScidAlias = null, + inboundScidAlias = null, + channelValueSats = 1000000uL, + unspendablePunishmentReserve = null, + userChannelId = "0".repeat(32), + feerateSatPer1000Weight = 1000u, + outboundCapacityMsat = 500000000uL, + inboundCapacityMsat = 500000000uL, + confirmationsRequired = null, + confirmations = null, + isOutbound = true, + isChannelReady = true, + isUsable = true, + isAnnounced = false, + cltvExpiryDelta = null, + counterpartyUnspendablePunishmentReserve = 0uL, + counterpartyOutboundHtlcMinimumMsat = null, + counterpartyOutboundHtlcMaximumMsat = null, + counterpartyForwardingInfoFeeBaseMsat = null, + counterpartyForwardingInfoFeeProportionalMillionths = null, + counterpartyForwardingInfoCltvExpiryDelta = null, + nextOutboundHtlcLimitMsat = 0uL, + nextOutboundHtlcMinimumMsat = 0uL, + forceCloseSpendDelay = null, + inboundHtlcMinimumMsat = 0uL, + inboundHtlcMaximumMsat = null, + config = org.lightningdevkit.ldknode.ChannelConfig( + forwardingFeeProportionalMillionths = 0u, + forwardingFeeBaseMsat = 0u, + cltvExpiryDelta = 0u, + maxDustHtlcExposure = org.lightningdevkit.ldknode.MaxDustHtlcExposure.FeeRateMultiplier(0uL), + forceCloseAvoidanceMaxFeeSatoshis = 0uL, + acceptUnderpayingHtlcs = false + ) + ) + + AppThemeSurface { + BottomSheetPreview { + ReceiveQrScreen( + cjitInvoice = remember { mutableStateOf(null) }, + cjitActive = remember { mutableStateOf(false) }, + walletState = MainUiState( + nodeLifecycleState = NodeLifecycleState.Running, + onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", + bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79vhx9n2ps8q6tcdehhxapqd9h8vmmfvdjjqen0wgsyqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxq", + bip21 = "bitcoin:bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l?lightning=lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", + channels = listOf(mockChannel) + ), + lightningState = to.bitkit.repositories.LightningState( + nodeLifecycleState = NodeLifecycleState.Running, + shouldBlockLightningReceive = false, + isGeoBlocked = false + ), + onClickEditInvoice = {}, + modifier = Modifier.sheetHeight(), + initialTab = ReceiveTab.AUTO + ) + } + } +} + +@Suppress("SpellCheckingInspection") +@Preview(showSystemUi = true, name = "Spending Mode") @Composable -private fun Preview() { +private fun PreviewSpendingMode() { AppThemeSurface { BottomSheetPreview { ReceiveQrScreen( @@ -645,6 +746,7 @@ private fun Preview() { cjitActive = remember { mutableStateOf(false) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, + bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79vhx9n2ps8q6tcdehhxapqd9h8vmmfvdjjqen0wgsyqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxq" ), lightningState = to.bitkit.repositories.LightningState( nodeLifecycleState = NodeLifecycleState.Running, @@ -653,6 +755,7 @@ private fun Preview() { ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), + initialTab = ReceiveTab.SPENDING ) } } From e65fa77c850ddf6c2d81367cfa59724ff363e91e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 07:41:56 -0300 Subject: [PATCH 14/68] feat: detail view background --- .../wallets/receive/ReceiveQrScreen.kt | 211 +----------------- 1 file changed, 9 insertions(+), 202 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 70eb88d74..6badc4f9b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -1,7 +1,6 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap -import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -17,7 +16,6 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.Switch import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -29,7 +27,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.keepScreenOn import androidx.compose.ui.platform.LocalContext @@ -45,12 +42,10 @@ import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.ext.truncate import to.bitkit.models.NodeLifecycleState -import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up -import to.bitkit.ui.components.Headline import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.components.Tooltip @@ -62,10 +57,8 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.shareQrCode import to.bitkit.ui.shared.util.shareText import to.bitkit.ui.theme.AppShapes -import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.MainUiState @Composable @@ -169,7 +162,7 @@ fun ReceiveQrScreen( onchainAddress = walletState.onchainAddress, bolt11 = walletState.bolt11, cjitInvoice = cjitInvoice.value, - bip21 = walletState.bip21 + modifier = Modifier.weight(1f) ) } else { ReceiveQrView( @@ -314,11 +307,10 @@ private fun ReceiveDetailsView( onchainAddress: String, bolt11: String, cjitInvoice: String?, - bip21: String, modifier: Modifier = Modifier, ) { Card( - colors = CardDefaults.cardColors(containerColor = Colors.White10), + colors = CardDefaults.cardColors(containerColor = Colors.Black), shape = AppShapes.small, modifier = modifier ) { @@ -370,194 +362,6 @@ private fun ReceiveDetailsView( } } -@Composable -private fun ReceiveNodeStateIndicator( - selectedTab: ReceiveTab, - cjitActive: Boolean, -) { - when { - selectedTab == ReceiveTab.SPENDING && cjitActive -> { - BodyS( - text = "CJIT Active", - color = Colors.Purple - ) - } - } -} - -@Composable -private fun ReceiveLightningFunds( - cjitInvoice: MutableState, - cjitActive: MutableState, - onCjitToggle: (Boolean) -> Unit, -) { - Column { - AnimatedVisibility(!cjitActive.value && cjitInvoice.value == null) { - Headline( - text = stringResource(R.string.wallet__receive_text_lnfunds).withAccent(accentColor = Colors.Purple) - ) - } - Row(verticalAlignment = Alignment.CenterVertically) { - BodyM(text = stringResource(R.string.wallet__receive_spending)) - Spacer(modifier = Modifier.weight(1f)) - AnimatedVisibility(!cjitActive.value && cjitInvoice.value == null) { - Icon( - painter = painterResource(R.drawable.empty_state_arrow_horizontal), - contentDescription = null, - tint = Colors.White64, - modifier = Modifier - .rotate(17.33f) - .padding(start = 7.65.dp, end = 13.19.dp) - ) - } - Switch( - checked = cjitActive.value, - onCheckedChange = onCjitToggle, - colors = AppSwitchDefaults.colorsPurple, - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ReceiveQrSlide( - uri: String, - qrLogoPainter: Painter, - modifier: Modifier, - onClickEditInvoice: () -> Unit, -) { - val context = LocalContext.current - - val qrButtonTooltipState = rememberTooltipState() - val coroutineScope = rememberCoroutineScope() - - var qrBitmap by remember { mutableStateOf(null) } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - ) { - QrCodeImage( - content = uri, - logoPainter = qrLogoPainter, - tipMessage = stringResource(R.string.wallet__receive_copied), - onBitmapGenerated = { bitmap -> qrBitmap = bitmap }, - testTag = "QRCode", - modifier = Modifier.weight(1f, fill = false) - ) - - Spacer(modifier = Modifier.height(16.dp)) - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.Top, - ) { - PrimaryButton( - text = stringResource(R.string.common__edit), - size = ButtonSize.Small, - onClick = onClickEditInvoice, - fullWidth = false, - color = Colors.White10, - icon = { - Icon( - painter = painterResource(R.drawable.ic_pencil_simple), - contentDescription = null, - tint = Colors.Brand, - modifier = Modifier.size(18.dp) - ) - }, - modifier = Modifier.testTag("SpecifyInvoiceButton") - ) - Tooltip( - text = stringResource(R.string.wallet__receive_copied), - tooltipState = qrButtonTooltipState - ) { - PrimaryButton( - text = stringResource(R.string.common__copy), - size = ButtonSize.Small, - onClick = { - context.setClipboardText(uri) - coroutineScope.launch { qrButtonTooltipState.show() } - }, - fullWidth = false, - color = Colors.White10, - icon = { - Icon( - painter = painterResource(R.drawable.ic_copy), - contentDescription = null, - tint = Colors.Brand, - modifier = Modifier.size(18.dp) - ) - }, - modifier = Modifier.testTag("ReceiveCopyQR") - ) - } - PrimaryButton( - text = stringResource(R.string.common__share), - size = ButtonSize.Small, - onClick = { - qrBitmap?.let { bitmap -> - shareQrCode(context, bitmap, uri) - } ?: shareText(context, uri) - }, - fullWidth = false, - color = Colors.White10, - icon = { - Icon( - painter = painterResource(R.drawable.ic_share), - contentDescription = null, - tint = Colors.Brand, - modifier = Modifier.size(18.dp) - ) - }, - ) - } - Spacer(modifier = Modifier.height(16.dp)) - } -} - -@Composable -private fun CopyValuesBox( - onchainAddress: String, - bolt11: String, - cjitInvoice: String?, - selectedTab: ReceiveTab, -) { - Card( - colors = CardDefaults.cardColors(containerColor = Colors.White10), - shape = AppShapes.small, - ) { - Column { - if (onchainAddress.isNotEmpty() && cjitInvoice == null) { - CopyAddressCard( - title = stringResource(R.string.wallet__receive_bitcoin_invoice), - address = onchainAddress, - type = CopyAddressType.ONCHAIN, - testTag = "ReceiveOnchainAddress", - ) - } - - if (selectedTab == ReceiveTab.AUTO || selectedTab == ReceiveTab.SPENDING) { - if (bolt11.isNotEmpty()) { - CopyAddressCard( - title = stringResource(R.string.wallet__receive_lightning_invoice), - address = bolt11, - type = CopyAddressType.LIGHTNING, - testTag = "ReceiveLightningAddress", - ) - } else if (cjitInvoice != null) { - CopyAddressCard( - title = stringResource(R.string.wallet__receive_lightning_invoice), - address = cjitInvoice, - type = CopyAddressType.LIGHTNING, - testTag = "ReceiveLightningAddress", - ) - } - } - } - } -} - enum class CopyAddressType { ONCHAIN, LIGHTNING } @OptIn(ExperimentalMaterial3Api::class) @@ -807,21 +611,24 @@ private fun PreviewSmall() { } } + @Suppress("SpellCheckingInspection") -@Preview(showBackground = true) +@Preview(showSystemUi = true, name = "Auto Mode") @Composable -private fun PreviewSlide2() { +private fun PreviewDetailsMode() { AppThemeSurface { Column( modifier = Modifier .gradientBackground() + .fillMaxSize() .padding(16.dp) ) { - CopyValuesBox( + ReceiveDetailsView( + tab = ReceiveTab.AUTO, onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", cjitInvoice = null, - selectedTab = ReceiveTab.AUTO + modifier = Modifier.weight(1f) ) } } From ec2469c3b3c01a6dafd97e37f48c0258f33ba9ba Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 08:17:57 -0300 Subject: [PATCH 15/68] feat: qr placeholder --- .../to/bitkit/ui/components/QrCodeImage.kt | 47 +++++++++-------- app/src/main/res/drawable/qr_placeholder.xml | 51 +++++++++++++++++++ 2 files changed, 78 insertions(+), 20 deletions(-) create mode 100644 app/src/main/res/drawable/qr_placeholder.xml diff --git a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt index e3e8af358..2d124cfb0 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -112,31 +113,37 @@ fun QrCodeImage( } else { imageComposable() } - } - } - - logoPainter?.let { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(68.dp) - .background(Color.White, shape = CircleShape) - .align(Alignment.Center) - ) { + } else { Image( - painter = it, - contentDescription = null, - modifier = Modifier.size(50.dp) + painter = painterResource(R.drawable.qr_placeholder), + contentDescription = content, + contentScale = ContentScale.Inside, ) } } - if (bitmap == null) { - CircularProgressIndicator( - color = Colors.Black, - strokeWidth = 4.dp, - modifier = Modifier.size(68.dp) - ) + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + logoPainter?.let { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(68.dp) + .background(Color.White, shape = CircleShape) + ) { + Image( + painter = it, + contentDescription = null, + modifier = Modifier.size(50.dp) + ) + } + } + + if (bitmap == null) { + CaptionB("Generating QR ...", color = Colors.Black) + } } } } diff --git a/app/src/main/res/drawable/qr_placeholder.xml b/app/src/main/res/drawable/qr_placeholder.xml new file mode 100644 index 000000000..4b2514715 --- /dev/null +++ b/app/src/main/res/drawable/qr_placeholder.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + From 0eb4fcba7ca7a8b281cb6ab567a0c7e0a7c19c23 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 08:32:44 -0300 Subject: [PATCH 16/68] fix: ln invoice extraction --- .../wallets/receive/ReceiveInvoiceUtils.kt | 28 ++++++------------- .../wallets/receive/ReceiveQrScreen.kt | 7 +---- .../screens/wallets/receive/ReceiveSheet.kt | 1 - 3 files changed, 9 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index 090068e32..968536cb5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -48,7 +48,8 @@ fun getInvoiceForTab( bip21: String, bolt11: String, cjitInvoice: String?, - onchainAddress: String + isNodeRunning: Boolean, + onchainAddress: String, ): String { return when (tab) { ReceiveTab.SAVINGS -> { @@ -56,15 +57,15 @@ fun getInvoiceForTab( val strippedBip21 = stripLightningFromBip21(bip21) strippedBip21.ifEmpty { onchainAddress } } + ReceiveTab.AUTO -> { - // Unified: prefer CJIT > full BIP21 - cjitInvoice?.takeIf { it.isNotEmpty() } - ?: bip21.ifEmpty { onchainAddress } + bip21.takeIf { isNodeRunning }.orEmpty() } + ReceiveTab.SPENDING -> { // Lightning only: prefer CJIT > bolt11 - cjitInvoice?.takeIf { it.isNotEmpty() } - ?: bolt11.ifEmpty { onchainAddress } + cjitInvoice?.takeIf { it.isNotEmpty() && isNodeRunning } + ?: bolt11 } } } @@ -84,20 +85,7 @@ fun getQrLogoResource(tab: ReceiveTab, hasCjit: Boolean): Int { if (hasCjit) R.drawable.ic_unified_circle else R.drawable.ic_unified_circle } + ReceiveTab.SPENDING -> R.drawable.ic_ln_circle } } - -/** - * Extension: Check if node lifecycle state is running. - */ -fun NodeLifecycleState.isRunning(): Boolean { - return this == NodeLifecycleState.Running -} - -/** - * Extension: Check if node lifecycle state is starting. - */ -fun NodeLifecycleState.isStarting(): Boolean { - return this == NodeLifecycleState.Starting -} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 6badc4f9b..e5b948adf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -64,7 +64,6 @@ import to.bitkit.viewmodels.MainUiState @Composable fun ReceiveQrScreen( cjitInvoice: MutableState, - cjitActive: MutableState, walletState: MainUiState, lightningState: to.bitkit.repositories.LightningState, onClickEditInvoice: () -> Unit, @@ -112,6 +111,7 @@ fun ReceiveQrScreen( bip21 = walletState.bip21, bolt11 = walletState.bolt11, cjitInvoice = cjitInvoice.value, + isNodeRunning = walletState.nodeLifecycleState.isRunning(), onchainAddress = walletState.onchainAddress ) } @@ -449,7 +449,6 @@ private fun PreviewSavingsMode() { BottomSheetPreview { ReceiveQrScreen( cjitInvoice = remember { mutableStateOf(null) }, - cjitActive = remember { mutableStateOf(false) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", @@ -518,7 +517,6 @@ private fun PreviewAutoMode() { BottomSheetPreview { ReceiveQrScreen( cjitInvoice = remember { mutableStateOf(null) }, - cjitActive = remember { mutableStateOf(false) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", @@ -547,7 +545,6 @@ private fun PreviewSpendingMode() { BottomSheetPreview { ReceiveQrScreen( cjitInvoice = remember { mutableStateOf(null) }, - cjitActive = remember { mutableStateOf(false) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79vhx9n2ps8q6tcdehhxapqd9h8vmmfvdjjqen0wgsyqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxq" @@ -572,7 +569,6 @@ private fun PreviewNodeNotReady() { BottomSheetPreview { ReceiveQrScreen( cjitInvoice = remember { mutableStateOf(null) }, - cjitActive = remember { mutableStateOf(false) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Starting, ), @@ -595,7 +591,6 @@ private fun PreviewSmall() { BottomSheetPreview { ReceiveQrScreen( cjitInvoice = remember { mutableStateOf(null) }, - cjitActive = remember { mutableStateOf(false) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, ), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index ec5ddc934..ff66417a4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -76,7 +76,6 @@ fun ReceiveSheet( ReceiveQrScreen( cjitInvoice = cjitInvoice, - cjitActive = showCreateCjit, walletState = walletState, lightningState = lightningState, onClickEditInvoice = { navController.navigate(ReceiveRoute.EditInvoice) }, From db352d144b708a4c778d183606102e485e7b4f40 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 08:43:49 -0300 Subject: [PATCH 17/68] chore: simplify onchain address extraction --- .../wallets/receive/ReceiveInvoiceUtils.kt | 44 ++----------------- .../wallets/receive/ReceiveQrScreen.kt | 6 +-- 2 files changed, 6 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index 968536cb5..e0a9702cb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -1,37 +1,6 @@ package to.bitkit.ui.screens.wallets.receive -import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R -import to.bitkit.models.NodeLifecycleState - -/** - * Strips the Lightning invoice parameter from a BIP21 URI, returning pure onchain address. - * - * Example: - * Input: "bitcoin:bc1q...?amount=0.001&lightning=lnbc..." - * Output: "bitcoin:bc1q...?amount=0.001" - * - * @param bip21 The full BIP21 URI - * @return BIP21 URI without lightning parameter - */ -fun stripLightningFromBip21(bip21: String): String { - if (bip21.isEmpty()) return bip21 - - // Remove lightning parameter and its value - // Pattern: &lightning=... or ?lightning=... - val lightningParamRegex = Regex("[?&]lightning=[^&]*") - var result = bip21.replace(lightningParamRegex, "") - - // If we removed the first param (started with ?), convert & to ? - if (result.contains("&") && !result.contains("?")) { - result = result.replaceFirst("&", "?") - } - - // Clean up trailing ? or & - result = result.trimEnd('?', '&') - - return result -} /** * Returns the appropriate invoice/address for the selected tab. @@ -53,9 +22,7 @@ fun getInvoiceForTab( ): String { return when (tab) { ReceiveTab.SAVINGS -> { - // Pure onchain: strip lightning from BIP21 - val strippedBip21 = stripLightningFromBip21(bip21) - strippedBip21.ifEmpty { onchainAddress } + onchainAddress } ReceiveTab.AUTO -> { @@ -77,15 +44,10 @@ fun getInvoiceForTab( * @param hasCjit Whether a CJIT invoice is active * @return Drawable resource ID for QR logo */ -fun getQrLogoResource(tab: ReceiveTab, hasCjit: Boolean): Int { +fun getQrLogoResource(tab: ReceiveTab): Int { return when (tab) { ReceiveTab.SAVINGS -> R.drawable.ic_btc_circle - ReceiveTab.AUTO -> { - // Unified logo if CJIT or standard unified - if (hasCjit) R.drawable.ic_unified_circle - else R.drawable.ic_unified_circle - } - + ReceiveTab.AUTO -> R.drawable.ic_unified_circle ReceiveTab.SPENDING -> R.drawable.ic_ln_circle } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index e5b948adf..615de69ec 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -89,7 +89,7 @@ fun ReceiveQrScreen( // Dynamic tab visibility val visibleTabs = remember(walletState, lightningState) { buildList { - add(ReceiveTab.SAVINGS) // Always visible + add(ReceiveTab.SAVINGS) if (walletState.channels.isNotEmpty()) { add(ReceiveTab.AUTO) } @@ -117,8 +117,8 @@ fun ReceiveQrScreen( } // QR logo based on selected tab - val qrLogoRes = remember(selectedTab, cjitInvoice.value) { - getQrLogoResource(selectedTab, cjitInvoice.value != null) + val qrLogoRes = remember(selectedTab) { + getQrLogoResource(selectedTab) } Column( From 398271e61e66ce07a07ffae289635dca33053458 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 08:52:19 -0300 Subject: [PATCH 18/68] feat: button detail visibility --- .../wallets/receive/ReceiveQrScreen.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 615de69ec..424cb8952 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -1,6 +1,7 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -177,16 +178,17 @@ fun ReceiveQrScreen( Spacer(Modifier.height(24.dp)) - // Toggle button - PrimaryButton( - text = stringResource( - if (showDetails) R.string.wallet__receive_show_qr - else R.string.wallet__receive_show_details - ), - onClick = { showDetails = !showDetails }, - fullWidth = true, - modifier = Modifier.testTag("ReceiveToggleButton") - ) + AnimatedVisibility(visible = lightningState.nodeLifecycleState.isRunning()) { + PrimaryButton( + text = stringResource( + if (showDetails) R.string.wallet__receive_show_qr + else R.string.wallet__receive_show_details + ), + onClick = { showDetails = !showDetails }, + fullWidth = true, + modifier = Modifier.testTag("ReceiveToggleButton") + ) + } Spacer(Modifier.height(16.dp)) } From 63273d21398fff31f2e4ed17b9a2651cc70bced2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 08:59:26 -0300 Subject: [PATCH 19/68] fix: qr_placeholder glitch --- app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt index 2d124cfb0..791b4ae61 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -9,10 +9,10 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable @@ -118,6 +118,7 @@ fun QrCodeImage( painter = painterResource(R.drawable.qr_placeholder), contentDescription = content, contentScale = ContentScale.Inside, + modifier = Modifier.fillMaxSize() ) } } From 58bb34b66ff67e191cecf1250de96eb026ddfdac Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 09:24:10 -0300 Subject: [PATCH 20/68] chore: clean import --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 424cb8952..45c0fb685 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -43,6 +43,7 @@ import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.ext.truncate import to.bitkit.models.NodeLifecycleState +import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize @@ -66,7 +67,7 @@ import to.bitkit.viewmodels.MainUiState fun ReceiveQrScreen( cjitInvoice: MutableState, walletState: MainUiState, - lightningState: to.bitkit.repositories.LightningState, + lightningState: LightningState, onClickEditInvoice: () -> Unit, modifier: Modifier = Modifier, initialTab: ReceiveTab? = null, From 72f073793a9657b352e6a6117b54cbb28a534469 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 10:06:50 -0300 Subject: [PATCH 21/68] fix: don't reset details on tab change --- .../java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 45c0fb685..6b1eb1d39 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -147,7 +147,6 @@ fun ReceiveQrScreen( }, onTabChange = { tab -> selectedTab = tab - showDetails = false // Reset to QR when switching tabs } ) From ea3e9b8cda41277438c69805741e51e6d63d4a0f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 10:15:10 -0300 Subject: [PATCH 22/68] feat: cjit onboard WIP --- .../wallets/receive/ReceiveQrScreen.kt | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 6b1eb1d39..7e77717fc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -74,10 +74,12 @@ fun ReceiveQrScreen( ) { SetMaxBrightness() + val hasUsableChannels = lightningState.channels.isNotEmpty() + // Tab selection state var selectedTab by remember { mutableStateOf( - initialTab ?: if (walletState.channels.isNotEmpty()) { + initialTab ?: if (hasUsableChannels) { ReceiveTab.AUTO } else { ReceiveTab.SAVINGS @@ -92,13 +94,17 @@ fun ReceiveQrScreen( val visibleTabs = remember(walletState, lightningState) { buildList { add(ReceiveTab.SAVINGS) - if (walletState.channels.isNotEmpty()) { + if (hasUsableChannels) { add(ReceiveTab.AUTO) } add(ReceiveTab.SPENDING) } } + val showingCjitOnboarding = remember(selectedTab, hasUsableChannels) { + selectedTab == ReceiveTab.SPENDING && !hasUsableChannels + } + // Auto-correct selected tab if it becomes hidden LaunchedEffect(visibleTabs) { if (selectedTab !in visibleTabs) { @@ -157,22 +163,30 @@ fun ReceiveQrScreen( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.weight(1f) ) { - if (showDetails) { - ReceiveDetailsView( - tab = selectedTab, - onchainAddress = walletState.onchainAddress, - bolt11 = walletState.bolt11, - cjitInvoice = cjitInvoice.value, - modifier = Modifier.weight(1f) - ) - } else { - ReceiveQrView( - uri = currentInvoice, - qrLogoPainter = painterResource(qrLogoRes), - onClickEditInvoice = onClickEditInvoice, - tab = selectedTab, - modifier = Modifier.fillMaxWidth() - ) + when { + showingCjitOnboarding -> { + CjitOnBoardingView( + modifier = Modifier.weight(1f) + ) + } + showDetails -> { + ReceiveDetailsView( + tab = selectedTab, + onchainAddress = walletState.onchainAddress, + bolt11 = walletState.bolt11, + cjitInvoice = cjitInvoice.value, + modifier = Modifier.weight(1f) + ) + } + else -> { + ReceiveQrView( + uri = currentInvoice, + qrLogoPainter = painterResource(qrLogoRes), + onClickEditInvoice = onClickEditInvoice, + tab = selectedTab, + modifier = Modifier.fillMaxWidth() + ) + } } } @@ -303,6 +317,11 @@ private fun ReceiveQrView( } } +@Composable +fun CjitOnBoardingView(modifier: Modifier = Modifier) { + +} + @Composable private fun ReceiveDetailsView( tab: ReceiveTab, From e10b37fb35e9be6ebc32355c57acf1af4a22d942 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 10:35:32 -0300 Subject: [PATCH 23/68] feat: cjit onboard WIP --- .../wallets/receive/ReceiveQrScreen.kt | 30 +++++++++++++------ app/src/main/res/values/strings.xml | 1 + 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 7e77717fc..43febbb11 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -87,10 +87,8 @@ fun ReceiveQrScreen( ) } - // QR vs Details toggle state var showDetails by remember { mutableStateOf(false) } - // Dynamic tab visibility val visibleTabs = remember(walletState, lightningState) { buildList { add(ReceiveTab.SAVINGS) @@ -169,6 +167,7 @@ fun ReceiveQrScreen( modifier = Modifier.weight(1f) ) } + showDetails -> { ReceiveDetailsView( tab = selectedTab, @@ -178,6 +177,7 @@ fun ReceiveQrScreen( modifier = Modifier.weight(1f) ) } + else -> { ReceiveQrView( uri = currentInvoice, @@ -195,9 +195,21 @@ fun ReceiveQrScreen( AnimatedVisibility(visible = lightningState.nodeLifecycleState.isRunning()) { PrimaryButton( text = stringResource( - if (showDetails) R.string.wallet__receive_show_qr - else R.string.wallet__receive_show_details + when { + showingCjitOnboarding -> R.string.wallet__receive__cjit + showDetails -> R.string.wallet__receive_show_qr + else -> R.string.wallet__receive_show_details + } ), + icon = { + if (showingCjitOnboarding) { + Icon( + painter = painterResource(R.drawable.ic_lightning_alt), + tint = Colors.Purple, + contentDescription = null + ) + } + }, onClick = { showDetails = !showDetails }, fullWidth = true, modifier = Modifier.testTag("ReceiveToggleButton") @@ -475,7 +487,7 @@ private fun PreviewSavingsMode() { onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", channels = emptyList() ), - lightningState = to.bitkit.repositories.LightningState( + lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, shouldBlockLightningReceive = false, isGeoBlocked = false @@ -545,7 +557,7 @@ private fun PreviewAutoMode() { bip21 = "bitcoin:bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l?lightning=lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", channels = listOf(mockChannel) ), - lightningState = to.bitkit.repositories.LightningState( + lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, shouldBlockLightningReceive = false, isGeoBlocked = false @@ -570,7 +582,7 @@ private fun PreviewSpendingMode() { nodeLifecycleState = NodeLifecycleState.Running, bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79vhx9n2ps8q6tcdehhxapqd9h8vmmfvdjjqen0wgsyqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxq" ), - lightningState = to.bitkit.repositories.LightningState( + lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, shouldBlockLightningReceive = false, isGeoBlocked = false @@ -593,7 +605,7 @@ private fun PreviewNodeNotReady() { walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Starting, ), - lightningState = to.bitkit.repositories.LightningState( + lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Starting, shouldBlockLightningReceive = false, isGeoBlocked = false @@ -615,7 +627,7 @@ private fun PreviewSmall() { walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, ), - lightningState = to.bitkit.repositories.LightningState( + lightningState = LightningState( nodeLifecycleState = NodeLifecycleState.Running, shouldBlockLightningReceive = false, isGeoBlocked = false diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 254d57044..941509368 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -963,6 +963,7 @@ Bitcoin invoice Lightning invoice Optional note to payer + Receive Lightning funds Show QR Code Show Details Savings From 2f2a39dde7de386d2af9819703ae0c2861e216de Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 10:45:57 -0300 Subject: [PATCH 24/68] feat: cjit navigation --- .../wallets/receive/ReceiveQrScreen.kt | 33 +++++++++++++------ .../screens/wallets/receive/ReceiveSheet.kt | 4 +++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 43febbb11..45800fc28 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -69,6 +69,7 @@ fun ReceiveQrScreen( walletState: MainUiState, lightningState: LightningState, onClickEditInvoice: () -> Unit, + onClickReceiveCjit: () -> Unit, modifier: Modifier = Modifier, initialTab: ReceiveTab? = null, ) { @@ -203,14 +204,21 @@ fun ReceiveQrScreen( ), icon = { if (showingCjitOnboarding) { - Icon( - painter = painterResource(R.drawable.ic_lightning_alt), - tint = Colors.Purple, - contentDescription = null - ) - } + Icon( + painter = painterResource(R.drawable.ic_lightning_alt), + tint = Colors.Purple, + contentDescription = null + ) + } + }, + onClick = { + if (showingCjitOnboarding) { + onClickReceiveCjit() + showDetails = false + } else { + showDetails = !showDetails + } }, - onClick = { showDetails = !showDetails }, fullWidth = true, modifier = Modifier.testTag("ReceiveToggleButton") ) @@ -494,7 +502,8 @@ private fun PreviewSavingsMode() { ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), - initialTab = ReceiveTab.SAVINGS + initialTab = ReceiveTab.SAVINGS, + onClickReceiveCjit = {}, ) } } @@ -564,7 +573,8 @@ private fun PreviewAutoMode() { ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), - initialTab = ReceiveTab.AUTO + initialTab = ReceiveTab.AUTO, + onClickReceiveCjit = {}, ) } } @@ -589,7 +599,8 @@ private fun PreviewSpendingMode() { ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), - initialTab = ReceiveTab.SPENDING + initialTab = ReceiveTab.SPENDING, + onClickReceiveCjit = {}, ) } } @@ -610,6 +621,7 @@ private fun PreviewNodeNotReady() { shouldBlockLightningReceive = false, isGeoBlocked = false ), + onClickReceiveCjit = {}, onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), ) @@ -634,6 +646,7 @@ private fun PreviewSmall() { ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), + onClickReceiveCjit = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index ff66417a4..84804e97c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -78,6 +78,10 @@ fun ReceiveSheet( cjitInvoice = cjitInvoice, walletState = walletState, lightningState = lightningState, + onClickReceiveCjit = { + showCreateCjit.value = true + navController.navigate(ReceiveRoute.Amount) + }, onClickEditInvoice = { navController.navigate(ReceiveRoute.EditInvoice) }, ) } From 8bb0d24b4d1307fa6bf320e59116262e31eb283c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 12:33:32 -0300 Subject: [PATCH 25/68] feat: cjit navigation --- .../to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index 84804e97c..fb1be6440 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -79,8 +79,13 @@ fun ReceiveSheet( walletState = walletState, lightningState = lightningState, onClickReceiveCjit = { - showCreateCjit.value = true - navController.navigate(ReceiveRoute.Amount) + if (lightningState.isGeoBlocked) { + // todo display toast instead + navController.navigate(ReceiveRoute.GeoBlock) + } else { + showCreateCjit.value = true + navController.navigate(ReceiveRoute.Amount) + } }, onClickEditInvoice = { navController.navigate(ReceiveRoute.EditInvoice) }, ) From eed3ce0df767389cd2d99c88b57500c6f8f78098 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 13:48:43 -0300 Subject: [PATCH 26/68] feat: cjit onboarding view --- .../wallets/receive/ReceiveQrScreen.kt | 50 ++++++++++++++++++- app/src/main/res/drawable/arrow.xml | 9 ++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/drawable/arrow.xml diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 45800fc28..35ddd2efb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -2,10 +2,13 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -28,6 +31,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.keepScreenOn import androidx.compose.ui.platform.LocalContext @@ -44,13 +48,17 @@ import to.bitkit.ext.setClipboardText import to.bitkit.ext.truncate import to.bitkit.models.NodeLifecycleState import to.bitkit.repositories.LightningState +import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.Display +import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.components.Tooltip +import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing import to.bitkit.ui.shared.effects.SetMaxBrightness @@ -61,6 +69,7 @@ import to.bitkit.ui.shared.util.shareText import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.MainUiState @Composable @@ -339,7 +348,44 @@ private fun ReceiveQrView( @Composable fun CjitOnBoardingView(modifier: Modifier = Modifier) { - + Column( + modifier = modifier + .clip(AppShapes.small) + .background(color = Colors.Black) + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Display("Receive on spending balance".withAccent(accentColor = Colors.Purple)) + VerticalSpacer(8.dp) + BodyM( + "Enjoy instant and cheap\ntransactions with friends, family,\nand merchants.", + color = Colors.White64, + modifier = Modifier.fillMaxWidth() + ) + VerticalSpacer(32.dp) + Box(modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Icon( + painter = painterResource(R.drawable.ic_lightning_alt), + tint = Colors.Purple, + contentDescription = null, + modifier = Modifier + .size(64.dp) + .align(Alignment.TopCenter) + ) + Icon( + painter = painterResource(R.drawable.arrow), + tint = Colors.Purple, + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 32.dp) + .fillMaxHeight() + ) + } + } } @Composable @@ -360,7 +406,7 @@ private fun ReceiveDetailsView( ReceiveTab.SAVINGS -> { if (onchainAddress.isNotEmpty()) { CopyAddressCard( - title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_bitcoin_invoice), + title = stringResource(R.string.wallet__receive_bitcoin_invoice), address = onchainAddress, type = CopyAddressType.ONCHAIN, testTag = "ReceiveOnchainAddress", diff --git a/app/src/main/res/drawable/arrow.xml b/app/src/main/res/drawable/arrow.xml new file mode 100644 index 000000000..9d3ab6b58 --- /dev/null +++ b/app/src/main/res/drawable/arrow.xml @@ -0,0 +1,9 @@ + + + From c6ab6348f166be938a44d7691a30d46d6c0d5418 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 14:01:52 -0300 Subject: [PATCH 27/68] chore: remove redundant code --- .../wallets/receive/ReceiveQrScreen.kt | 45 ++++--------------- .../screens/wallets/receive/ReceiveSheet.kt | 1 - 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 35ddd2efb..d67d091d1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -47,14 +47,12 @@ import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.ext.truncate import to.bitkit.models.NodeLifecycleState -import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display -import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.components.Tooltip @@ -76,7 +74,6 @@ import to.bitkit.viewmodels.MainUiState fun ReceiveQrScreen( cjitInvoice: MutableState, walletState: MainUiState, - lightningState: LightningState, onClickEditInvoice: () -> Unit, onClickReceiveCjit: () -> Unit, modifier: Modifier = Modifier, @@ -84,7 +81,7 @@ fun ReceiveQrScreen( ) { SetMaxBrightness() - val hasUsableChannels = lightningState.channels.isNotEmpty() + val hasUsableChannels = walletState.channels.isNotEmpty() // Tab selection state var selectedTab by remember { @@ -99,7 +96,7 @@ fun ReceiveQrScreen( var showDetails by remember { mutableStateOf(false) } - val visibleTabs = remember(walletState, lightningState) { + val visibleTabs = remember(hasUsableChannels) { buildList { add(ReceiveTab.SAVINGS) if (hasUsableChannels) { @@ -202,7 +199,7 @@ fun ReceiveQrScreen( Spacer(Modifier.height(24.dp)) - AnimatedVisibility(visible = lightningState.nodeLifecycleState.isRunning()) { + AnimatedVisibility(visible = walletState.nodeLifecycleState.isRunning()) { PrimaryButton( text = stringResource( when { @@ -363,9 +360,10 @@ fun CjitOnBoardingView(modifier: Modifier = Modifier) { modifier = Modifier.fillMaxWidth() ) VerticalSpacer(32.dp) - Box(modifier = Modifier - .fillMaxWidth() - .weight(1f) + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) ) { Icon( painter = painterResource(R.drawable.ic_lightning_alt), @@ -541,11 +539,6 @@ private fun PreviewSavingsMode() { onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", channels = emptyList() ), - lightningState = LightningState( - nodeLifecycleState = NodeLifecycleState.Running, - shouldBlockLightningReceive = false, - isGeoBlocked = false - ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), initialTab = ReceiveTab.SAVINGS, @@ -607,15 +600,10 @@ private fun PreviewAutoMode() { cjitInvoice = remember { mutableStateOf(null) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, + channels = listOf(mockChannel), onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79vhx9n2ps8q6tcdehhxapqd9h8vmmfvdjjqen0wgsyqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxq", - bip21 = "bitcoin:bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l?lightning=lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", - channels = listOf(mockChannel) - ), - lightningState = LightningState( - nodeLifecycleState = NodeLifecycleState.Running, - shouldBlockLightningReceive = false, - isGeoBlocked = false + bip21 = "bitcoin:bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l?lightning=lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79..." ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), @@ -638,11 +626,6 @@ private fun PreviewSpendingMode() { nodeLifecycleState = NodeLifecycleState.Running, bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79vhx9n2ps8q6tcdehhxapqd9h8vmmfvdjjqen0wgsyqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxq" ), - lightningState = LightningState( - nodeLifecycleState = NodeLifecycleState.Running, - shouldBlockLightningReceive = false, - isGeoBlocked = false - ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), initialTab = ReceiveTab.SPENDING, @@ -662,11 +645,6 @@ private fun PreviewNodeNotReady() { walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Starting, ), - lightningState = LightningState( - nodeLifecycleState = NodeLifecycleState.Starting, - shouldBlockLightningReceive = false, - isGeoBlocked = false - ), onClickReceiveCjit = {}, onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), @@ -685,11 +663,6 @@ private fun PreviewSmall() { walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, ), - lightningState = LightningState( - nodeLifecycleState = NodeLifecycleState.Running, - shouldBlockLightningReceive = false, - isGeoBlocked = false - ), onClickEditInvoice = {}, modifier = Modifier.sheetHeight(), onClickReceiveCjit = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index fb1be6440..d7afd8d9f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -77,7 +77,6 @@ fun ReceiveSheet( ReceiveQrScreen( cjitInvoice = cjitInvoice, walletState = walletState, - lightningState = lightningState, onClickReceiveCjit = { if (lightningState.isGeoBlocked) { // todo display toast instead From 6ce2bd5938c6e0e78d26b50b3861aa0833cc7e84 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 14:06:01 -0300 Subject: [PATCH 28/68] fix: filter usable channels --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index d67d091d1..f6f4e44ef 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -81,7 +81,7 @@ fun ReceiveQrScreen( ) { SetMaxBrightness() - val hasUsableChannels = walletState.channels.isNotEmpty() + val hasUsableChannels = walletState.channels.any { it.isUsable } // Tab selection state var selectedTab by remember { From d8c2f0fbc96fcd0adc18cd73295149f5ab357aed Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 14:16:10 -0300 Subject: [PATCH 29/68] fix: preview --- .../wallets/receive/ReceiveQrScreen.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index f6f4e44ef..a60c15aac 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -618,12 +618,54 @@ private fun PreviewAutoMode() { @Preview(showSystemUi = true, name = "Spending Mode") @Composable private fun PreviewSpendingMode() { + val mockChannel = ChannelDetails( + channelId = "0".repeat(64), + counterpartyNodeId = "0".repeat(66), + fundingTxo = null, + shortChannelId = null, + outboundScidAlias = null, + inboundScidAlias = null, + channelValueSats = 1000000uL, + unspendablePunishmentReserve = null, + userChannelId = "0".repeat(32), + feerateSatPer1000Weight = 1000u, + outboundCapacityMsat = 500000000uL, + inboundCapacityMsat = 500000000uL, + confirmationsRequired = null, + confirmations = null, + isOutbound = true, + isChannelReady = true, + isUsable = true, + isAnnounced = false, + cltvExpiryDelta = null, + counterpartyUnspendablePunishmentReserve = 0uL, + counterpartyOutboundHtlcMinimumMsat = null, + counterpartyOutboundHtlcMaximumMsat = null, + counterpartyForwardingInfoFeeBaseMsat = null, + counterpartyForwardingInfoFeeProportionalMillionths = null, + counterpartyForwardingInfoCltvExpiryDelta = null, + nextOutboundHtlcLimitMsat = 0uL, + nextOutboundHtlcMinimumMsat = 0uL, + forceCloseSpendDelay = null, + inboundHtlcMinimumMsat = 0uL, + inboundHtlcMaximumMsat = null, + config = org.lightningdevkit.ldknode.ChannelConfig( + forwardingFeeProportionalMillionths = 0u, + forwardingFeeBaseMsat = 0u, + cltvExpiryDelta = 0u, + maxDustHtlcExposure = org.lightningdevkit.ldknode.MaxDustHtlcExposure.FeeRateMultiplier(0uL), + forceCloseAvoidanceMaxFeeSatoshis = 0uL, + acceptUnderpayingHtlcs = false + ) + ) + AppThemeSurface { BottomSheetPreview { ReceiveQrScreen( cjitInvoice = remember { mutableStateOf(null) }, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, + channels = listOf(mockChannel), bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79vhx9n2ps8q6tcdehhxapqd9h8vmmfvdjjqen0wgsyqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxqcrqvpsxq" ), onClickEditInvoice = {}, From e023fa2e04103ac5a1e9eee567894c285f5dab48 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 28 Nov 2025 14:21:48 -0300 Subject: [PATCH 30/68] fix: only display cjit if node is running --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index a60c15aac..82f4d1cf5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -106,8 +106,8 @@ fun ReceiveQrScreen( } } - val showingCjitOnboarding = remember(selectedTab, hasUsableChannels) { - selectedTab == ReceiveTab.SPENDING && !hasUsableChannels + val showingCjitOnboarding = remember(selectedTab, walletState) { + selectedTab == ReceiveTab.SPENDING && !hasUsableChannels && walletState.nodeLifecycleState.isRunning() } // Auto-correct selected tab if it becomes hidden From 2b94867a8838e72a9fabf40793e977ab67db16b2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 07:15:37 -0300 Subject: [PATCH 31/68] feat: toast geoblocked --- app/src/main/java/to/bitkit/ui/ContentView.kt | 4 ++++ .../ui/screens/wallets/receive/ReceiveSheet.kt | 12 +++++++++++- .../main/java/to/bitkit/viewmodels/AppViewModel.kt | 10 ++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 873ef5a77..b9d594aa6 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -388,6 +388,10 @@ fun ContentView( navigateToExternalConnection = { navController.navigate(ExternalConnection()) appViewModel.hideSheet() + }, + toast = { toast -> + appViewModel.toast(toast) + } ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index d7afd8d9f..ec8f5f6f9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -16,6 +16,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import kotlinx.serialization.Serializable +import to.bitkit.models.Toast import to.bitkit.repositories.LightningState import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.shared.modifiers.sheetHeight @@ -31,6 +32,7 @@ import to.bitkit.viewmodels.WalletViewModelEffects fun ReceiveSheet( navigateToExternalConnection: () -> Unit, walletState: MainUiState, + toast: (Toast) -> Unit, editInvoiceAmountViewModel: AmountInputViewModel = hiltViewModel(), settingsViewModel: SettingsViewModel = hiltViewModel(), ) { @@ -79,7 +81,15 @@ fun ReceiveSheet( walletState = walletState, onClickReceiveCjit = { if (lightningState.isGeoBlocked) { - // todo display toast instead + toast( + Toast( + type = Toast.ToastType.ERROR, + title = "Instant Payments Unavailable", + description = "Bitkit does not provide Lightning services in your country, but you can still connect to other nodes.", + autoHide = true, + ) + ) + navController.navigate(ReceiveRoute.GeoBlock) } else { showCreateCjit.value = true diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index c9546614e..1732edd4b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1448,6 +1448,16 @@ class AppViewModel @Inject constructor( toast(type = Toast.ToastType.ERROR, title = "Error", description = error.message ?: "Unknown error") } + fun toast(toast: Toast) { + toast( + type = toast.type, + title = toast.title, + description = toast.description, + autoHide = toast.autoHide, + visibilityTime = toast.visibilityTime + ) + } + fun hideToast() { currentToast = null } From 46f3ddf38f7872e49d2b939624f5efeb0c04e740 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 07:40:23 -0300 Subject: [PATCH 32/68] chore: remove geoblock navigation --- .../java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index ec8f5f6f9..d71b1b375 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -89,8 +89,6 @@ fun ReceiveSheet( autoHide = true, ) ) - - navController.navigate(ReceiveRoute.GeoBlock) } else { showCreateCjit.value = true navController.navigate(ReceiveRoute.Amount) From 670a67a2cbb2dbaa0488395146eb72b54a7c57d9 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 08:09:10 -0300 Subject: [PATCH 33/68] feat: implement swipe --- .../wallets/receive/ReceiveQrScreen.kt | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 82f4d1cf5..88e6ecc94 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -3,6 +3,7 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,8 +32,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.keepScreenOn import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag @@ -106,6 +110,10 @@ fun ReceiveQrScreen( } } + val currentTabIndex = remember(selectedTab, visibleTabs) { + visibleTabs.indexOf(selectedTab) + } + val showingCjitOnboarding = remember(selectedTab, walletState) { selectedTab == ReceiveTab.SPENDING && !hasUsableChannels && walletState.nodeLifecycleState.isRunning() } @@ -166,7 +174,15 @@ fun ReceiveQrScreen( // Content area (QR or Details) Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .swipeToChangeTab( + currentTabIndex = currentTabIndex, + tabCount = visibleTabs.size, + onTabChange = { newIndex -> + selectedTab = visibleTabs[newIndex] + } + ) ) { when { showingCjitOnboarding -> { @@ -735,3 +751,33 @@ private fun PreviewDetailsMode() { } } } + +private fun Modifier.swipeToChangeTab( + currentTabIndex: Int, + tabCount: Int, + onTabChange: (Int) -> Unit +) = composed { + val threshold = remember { 1500f } + val velocityTracker = remember { VelocityTracker() } + + pointerInput(currentTabIndex) { + detectHorizontalDragGestures( + onHorizontalDrag = { change, _ -> + velocityTracker.addPosition(change.uptimeMillis, change.position) + }, + onDragEnd = { + val velocity = velocityTracker.calculateVelocity().x + when { + velocity >= threshold && currentTabIndex > 0 -> + onTabChange(currentTabIndex - 1) + velocity <= -threshold && currentTabIndex < tabCount - 1 -> + onTabChange(currentTabIndex + 1) + } + velocityTracker.resetTracking() + }, + onDragCancel = { + velocityTracker.resetTracking() + }, + ) + } +} From 5d7ea5eedeee28df2332394938030bdffe75f051 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 08:10:12 -0300 Subject: [PATCH 34/68] chore: reuse variable --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 88e6ecc94..70236aab0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -158,7 +158,7 @@ fun ReceiveQrScreen( // Tab row CustomTabRowWithSpacing( tabs = visibleTabs, - currentTabIndex = visibleTabs.indexOf(selectedTab), + currentTabIndex = currentTabIndex, selectedColor = when (selectedTab) { ReceiveTab.SAVINGS -> Colors.Brand ReceiveTab.AUTO -> Colors.White From 2f5c9c37f812ced52d802c12958f8b9a1cd60b8f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 08:41:45 -0300 Subject: [PATCH 35/68] refactor replace mutable state with string --- .../wallets/receive/ReceiveQrScreen.kt | 22 +++++++++---------- .../screens/wallets/receive/ReceiveSheet.kt | 2 +- .../ui/screens/wallets/receive/ReceiveTab.kt | 6 ++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 70236aab0..b943c18c0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -76,7 +75,7 @@ import to.bitkit.viewmodels.MainUiState @Composable fun ReceiveQrScreen( - cjitInvoice: MutableState, + cjitInvoice: String?, walletState: MainUiState, onClickEditInvoice: () -> Unit, onClickReceiveCjit: () -> Unit, @@ -126,12 +125,12 @@ fun ReceiveQrScreen( } // Current invoice for display - val currentInvoice = remember(selectedTab, walletState, cjitInvoice.value) { + val currentInvoice = remember(selectedTab, walletState, cjitInvoice) { getInvoiceForTab( tab = selectedTab, bip21 = walletState.bip21, bolt11 = walletState.bolt11, - cjitInvoice = cjitInvoice.value, + cjitInvoice = cjitInvoice, isNodeRunning = walletState.nodeLifecycleState.isRunning(), onchainAddress = walletState.onchainAddress ) @@ -196,7 +195,7 @@ fun ReceiveQrScreen( tab = selectedTab, onchainAddress = walletState.onchainAddress, bolt11 = walletState.bolt11, - cjitInvoice = cjitInvoice.value, + cjitInvoice = cjitInvoice, modifier = Modifier.weight(1f) ) } @@ -549,7 +548,7 @@ private fun PreviewSavingsMode() { AppThemeSurface { BottomSheetPreview { ReceiveQrScreen( - cjitInvoice = remember { mutableStateOf(null) }, + cjitInvoice = null, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", @@ -613,7 +612,7 @@ private fun PreviewAutoMode() { AppThemeSurface { BottomSheetPreview { ReceiveQrScreen( - cjitInvoice = remember { mutableStateOf(null) }, + cjitInvoice = null, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, channels = listOf(mockChannel), @@ -678,7 +677,7 @@ private fun PreviewSpendingMode() { AppThemeSurface { BottomSheetPreview { ReceiveQrScreen( - cjitInvoice = remember { mutableStateOf(null) }, + cjitInvoice = null, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, channels = listOf(mockChannel), @@ -699,7 +698,7 @@ private fun PreviewNodeNotReady() { AppThemeSurface { BottomSheetPreview { ReceiveQrScreen( - cjitInvoice = remember { mutableStateOf(null) }, + cjitInvoice = null, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Starting, ), @@ -717,7 +716,7 @@ private fun PreviewSmall() { AppThemeSurface { BottomSheetPreview { ReceiveQrScreen( - cjitInvoice = remember { mutableStateOf(null) }, + cjitInvoice = null, walletState = MainUiState( nodeLifecycleState = NodeLifecycleState.Running, ), @@ -755,7 +754,7 @@ private fun PreviewDetailsMode() { private fun Modifier.swipeToChangeTab( currentTabIndex: Int, tabCount: Int, - onTabChange: (Int) -> Unit + onTabChange: (Int) -> Unit, ) = composed { val threshold = remember { 1500f } val velocityTracker = remember { VelocityTracker() } @@ -770,6 +769,7 @@ private fun Modifier.swipeToChangeTab( when { velocity >= threshold && currentTabIndex > 0 -> onTabChange(currentTabIndex - 1) + velocity <= -threshold && currentTabIndex < tabCount - 1 -> onTabChange(currentTabIndex + 1) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index d71b1b375..b145251c4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -77,7 +77,7 @@ fun ReceiveSheet( } ReceiveQrScreen( - cjitInvoice = cjitInvoice, + cjitInvoice = cjitInvoice.value, walletState = walletState, onClickReceiveCjit = { if (lightningState.isGeoBlocked) { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt index 087e30b13..d7e2d7dde 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt @@ -6,9 +6,9 @@ import to.bitkit.R import to.bitkit.ui.screens.wallets.activity.components.TabItem enum class ReceiveTab : TabItem { - SAVINGS, // Pure onchain (BIP21 without Lightning) - AUTO, // Unified (BIP21 with Lightning or CJIT invoice) - SPENDING; // Pure Lightning (bolt11 or CJIT invoice) + SAVINGS, + AUTO, + SPENDING; override val uiText: String @Composable From a70d803f1daa7e4da3f519ce1c99e34dcd69d0af Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 08:46:56 -0300 Subject: [PATCH 36/68] fix: display spending as default when cjit invoice is not null --- .../ui/screens/wallets/receive/ReceiveQrScreen.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index b943c18c0..af6d4b7ae 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -89,10 +89,10 @@ fun ReceiveQrScreen( // Tab selection state var selectedTab by remember { mutableStateOf( - initialTab ?: if (hasUsableChannels) { - ReceiveTab.AUTO - } else { - ReceiveTab.SAVINGS + initialTab ?: when { + !cjitInvoice.isNullOrEmpty() -> ReceiveTab.SPENDING // If CJIT invoice exists, default to SPENDING + hasUsableChannels -> ReceiveTab.AUTO + else -> ReceiveTab.SAVINGS } ) } @@ -113,8 +113,11 @@ fun ReceiveQrScreen( visibleTabs.indexOf(selectedTab) } - val showingCjitOnboarding = remember(selectedTab, walletState) { - selectedTab == ReceiveTab.SPENDING && !hasUsableChannels && walletState.nodeLifecycleState.isRunning() + val showingCjitOnboarding = remember(selectedTab, walletState, cjitInvoice) { + selectedTab == ReceiveTab.SPENDING && + !hasUsableChannels && + walletState.nodeLifecycleState.isRunning() && + cjitInvoice.isNullOrEmpty() // Only show onboarding if there's no CJIT invoice } // Auto-correct selected tab if it becomes hidden From 1f33e00261d6b4f81e71ab932bb13a6782c0d6c8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 08:48:06 -0300 Subject: [PATCH 37/68] fix: edit cjit --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index af6d4b7ae..7a2cf1492 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -207,7 +207,11 @@ fun ReceiveQrScreen( ReceiveQrView( uri = currentInvoice, qrLogoPainter = painterResource(qrLogoRes), - onClickEditInvoice = onClickEditInvoice, + onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { + onClickEditInvoice + } else { + onClickReceiveCjit + }, tab = selectedTab, modifier = Modifier.fillMaxWidth() ) From c8662619d5653930b58899637cb337e20e796348 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 09:17:46 -0300 Subject: [PATCH 38/68] chore: lint --- .../wallets/activity/components/CustomTabRowWithSpacing.kt | 2 +- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index 9d6fe678d..fe932533f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -36,9 +36,9 @@ interface TabItem { fun CustomTabRowWithSpacing( tabs: List, currentTabIndex: Int, - selectedColor: Color = Colors.Brand, onTabChange: (T) -> Unit, modifier: Modifier = Modifier, + selectedColor: Color = Colors.Brand, ) { Column(modifier = modifier) { Row( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 7a2cf1492..4a7692743 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -90,7 +90,7 @@ fun ReceiveQrScreen( var selectedTab by remember { mutableStateOf( initialTab ?: when { - !cjitInvoice.isNullOrEmpty() -> ReceiveTab.SPENDING // If CJIT invoice exists, default to SPENDING + !cjitInvoice.isNullOrEmpty() -> ReceiveTab.SPENDING hasUsableChannels -> ReceiveTab.AUTO else -> ReceiveTab.SAVINGS } @@ -117,7 +117,7 @@ fun ReceiveQrScreen( selectedTab == ReceiveTab.SPENDING && !hasUsableChannels && walletState.nodeLifecycleState.isRunning() && - cjitInvoice.isNullOrEmpty() // Only show onboarding if there's no CJIT invoice + cjitInvoice.isNullOrEmpty() } // Auto-correct selected tab if it becomes hidden @@ -263,8 +263,8 @@ private fun ReceiveQrView( uri: String, qrLogoPainter: Painter, onClickEditInvoice: () -> Unit, - modifier: Modifier = Modifier, tab: ReceiveTab, + modifier: Modifier = Modifier, ) { val context = LocalContext.current val qrButtonTooltipState = rememberTooltipState() From ccd92a5061a3ed626d22c2dcb7a72ff0caa8472e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 09:21:15 -0300 Subject: [PATCH 39/68] chore: lint --- app/src/main/java/to/bitkit/ui/ContentView.kt | 1 - .../activity/components/CustomTabRowWithSpacing.kt | 12 ++++++------ .../ui/screens/wallets/receive/ReceiveQrScreen.kt | 1 - 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index b9d594aa6..8a33ec30c 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -391,7 +391,6 @@ fun ContentView( }, toast = { toast -> appViewModel.toast(toast) - } ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index fe932533f..97d003fc2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -26,12 +26,6 @@ import androidx.compose.ui.unit.dp import to.bitkit.ui.components.CaptionB import to.bitkit.ui.theme.Colors -interface TabItem { - val name: String - val uiText: String - @Composable get -} - @Composable fun CustomTabRowWithSpacing( tabs: List, @@ -96,3 +90,9 @@ fun CustomTabRowWithSpacing( } } } + +interface TabItem { + val name: String + val uiText: String + @Composable get +} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 4a7692743..3d04acfb2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -735,7 +735,6 @@ private fun PreviewSmall() { } } - @Suppress("SpellCheckingInspection") @Preview(showSystemUi = true, name = "Auto Mode") @Composable From a4cf140829ab0a791e3fdfab69ab73c3f21d3219 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 09:46:52 -0300 Subject: [PATCH 40/68] refactor: implement Modifier.node --- .../modifiers/SwipeToChangeTabModifier.kt | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt new file mode 100644 index 000000000..c050e777e --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt @@ -0,0 +1,54 @@ +package to.bitkit.ui.shared.modifiers + +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.util.VelocityTracker + +/** + * Enables tab navigation via horizontal swipe gestures. + * + * Detects horizontal swipe velocity and navigates to adjacent tabs when the velocity + * exceeds the specified threshold. Provides boundary protection to prevent navigation + * beyond the first and last tabs. + * + * @param currentTabIndex The currently selected tab index (0-based) + * @param tabCount Total number of tabs available for navigation + * @param onTabChange Callback invoked when user swipes to change tabs, receives the new tab index + * @param threshold Velocity threshold in pixels per second (default: 1500f) + * Swipe velocity must exceed this value to trigger navigation + */ +fun Modifier.swipeToChangeTab( + currentTabIndex: Int, + tabCount: Int, + onTabChange: (Int) -> Unit, + threshold: Float = DEFAULT_SWIPE_THRESHOLD, +): Modifier = composed { + val velocityTracker = remember { VelocityTracker() } + + pointerInput(currentTabIndex) { + detectHorizontalDragGestures( + onHorizontalDrag = { change, _ -> + velocityTracker.addPosition(change.uptimeMillis, change.position) + }, + onDragEnd = { + val velocity = velocityTracker.calculateVelocity().x + when { + velocity >= threshold && currentTabIndex > 0 -> + onTabChange(currentTabIndex - 1) + + velocity <= -threshold && currentTabIndex < tabCount - 1 -> + onTabChange(currentTabIndex + 1) + } + velocityTracker.resetTracking() + }, + onDragCancel = { + velocityTracker.resetTracking() + }, + ) + } +} + +private const val DEFAULT_SWIPE_THRESHOLD = 1500f From 777cb6c0d50715d2c603dc2a8b34f47110c4b223 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 09:46:56 -0300 Subject: [PATCH 41/68] refactor: implement Modifier.node --- .../wallets/activity/AllActivityScreen.kt | 30 +--------------- .../wallets/receive/ReceiveQrScreen.kt | 36 +------------------ 2 files changed, 2 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt index c99a213f7..a6558b341 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt @@ -1,7 +1,6 @@ package to.bitkit.ui.screens.wallets.activity import androidx.activity.compose.BackHandler -import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -12,11 +11,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -33,6 +28,7 @@ import to.bitkit.ui.screens.wallets.activity.components.ActivityListFilter import to.bitkit.ui.screens.wallets.activity.components.ActivityListGrouped import to.bitkit.ui.screens.wallets.activity.components.ActivityTab import to.bitkit.ui.screens.wallets.activity.utils.previewActivityItems +import to.bitkit.ui.shared.modifiers.swipeToChangeTab import to.bitkit.ui.shared.util.screen import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.viewmodels.ActivityListViewModel @@ -146,30 +142,6 @@ private fun AllActivityScreenContent( } } -private fun Modifier.swipeToChangeTab(currentTabIndex: Int, tabCount: Int, onTabChange: (Int) -> Unit) = composed { - val threshold = remember { 1500f } - val velocityTracker = remember { VelocityTracker() } - - pointerInput(currentTabIndex) { - detectHorizontalDragGestures( - onHorizontalDrag = { change, _ -> - velocityTracker.addPosition(change.uptimeMillis, change.position) - }, - onDragEnd = { - val velocity = velocityTracker.calculateVelocity().x - when { - velocity >= threshold && currentTabIndex > 0 -> onTabChange(currentTabIndex - 1) - velocity <= -threshold && currentTabIndex < tabCount - 1 -> onTabChange(currentTabIndex + 1) - } - velocityTracker.resetTracking() - }, - onDragCancel = { - velocityTracker.resetTracking() - }, - ) - } -} - @Preview(showSystemUi = true) @Composable private fun Preview() { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 3d04acfb2..ba8846e37 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -3,7 +3,6 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,11 +30,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.keepScreenOn import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag @@ -64,6 +60,7 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing import to.bitkit.ui.shared.effects.SetMaxBrightness import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.shared.modifiers.swipeToChangeTab import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.shareQrCode import to.bitkit.ui.shared.util.shareText @@ -756,34 +753,3 @@ private fun PreviewDetailsMode() { } } } - -private fun Modifier.swipeToChangeTab( - currentTabIndex: Int, - tabCount: Int, - onTabChange: (Int) -> Unit, -) = composed { - val threshold = remember { 1500f } - val velocityTracker = remember { VelocityTracker() } - - pointerInput(currentTabIndex) { - detectHorizontalDragGestures( - onHorizontalDrag = { change, _ -> - velocityTracker.addPosition(change.uptimeMillis, change.position) - }, - onDragEnd = { - val velocity = velocityTracker.calculateVelocity().x - when { - velocity >= threshold && currentTabIndex > 0 -> - onTabChange(currentTabIndex - 1) - - velocity <= -threshold && currentTabIndex < tabCount - 1 -> - onTabChange(currentTabIndex + 1) - } - velocityTracker.resetTracking() - }, - onDragCancel = { - velocityTracker.resetTracking() - }, - ) - } -} From 0b2a052d473688830c2b61ab55f92e7c05a7b7a3 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Mon, 1 Dec 2025 09:54:02 -0300 Subject: [PATCH 42/68] chore: remove dead parameter --- .../to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index e0a9702cb..562d375cb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -41,7 +41,6 @@ fun getInvoiceForTab( * Returns the appropriate QR code logo resource for the selected tab. * * @param tab The selected receive tab - * @param hasCjit Whether a CJIT invoice is active * @return Drawable resource ID for QR logo */ fun getQrLogoResource(tab: ReceiveTab): Int { From ceb37393d48d6f6864e0531f3c29f84ee27ff43b Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 2 Dec 2025 12:51:50 +0100 Subject: [PATCH 43/68] adjust test tag --- .../bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index ba8846e37..5d15a9ea5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -245,7 +245,13 @@ fun ReceiveQrScreen( } }, fullWidth = true, - modifier = Modifier.testTag("ReceiveToggleButton") + modifier = Modifier.testTag( + if (showDetails) { + "QRCode" + } else { + "ShowDetails" + } + ) ) } From e69e4bd7756f3c74f68cb4ab661ee027e1120c85 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 2 Dec 2025 13:35:13 -0300 Subject: [PATCH 44/68] chore: imports --- .../screens/wallets/receive/ReceiveQrScreen.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 5d15a9ea5..261cefe21 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -281,7 +281,7 @@ private fun ReceiveQrView( QrCodeImage( content = uri, logoPainter = qrLogoPainter, - tipMessage = androidx.compose.ui.res.stringResource(R.string.wallet__receive_copied), + tipMessage = stringResource(R.string.wallet__receive_copied), onBitmapGenerated = { bitmap -> qrBitmap = bitmap }, testTag = "QRCode", modifier = Modifier.weight(1f, fill = false) @@ -293,7 +293,7 @@ private fun ReceiveQrView( verticalAlignment = Alignment.Top, ) { PrimaryButton( - text = androidx.compose.ui.res.stringResource(R.string.common__edit), + text = stringResource(R.string.common__edit), size = ButtonSize.Small, onClick = onClickEditInvoice, fullWidth = false, @@ -313,11 +313,11 @@ private fun ReceiveQrView( modifier = Modifier.testTag("SpecifyInvoiceButton") ) Tooltip( - text = androidx.compose.ui.res.stringResource(R.string.wallet__receive_copied), + text = stringResource(R.string.wallet__receive_copied), tooltipState = qrButtonTooltipState ) { PrimaryButton( - text = androidx.compose.ui.res.stringResource(R.string.common__copy), + text = stringResource(R.string.common__copy), size = ButtonSize.Small, onClick = { context.setClipboardText(uri) @@ -341,7 +341,7 @@ private fun ReceiveQrView( ) } PrimaryButton( - text = androidx.compose.ui.res.stringResource(R.string.common__share), + text = stringResource(R.string.common__share), size = ButtonSize.Small, onClick = { qrBitmap?.let { bitmap -> @@ -441,7 +441,7 @@ private fun ReceiveDetailsView( // Show both onchain AND lightning if available if (onchainAddress.isNotEmpty()) { CopyAddressCard( - title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_bitcoin_invoice), + title = stringResource(R.string.wallet__receive_bitcoin_invoice), address = onchainAddress, type = CopyAddressType.ONCHAIN, testTag = "ReceiveOnchainAddress", @@ -449,7 +449,7 @@ private fun ReceiveDetailsView( } if (cjitInvoice != null || bolt11.isNotEmpty()) { CopyAddressCard( - title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_lightning_invoice), + title = stringResource(R.string.wallet__receive_lightning_invoice), address = cjitInvoice ?: bolt11, type = CopyAddressType.LIGHTNING, testTag = "ReceiveLightningAddress", @@ -460,7 +460,7 @@ private fun ReceiveDetailsView( ReceiveTab.SPENDING -> { if (cjitInvoice != null || bolt11.isNotEmpty()) { CopyAddressCard( - title = androidx.compose.ui.res.stringResource(R.string.wallet__receive_lightning_invoice), + title = stringResource(R.string.wallet__receive_lightning_invoice), address = cjitInvoice ?: bolt11, type = CopyAddressType.LIGHTNING, testTag = "ReceiveLightningAddress", From 805dd5450ee43893c2a2dc66cdd455e35e0f8adc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Tue, 2 Dec 2025 13:53:50 -0300 Subject: [PATCH 45/68] chore: extract string resources --- app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt | 3 ++- .../bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 4 ++-- .../to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt | 9 +++++++-- app/src/main/res/values/strings.xml | 5 +++++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt index 791b4ae61..e91c1d326 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -143,7 +144,7 @@ fun QrCodeImage( } if (bitmap == null) { - CaptionB("Generating QR ...", color = Colors.Black) + CaptionB(stringResource(R.string.wallet__receive_qr_generating), color = Colors.Black) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 261cefe21..081fd5579 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -377,10 +377,10 @@ fun CjitOnBoardingView(modifier: Modifier = Modifier) { .padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Display("Receive on spending balance".withAccent(accentColor = Colors.Purple)) + Display(stringResource(R.string.wallet__receive_onboarding_title).withAccent(accentColor = Colors.Purple)) VerticalSpacer(8.dp) BodyM( - "Enjoy instant and cheap\ntransactions with friends, family,\nand merchants.", + stringResource(R.string.wallet__receive_onboarding_description), color = Colors.White64, modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index b145251c4..71b27d3b2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -11,11 +11,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import kotlinx.serialization.Serializable +import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.repositories.LightningState import to.bitkit.ui.screens.wallets.send.AddTagScreen @@ -45,6 +47,9 @@ fun ReceiveSheet( val cjitEntryDetails = remember { mutableStateOf(null) } val lightningState: LightningState by wallet.lightningState.collectAsStateWithLifecycle() + val geoBlockedTitle = stringResource(R.string.wallet__receive_geo_blocked_title) + val geoBlockedDescription = stringResource(R.string.wallet__receive_geo_blocked_description) + LaunchedEffect(Unit) { wallet.resetPreActivityMetadataTagsForCurrentInvoice() wallet.refreshReceiveState() @@ -84,8 +89,8 @@ fun ReceiveSheet( toast( Toast( type = Toast.ToastType.ERROR, - title = "Instant Payments Unavailable", - description = "Bitkit does not provide Lightning services in your country, but you can still connect to other nodes.", + title = geoBlockedTitle, + description = geoBlockedDescription, autoHide = true, ) ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 941509368..a3a42c6a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -983,6 +983,11 @@ Failed to send funds to your spending account. You will receive Spending Balance Initializing... + Receive on <accent>spending balance</accent> + Enjoy instant and cheap\ntransactions with friends, family,\nand merchants. + Generating QR ... + Instant Payments Unavailable + Bitkit does not provide Lightning services in your country, but you can still connect to other nodes. MINIMUM Activity Show All Activity From 74aba7452a84ee01eedaa1415a1bb0a6b4cd3d65 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 07:24:19 -0300 Subject: [PATCH 46/68] fix: keep bip 21 onchain info to onchain only payments --- .../wallets/receive/ReceiveInvoiceUtils.kt | 22 ++- .../receive/ReceiveInvoiceUtilsTest.kt | 169 ++++++++++++++++++ 2 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index 562d375cb..76ee81020 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -22,7 +22,8 @@ fun getInvoiceForTab( ): String { return when (tab) { ReceiveTab.SAVINGS -> { - onchainAddress + // Return BIP21 without lightning parameter to preserve amount and other parameters + removeLightningFromBip21(bip21, onchainAddress) } ReceiveTab.AUTO -> { @@ -37,6 +38,25 @@ fun getInvoiceForTab( } } +/** + * Removes the lightning parameter from a BIP21 URI while preserving all other parameters. + * + * @param bip21 Full BIP21 URI (e.g., bitcoin:address?amount=0.001&lightning=lnbc...) + * @param fallbackAddress Fallback address if BIP21 is empty or invalid + * @return BIP21 URI without the lightning parameter (e.g., bitcoin:address?amount=0.001) + */ +private fun removeLightningFromBip21(bip21: String, fallbackAddress: String): String { + if (bip21.isBlank()) return fallbackAddress + + // Remove lightning parameter using regex + // Handles both "?lightning=..." and "&lightning=..." cases + val withoutLightning = bip21 + .replace(Regex("[?&]lightning=[^&]*"), "") + .replace(Regex("\\?$"), "") // Remove trailing ? if it's the last char + + return withoutLightning.ifBlank { fallbackAddress } +} + /** * Returns the appropriate QR code logo resource for the selected tab. * diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt new file mode 100644 index 000000000..73859869e --- /dev/null +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt @@ -0,0 +1,169 @@ +package to.bitkit.ui.screens.wallets.receive + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ReceiveInvoiceUtilsTest { + + private val testAddress = "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq" + private val testBolt11 = "lnbc1500n1pn2s39xpp5wyxw0e9fvvf..." + private val testCjitInvoice = "lnbc2000n1pn2s39xpp5zyxw0e9fvvf..." + + @Test + fun `getInvoiceForTab SAVINGS returns BIP21 without lightning parameter`() { + val bip21WithAmount = "bitcoin:$testAddress?amount=0.001&message=Test&lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.SAVINGS, + bip21 = bip21WithAmount, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals("bitcoin:$testAddress?amount=0.001&message=Test", result) + } + + @Test + fun `getInvoiceForTab SAVINGS preserves amount when lightning is last parameter`() { + val bip21 = "bitcoin:$testAddress?amount=0.00050000&lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.SAVINGS, + bip21 = bip21, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals("bitcoin:$testAddress?amount=0.00050000", result) + } + + @Test + fun `getInvoiceForTab SAVINGS handles BIP21 without lightning parameter`() { + val bip21WithoutLightning = "bitcoin:$testAddress?amount=0.002&message=Test" + + val result = getInvoiceForTab( + tab = ReceiveTab.SAVINGS, + bip21 = bip21WithoutLightning, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals("bitcoin:$testAddress?amount=0.002&message=Test", result) + } + + @Test + fun `getInvoiceForTab SAVINGS returns fallback address when BIP21 is empty`() { + val result = getInvoiceForTab( + tab = ReceiveTab.SAVINGS, + bip21 = "", + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals(testAddress, result) + } + + @Test + fun `getInvoiceForTab SAVINGS returns fallback when BIP21 only has lightning`() { + val bip21OnlyLightning = "bitcoin:$testAddress?lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.SAVINGS, + bip21 = bip21OnlyLightning, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals("bitcoin:$testAddress", result) + } + + @Test + fun `getInvoiceForTab AUTO returns full BIP21 when node is running`() { + val bip21 = "bitcoin:$testAddress?amount=0.001&lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.AUTO, + bip21 = bip21, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals(bip21, result) + } + + @Test + fun `getInvoiceForTab AUTO returns empty when node is not running`() { + val bip21 = "bitcoin:$testAddress?amount=0.001&lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.AUTO, + bip21 = bip21, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = false, + onchainAddress = testAddress + ) + + assertEquals("", result) + } + + @Test + fun `getInvoiceForTab SPENDING returns CJIT invoice when available and node running`() { + val bip21 = "bitcoin:$testAddress?lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.SPENDING, + bip21 = bip21, + bolt11 = testBolt11, + cjitInvoice = testCjitInvoice, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals(testCjitInvoice, result) + } + + @Test + fun `getInvoiceForTab SPENDING returns bolt11 when CJIT unavailable`() { + val bip21 = "bitcoin:$testAddress?lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.SPENDING, + bip21 = bip21, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals(testBolt11, result) + } + + @Test + fun `getInvoiceForTab SPENDING returns bolt11 when node not running even with CJIT`() { + val bip21 = "bitcoin:$testAddress?lightning=$testBolt11" + + val result = getInvoiceForTab( + tab = ReceiveTab.SPENDING, + bip21 = bip21, + bolt11 = testBolt11, + cjitInvoice = testCjitInvoice, + isNodeRunning = false, + onchainAddress = testAddress + ) + + assertEquals(testBolt11, result) + } +} From cb641aeb8ca31e481e6ef66e14fc8412ee507840 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 07:37:24 -0300 Subject: [PATCH 47/68] fix: display only address on details --- .../wallets/receive/ReceiveInvoiceUtils.kt | 2 +- .../wallets/receive/ReceiveQrScreen.kt | 39 ++++++++++++------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index 76ee81020..9d8445d39 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -45,7 +45,7 @@ fun getInvoiceForTab( * @param fallbackAddress Fallback address if BIP21 is empty or invalid * @return BIP21 URI without the lightning parameter (e.g., bitcoin:address?amount=0.001) */ -private fun removeLightningFromBip21(bip21: String, fallbackAddress: String): String { +fun removeLightningFromBip21(bip21: String, fallbackAddress: String): String { if (bip21.isBlank()) return fallbackAddress // Remove lightning parameter using regex diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 081fd5579..71dce4312 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -193,8 +193,7 @@ fun ReceiveQrScreen( showDetails -> { ReceiveDetailsView( tab = selectedTab, - onchainAddress = walletState.onchainAddress, - bolt11 = walletState.bolt11, + walletState = walletState, cjitInvoice = cjitInvoice, modifier = Modifier.weight(1f) ) @@ -414,8 +413,7 @@ fun CjitOnBoardingView(modifier: Modifier = Modifier) { @Composable private fun ReceiveDetailsView( tab: ReceiveTab, - onchainAddress: String, - bolt11: String, + walletState: MainUiState, cjitInvoice: String?, modifier: Modifier = Modifier, ) { @@ -427,10 +425,14 @@ private fun ReceiveDetailsView( Column { when (tab) { ReceiveTab.SAVINGS -> { - if (onchainAddress.isNotEmpty()) { + if (walletState.onchainAddress.isNotEmpty()) { CopyAddressCard( title = stringResource(R.string.wallet__receive_bitcoin_invoice), - address = onchainAddress, + address = removeLightningFromBip21( + bip21 = walletState.bip21, + fallbackAddress = walletState.onchainAddress + ), + body = walletState.onchainAddress, type = CopyAddressType.ONCHAIN, testTag = "ReceiveOnchainAddress", ) @@ -439,18 +441,22 @@ private fun ReceiveDetailsView( ReceiveTab.AUTO -> { // Show both onchain AND lightning if available - if (onchainAddress.isNotEmpty()) { + if (walletState.onchainAddress.isNotEmpty()) { CopyAddressCard( title = stringResource(R.string.wallet__receive_bitcoin_invoice), - address = onchainAddress, + address = removeLightningFromBip21( + bip21 = walletState.bip21, + fallbackAddress = walletState.onchainAddress + ), + body = walletState.onchainAddress, type = CopyAddressType.ONCHAIN, testTag = "ReceiveOnchainAddress", ) } - if (cjitInvoice != null || bolt11.isNotEmpty()) { + if (cjitInvoice != null || walletState.bolt11.isNotEmpty()) { CopyAddressCard( title = stringResource(R.string.wallet__receive_lightning_invoice), - address = cjitInvoice ?: bolt11, + address = cjitInvoice ?: walletState.bolt11, type = CopyAddressType.LIGHTNING, testTag = "ReceiveLightningAddress", ) @@ -458,10 +464,10 @@ private fun ReceiveDetailsView( } ReceiveTab.SPENDING -> { - if (cjitInvoice != null || bolt11.isNotEmpty()) { + if (cjitInvoice != null || walletState.bolt11.isNotEmpty()) { CopyAddressCard( title = stringResource(R.string.wallet__receive_lightning_invoice), - address = cjitInvoice ?: bolt11, + address = cjitInvoice ?: walletState.bolt11, type = CopyAddressType.LIGHTNING, testTag = "ReceiveLightningAddress", ) @@ -478,6 +484,7 @@ enum class CopyAddressType { ONCHAIN, LIGHTNING } @Composable private fun CopyAddressCard( title: String, + body: String? = null, address: String, type: CopyAddressType, testTag: String? = null, @@ -502,7 +509,7 @@ private fun CopyAddressCard( } Spacer(modifier = Modifier.height(16.dp)) BodyS( - text = address.truncate(32).uppercase(), + text = (body ?: address).truncate(32).uppercase(), modifier = testTag?.let { Modifier.testTag(it) } ?: Modifier ) Spacer(modifier = Modifier.height(16.dp)) @@ -751,8 +758,10 @@ private fun PreviewDetailsMode() { ) { ReceiveDetailsView( tab = ReceiveTab.AUTO, - onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", - bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", + walletState = MainUiState( + onchainAddress = "bcrt1qfserxgtuesul4m9zva56wzk849yf9l8rk4qy0l", + bolt11 = "lnbcrt500u1pn7umn7pp5x0s9lt9fwrff6rp70pz3guwnjgw97sjuv79...", + ), cjitInvoice = null, modifier = Modifier.weight(1f) ) From f0de4de2cf030a7f25e0f895c74141a23b5d273f Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 08:57:03 -0300 Subject: [PATCH 48/68] refactor: replace qr animation with HorizontalPager --- .../components/CustomTabRowWithSpacing.kt | 11 +- .../wallets/receive/ReceiveQrScreen.kt | 171 +++++++++++++----- .../animations/TabTransitionAnimations.kt | 70 +++++++ .../modifiers/SwipeToChangeTabModifier.kt | 54 ++++-- 4 files changed, 243 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/shared/animations/TabTransitionAnimations.kt diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index 97d003fc2..722284385 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -61,16 +61,21 @@ fun CustomTabRowWithSpacing( ) } - // Animated indicator val animatedAlpha by animateFloatAsState( targetValue = if (isSelected) 1f else 0.2f, - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + animationSpec = tween( + durationMillis = 250, + easing = FastOutSlowInEasing + ), label = "indicatorAlpha" ) val animatedColor by animateColorAsState( targetValue = if (isSelected) selectedColor else Colors.White, - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), + animationSpec = tween( + durationMillis = 250, + easing = FastOutSlowInEasing + ), label = "indicatorColor" ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 71dce4312..b9a211b7f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -1,11 +1,16 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -16,6 +21,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -23,7 +30,9 @@ import androidx.compose.material3.Icon import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -32,8 +41,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.keepScreenOn import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -58,9 +69,9 @@ import to.bitkit.ui.components.Tooltip import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing +import to.bitkit.ui.shared.animations.TabTransitionAnimations import to.bitkit.ui.shared.effects.SetMaxBrightness import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.modifiers.swipeToChangeTab import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.shareQrCode import to.bitkit.ui.shared.util.shareText @@ -81,6 +92,7 @@ fun ReceiveQrScreen( ) { SetMaxBrightness() + val haptic = LocalHapticFeedback.current val hasUsableChannels = walletState.channels.any { it.isUsable } // Tab selection state @@ -110,6 +122,51 @@ fun ReceiveQrScreen( visibleTabs.indexOf(selectedTab) } + // HorizontalPager state + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = currentTabIndex.coerceAtLeast(0), + pageCount = { visibleTabs.size } + ) + + // Sync: Pager swipes → update selectedTab + LaunchedEffect(pagerState.currentPage) { + val newTab = visibleTabs.getOrNull(pagerState.currentPage) + if (newTab != null && newTab != selectedTab) { + selectedTab = newTab + } + } + + // Sync: Validate pager position when tabs change + LaunchedEffect(visibleTabs) { + if (pagerState.currentPage >= visibleTabs.size) { + pagerState.animateScrollToPage(visibleTabs.size - 1) + } + } + + // Sync: selectedTab changes → scroll pager + LaunchedEffect(selectedTab, visibleTabs) { + val newIndex = visibleTabs.indexOf(selectedTab) + if (newIndex >= 0 && newIndex != pagerState.currentPage) { + pagerState.animateScrollToPage(newIndex) + } + } + + // Track previous tab to determine animation direction + var previousTabIndex by remember { mutableIntStateOf(currentTabIndex) } + + // Derive animation direction based on tab index change + val isForward by remember { + derivedStateOf { + currentTabIndex > previousTabIndex + } + } + + // Update previous index when current changes + LaunchedEffect(currentTabIndex) { + previousTabIndex = currentTabIndex + } + val showingCjitOnboarding = remember(selectedTab, walletState, cjitInvoice) { selectedTab == ReceiveTab.SPENDING && !hasUsableChannels && @@ -117,13 +174,6 @@ fun ReceiveQrScreen( cjitInvoice.isNullOrEmpty() } - // Auto-correct selected tab if it becomes hidden - LaunchedEffect(visibleTabs) { - if (selectedTab !in visibleTabs) { - selectedTab = visibleTabs.first() - } - } - // Current invoice for display val currentInvoice = remember(selectedTab, walletState, cjitInvoice) { getInvoiceForTab( @@ -157,60 +207,87 @@ fun ReceiveQrScreen( // Tab row CustomTabRowWithSpacing( tabs = visibleTabs, - currentTabIndex = currentTabIndex, + currentTabIndex = pagerState.currentPage, selectedColor = when (selectedTab) { ReceiveTab.SAVINGS -> Colors.Brand ReceiveTab.AUTO -> Colors.White ReceiveTab.SPENDING -> Colors.Purple }, onTabChange = { tab -> + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) selectedTab = tab + val newIndex = visibleTabs.indexOf(tab) + scope.launch { pagerState.animateScrollToPage(newIndex) } } ) Spacer(Modifier.height(24.dp)) - // Content area (QR or Details) - Column( - horizontalAlignment = Alignment.CenterHorizontally, + // Content area (QR or Details) with HorizontalPager + HorizontalPager( + state = pagerState, + pageSpacing = 8.dp, modifier = Modifier .weight(1f) - .swipeToChangeTab( - currentTabIndex = currentTabIndex, - tabCount = visibleTabs.size, - onTabChange = { newIndex -> - selectedTab = visibleTabs[newIndex] - } - ) - ) { - when { - showingCjitOnboarding -> { - CjitOnBoardingView( - modifier = Modifier.weight(1f) - ) - } - - showDetails -> { - ReceiveDetailsView( - tab = selectedTab, - walletState = walletState, - cjitInvoice = cjitInvoice, - modifier = Modifier.weight(1f) - ) - } - - else -> { - ReceiveQrView( - uri = currentInvoice, - qrLogoPainter = painterResource(qrLogoRes), - onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { - onClickEditInvoice + .fillMaxWidth() + ) { page -> + val tab = visibleTabs[page] + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + // Animated content switcher with direction-aware transitions + AnimatedContent( + targetState = Triple(tab, showDetails, showingCjitOnboarding), + transitionSpec = { + // Only animate tab changes, not showDetails toggle + if (targetState.first != initialState.first) { + TabTransitionAnimations.tabContentTransition(isForward) } else { - onClickReceiveCjit - }, - tab = selectedTab, - modifier = Modifier.fillMaxWidth() - ) + // Instant transition for showDetails toggle + ContentTransform( + targetContentEnter = EnterTransition.None, + initialContentExit = ExitTransition.None + ) + } + }, + contentKey = { (currentTab, details, onboarding) -> + // Use tab + state as key for proper animation + "$currentTab-$details-$onboarding" + }, + label = "ReceiveTabContent" + ) { (targetTab, targetShowDetails, targetShowingCjitOnboarding) -> + when { + targetShowingCjitOnboarding -> { + CjitOnBoardingView( + modifier = Modifier.weight(1f) + ) + } + + targetShowDetails -> { + ReceiveDetailsView( + tab = targetTab, + walletState = walletState, + cjitInvoice = cjitInvoice, + modifier = Modifier.weight(1f) + ) + } + + else -> { + ReceiveQrView( + uri = currentInvoice, + qrLogoPainter = painterResource(qrLogoRes), + onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { + onClickEditInvoice + } else { + onClickReceiveCjit + }, + tab = targetTab, + modifier = Modifier.fillMaxWidth() + ) + } + } } } } diff --git a/app/src/main/java/to/bitkit/ui/shared/animations/TabTransitionAnimations.kt b/app/src/main/java/to/bitkit/ui/shared/animations/TabTransitionAnimations.kt new file mode 100644 index 000000000..3e2cc32a6 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/shared/animations/TabTransitionAnimations.kt @@ -0,0 +1,70 @@ +package to.bitkit.ui.shared.animations + +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.ui.unit.IntOffset + +/** + * Animation specifications for tab transitions with iOS-like smooth feel. + * + * - Tween animation: 450ms duration with FastOutSlowInEasing + * - Direction-aware horizontal sliding + * - Smooth, natural feel matching iOS PageTabViewStyle + */ +object TabTransitionAnimations { + + /** + * Tween animation for smooth tab content transitions. + * - Duration: 300ms (matches iOS native tab transitions) + * - Easing: FastOutSlowInEasing (iOS-like deceleration curve) + */ + private val tabTweenSpec = tween( + durationMillis = 300, + easing = FastOutSlowInEasing + ) + + /** + * Tween animation for IntOffset (horizontal sliding). + */ + private val tabTweenSpecIntOffset = tween( + durationMillis = 300, + easing = FastOutSlowInEasing + ) + + /** + * Direction-aware tab content transition. + * + * @param isForward true if moving to next tab (swipe left), false if previous (swipe right) + */ + fun tabContentTransition(isForward: Boolean): ContentTransform { + val slideInOffset = if (isForward) { + { fullWidth: Int -> fullWidth } // Slide in from right + } else { + { fullWidth: Int -> -fullWidth } // Slide in from left + } + + val slideOutOffset = if (isForward) { + { fullWidth: Int -> -fullWidth / 5 } // Slide out to left (20% parallax) + } else { + { fullWidth: Int -> fullWidth / 5 } // Slide out to right (20% parallax) + } + + return slideInHorizontally( + initialOffsetX = slideInOffset, + animationSpec = tabTweenSpecIntOffset + ) + fadeIn( + animationSpec = tabTweenSpec + ) togetherWith slideOutHorizontally( + targetOffsetX = slideOutOffset, + animationSpec = tabTweenSpecIntOffset + ) + fadeOut( + animationSpec = tabTweenSpec + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt index c050e777e..82c6bd1c8 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/SwipeToChangeTabModifier.kt @@ -1,54 +1,82 @@ package to.bitkit.ui.shared.modifiers import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import kotlin.math.abs /** * Enables tab navigation via horizontal swipe gestures. * - * Detects horizontal swipe velocity and navigates to adjacent tabs when the velocity - * exceeds the specified threshold. Provides boundary protection to prevent navigation - * beyond the first and last tabs. + * Detects horizontal swipe using both velocity and drag distance to provide + * iOS-like swipe sensitivity. Navigates to adjacent tabs when either: + * - Velocity exceeds threshold (for quick flicks) + * - Drag distance exceeds threshold (for slower, deliberate swipes) * * @param currentTabIndex The currently selected tab index (0-based) * @param tabCount Total number of tabs available for navigation * @param onTabChange Callback invoked when user swipes to change tabs, receives the new tab index - * @param threshold Velocity threshold in pixels per second (default: 1500f) - * Swipe velocity must exceed this value to trigger navigation + * @param velocityThreshold Velocity threshold in px/s (default: 600f) + * @param distanceThreshold Distance threshold in dp (default: 50.dp) */ fun Modifier.swipeToChangeTab( currentTabIndex: Int, tabCount: Int, onTabChange: (Int) -> Unit, - threshold: Float = DEFAULT_SWIPE_THRESHOLD, + velocityThreshold: Float = DEFAULT_VELOCITY_THRESHOLD, + distanceThreshold: Float = DEFAULT_DISTANCE_THRESHOLD_DP, ): Modifier = composed { val velocityTracker = remember { VelocityTracker() } + var totalDragDistance by remember { mutableFloatStateOf(0f) } + val distanceThresholdPx = with(LocalDensity.current) { distanceThreshold.dp.toPx() } pointerInput(currentTabIndex) { detectHorizontalDragGestures( - onHorizontalDrag = { change, _ -> + onHorizontalDrag = { change, dragAmount -> velocityTracker.addPosition(change.uptimeMillis, change.position) + totalDragDistance += dragAmount }, onDragEnd = { val velocity = velocityTracker.calculateVelocity().x - when { - velocity >= threshold && currentTabIndex > 0 -> - onTabChange(currentTabIndex - 1) + val dragDistance = totalDragDistance - velocity <= -threshold && currentTabIndex < tabCount - 1 -> - onTabChange(currentTabIndex + 1) + // Check if either velocity OR distance threshold is met + val shouldNavigate = abs(velocity) >= velocityThreshold || + abs(dragDistance) >= distanceThresholdPx + + if (shouldNavigate) { + when { + // Swipe right (previous tab) - positive velocity/drag + (velocity > 0 || dragDistance > 0) && currentTabIndex > 0 -> + onTabChange(currentTabIndex - 1) + + // Swipe left (next tab) - negative velocity/drag + (velocity < 0 || dragDistance < 0) && currentTabIndex < tabCount - 1 -> + onTabChange(currentTabIndex + 1) + } } + velocityTracker.resetTracking() + totalDragDistance = 0f }, onDragCancel = { velocityTracker.resetTracking() + totalDragDistance = 0f }, ) } } -private const val DEFAULT_SWIPE_THRESHOLD = 1500f +// Reduced from 1500 to 600 for better iOS-like sensitivity +private const val DEFAULT_VELOCITY_THRESHOLD = 600f + +// Added distance threshold: 50dp drag triggers navigation +private const val DEFAULT_DISTANCE_THRESHOLD_DP = 50f From 0adec7887aac914b7958855f7560d513c19048b7 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 09:08:38 -0300 Subject: [PATCH 49/68] chore: lint --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index b9a211b7f..181015975 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -561,9 +560,9 @@ enum class CopyAddressType { ONCHAIN, LIGHTNING } @Composable private fun CopyAddressCard( title: String, - body: String? = null, address: String, type: CopyAddressType, + body: String? = null, testTag: String? = null, ) { val context = LocalContext.current From 2baacf303fcb173087aa9e7f3b0a7bbeea553eff Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 09:21:30 -0300 Subject: [PATCH 50/68] fix: page spacing --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 181015975..0a82ada29 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -225,7 +225,7 @@ fun ReceiveQrScreen( // Content area (QR or Details) with HorizontalPager HorizontalPager( state = pagerState, - pageSpacing = 8.dp, + pageSpacing = 16.dp, modifier = Modifier .weight(1f) .fillMaxWidth() From 1feffcb74e508fd13e5f48b981f947a7f5f1d791 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 11:04:23 -0300 Subject: [PATCH 51/68] chore: restore geoblocked navigation --- app/src/main/java/to/bitkit/ui/ContentView.kt | 3 --- .../ui/screens/wallets/receive/ReceiveSheet.kt | 16 +--------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 8a33ec30c..873ef5a77 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -388,9 +388,6 @@ fun ContentView( navigateToExternalConnection = { navController.navigate(ExternalConnection()) appViewModel.hideSheet() - }, - toast = { toast -> - appViewModel.toast(toast) } ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index 71b27d3b2..2027c58de 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -11,14 +11,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import kotlinx.serialization.Serializable -import to.bitkit.R -import to.bitkit.models.Toast import to.bitkit.repositories.LightningState import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.shared.modifiers.sheetHeight @@ -34,7 +31,6 @@ import to.bitkit.viewmodels.WalletViewModelEffects fun ReceiveSheet( navigateToExternalConnection: () -> Unit, walletState: MainUiState, - toast: (Toast) -> Unit, editInvoiceAmountViewModel: AmountInputViewModel = hiltViewModel(), settingsViewModel: SettingsViewModel = hiltViewModel(), ) { @@ -47,9 +43,6 @@ fun ReceiveSheet( val cjitEntryDetails = remember { mutableStateOf(null) } val lightningState: LightningState by wallet.lightningState.collectAsStateWithLifecycle() - val geoBlockedTitle = stringResource(R.string.wallet__receive_geo_blocked_title) - val geoBlockedDescription = stringResource(R.string.wallet__receive_geo_blocked_description) - LaunchedEffect(Unit) { wallet.resetPreActivityMetadataTagsForCurrentInvoice() wallet.refreshReceiveState() @@ -86,14 +79,7 @@ fun ReceiveSheet( walletState = walletState, onClickReceiveCjit = { if (lightningState.isGeoBlocked) { - toast( - Toast( - type = Toast.ToastType.ERROR, - title = geoBlockedTitle, - description = geoBlockedDescription, - autoHide = true, - ) - ) + navController.navigate(ReceiveRoute.GeoBlock) } else { showCreateCjit.value = true navController.navigate(ReceiveRoute.Amount) From 03088e48a2a5b9445d58c83aadf552a51ca558bc Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 11:26:06 -0300 Subject: [PATCH 52/68] fix: remove ripple from tabs --- .../wallets/activity/components/CustomTabRowWithSpacing.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt index 722284385..9f90a8739 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/CustomTabRowWithSpacing.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,6 +23,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import to.bitkit.ui.components.CaptionB +import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.Colors @Composable @@ -49,7 +49,7 @@ fun CustomTabRowWithSpacing( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() - .clickable { onTabChange(tab) } + .clickableAlpha { onTabChange(tab) } .padding(vertical = 8.dp) .testTag("Tab-${tab.name.lowercase()}"), ) { From 591d70e3220f2f294620933837dd4d400cd62b65 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 11:43:18 -0300 Subject: [PATCH 53/68] fix: cache invoice --- .../wallets/receive/ReceiveQrScreen.kt | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 0a82ada29..a39731fc1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -117,6 +117,19 @@ fun ReceiveQrScreen( } } + val invoicesByTab = remember(visibleTabs, walletState, cjitInvoice) { + visibleTabs.associateWith { tab -> + getInvoiceForTab( + tab = tab, + bip21 = walletState.bip21, + bolt11 = walletState.bolt11, + cjitInvoice = cjitInvoice, + isNodeRunning = walletState.nodeLifecycleState.isRunning(), + onchainAddress = walletState.onchainAddress + ) + } + } + val currentTabIndex = remember(selectedTab, visibleTabs) { visibleTabs.indexOf(selectedTab) } @@ -154,14 +167,10 @@ fun ReceiveQrScreen( // Track previous tab to determine animation direction var previousTabIndex by remember { mutableIntStateOf(currentTabIndex) } - // Derive animation direction based on tab index change val isForward by remember { - derivedStateOf { - currentTabIndex > previousTabIndex - } + derivedStateOf { currentTabIndex > previousTabIndex } } - // Update previous index when current changes LaunchedEffect(currentTabIndex) { previousTabIndex = currentTabIndex } @@ -173,18 +182,6 @@ fun ReceiveQrScreen( cjitInvoice.isNullOrEmpty() } - // Current invoice for display - val currentInvoice = remember(selectedTab, walletState, cjitInvoice) { - getInvoiceForTab( - tab = selectedTab, - bip21 = walletState.bip21, - bolt11 = walletState.bolt11, - cjitInvoice = cjitInvoice, - isNodeRunning = walletState.nodeLifecycleState.isRunning(), - onchainAddress = walletState.onchainAddress - ) - } - // QR logo based on selected tab val qrLogoRes = remember(selectedTab) { getQrLogoResource(selectedTab) @@ -236,15 +233,12 @@ fun ReceiveQrScreen( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize() ) { - // Animated content switcher with direction-aware transitions AnimatedContent( targetState = Triple(tab, showDetails, showingCjitOnboarding), transitionSpec = { - // Only animate tab changes, not showDetails toggle if (targetState.first != initialState.first) { TabTransitionAnimations.tabContentTransition(isForward) } else { - // Instant transition for showDetails toggle ContentTransform( targetContentEnter = EnterTransition.None, initialContentExit = ExitTransition.None @@ -252,7 +246,6 @@ fun ReceiveQrScreen( } }, contentKey = { (currentTab, details, onboarding) -> - // Use tab + state as key for proper animation "$currentTab-$details-$onboarding" }, label = "ReceiveTabContent" @@ -274,8 +267,10 @@ fun ReceiveQrScreen( } else -> { + val cachedInvoice = invoicesByTab[targetTab].orEmpty() + ReceiveQrView( - uri = currentInvoice, + uri = cachedInvoice, qrLogoPainter = painterResource(qrLogoRes), onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { onClickEditInvoice From 851200cf35f54a6e536f760e67973864480d756d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 11:46:00 -0300 Subject: [PATCH 54/68] fix: detail view weight --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index a39731fc1..b09de8542 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -248,7 +248,8 @@ fun ReceiveQrScreen( contentKey = { (currentTab, details, onboarding) -> "$currentTab-$details-$onboarding" }, - label = "ReceiveTabContent" + label = "ReceiveTabContent", + modifier = Modifier.weight(1f) ) { (targetTab, targetShowDetails, targetShowingCjitOnboarding) -> when { targetShowingCjitOnboarding -> { From 0ecc0d723c9cb078555abbaf38db3a9688e85b40 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 11:55:36 -0300 Subject: [PATCH 55/68] fix: restore circular indicator --- .../to/bitkit/ui/components/QrCodeImage.kt | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt index e91c1d326..66e46fef1 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable @@ -124,28 +125,28 @@ fun QrCodeImage( } } - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally - ) { - logoPainter?.let { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(68.dp) - .background(Color.White, shape = CircleShape) - ) { - Image( - painter = it, - contentDescription = null, - modifier = Modifier.size(50.dp) - ) - } + logoPainter?.let { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(68.dp) + .background(Color.White, shape = CircleShape) + .align(Alignment.Center) + ) { + Image( + painter = it, + contentDescription = null, + modifier = Modifier.size(50.dp) + ) } + } - if (bitmap == null) { - CaptionB(stringResource(R.string.wallet__receive_qr_generating), color = Colors.Black) - } + if (bitmap == null) { + CircularProgressIndicator( + color = Colors.Black, + strokeWidth = 4.dp, + modifier = Modifier.size(68.dp) + ) } } } From ed5a9073aee151b2636a593a4745fb2085fdb7ab Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 13:43:23 -0300 Subject: [PATCH 56/68] refactor: replace horizontal pager wit lazy row --- .../wallets/receive/ReceiveQrScreen.kt | 228 +++++++----------- 1 file changed, 90 insertions(+), 138 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index b09de8542..739f29266 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -1,11 +1,7 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -20,18 +16,18 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -68,7 +64,6 @@ import to.bitkit.ui.components.Tooltip import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing -import to.bitkit.ui.shared.animations.TabTransitionAnimations import to.bitkit.ui.shared.effects.SetMaxBrightness import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground @@ -94,17 +89,6 @@ fun ReceiveQrScreen( val haptic = LocalHapticFeedback.current val hasUsableChannels = walletState.channels.any { it.isUsable } - // Tab selection state - var selectedTab by remember { - mutableStateOf( - initialTab ?: when { - !cjitInvoice.isNullOrEmpty() -> ReceiveTab.SPENDING - hasUsableChannels -> ReceiveTab.AUTO - else -> ReceiveTab.SAVINGS - } - ) - } - var showDetails by remember { mutableStateOf(false) } val visibleTabs = remember(hasUsableChannels) { @@ -117,76 +101,44 @@ fun ReceiveQrScreen( } } - val invoicesByTab = remember(visibleTabs, walletState, cjitInvoice) { - visibleTabs.associateWith { tab -> - getInvoiceForTab( - tab = tab, - bip21 = walletState.bip21, - bolt11 = walletState.bolt11, - cjitInvoice = cjitInvoice, - isNodeRunning = walletState.nodeLifecycleState.isRunning(), - onchainAddress = walletState.onchainAddress - ) + // Determine initial tab index + val initialTabIndex = remember(initialTab, visibleTabs) { + if (initialTab != null) { + visibleTabs.indexOf(initialTab).coerceAtLeast(0) + } else { + when { + !cjitInvoice.isNullOrEmpty() -> visibleTabs.indexOf(ReceiveTab.SPENDING) + hasUsableChannels -> visibleTabs.indexOf(ReceiveTab.AUTO) + else -> visibleTabs.indexOf(ReceiveTab.SAVINGS) + }.coerceAtLeast(0) } } - val currentTabIndex = remember(selectedTab, visibleTabs) { - visibleTabs.indexOf(selectedTab) - } - - // HorizontalPager state + // LazyRow state with snap behavior val scope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = currentTabIndex.coerceAtLeast(0), - pageCount = { visibleTabs.size } + val lazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = initialTabIndex ) - // Sync: Pager swipes → update selectedTab - LaunchedEffect(pagerState.currentPage) { - val newTab = visibleTabs.getOrNull(pagerState.currentPage) - if (newTab != null && newTab != selectedTab) { - selectedTab = newTab - } - } - - // Sync: Validate pager position when tabs change - LaunchedEffect(visibleTabs) { - if (pagerState.currentPage >= visibleTabs.size) { - pagerState.animateScrollToPage(visibleTabs.size - 1) - } - } - - // Sync: selectedTab changes → scroll pager - LaunchedEffect(selectedTab, visibleTabs) { - val newIndex = visibleTabs.indexOf(selectedTab) - if (newIndex >= 0 && newIndex != pagerState.currentPage) { - pagerState.animateScrollToPage(newIndex) - } - } - - // Track previous tab to determine animation direction - var previousTabIndex by remember { mutableIntStateOf(currentTabIndex) } - - val isForward by remember { - derivedStateOf { currentTabIndex > previousTabIndex } - } + val snapBehavior = rememberSnapFlingBehavior( + lazyListState = lazyListState, + snapPosition = SnapPosition.Center + ) - LaunchedEffect(currentTabIndex) { - previousTabIndex = currentTabIndex + // Derive selectedTab from scroll position + val selectedTab = remember(lazyListState.firstVisibleItemIndex, visibleTabs) { + visibleTabs.getOrNull(lazyListState.firstVisibleItemIndex) + ?: visibleTabs.firstOrNull() + ?: ReceiveTab.SAVINGS } - val showingCjitOnboarding = remember(selectedTab, walletState, cjitInvoice) { + val showingCjitOnboarding = remember(selectedTab, walletState, cjitInvoice, hasUsableChannels) { selectedTab == ReceiveTab.SPENDING && !hasUsableChannels && walletState.nodeLifecycleState.isRunning() && cjitInvoice.isNullOrEmpty() } - // QR logo based on selected tab - val qrLogoRes = remember(selectedTab) { - getQrLogoResource(selectedTab) - } - Column( modifier = modifier .fillMaxSize() @@ -203,7 +155,7 @@ fun ReceiveQrScreen( // Tab row CustomTabRowWithSpacing( tabs = visibleTabs, - currentTabIndex = pagerState.currentPage, + currentTabIndex = lazyListState.firstVisibleItemIndex, selectedColor = when (selectedTab) { ReceiveTab.SAVINGS -> Colors.Brand ReceiveTab.AUTO -> Colors.White @@ -211,76 +163,76 @@ fun ReceiveQrScreen( }, onTabChange = { tab -> haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) - selectedTab = tab val newIndex = visibleTabs.indexOf(tab) - scope.launch { pagerState.animateScrollToPage(newIndex) } + scope.launch { + lazyListState.animateScrollToItem(newIndex) + } } ) Spacer(Modifier.height(24.dp)) - // Content area (QR or Details) with HorizontalPager - HorizontalPager( - state = pagerState, - pageSpacing = 16.dp, + // Content area (QR or Details) with LazyRow + LazyRow( + state = lazyListState, + flingBehavior = snapBehavior, + horizontalArrangement = Arrangement.spacedBy(0.dp), + userScrollEnabled = true, modifier = Modifier .weight(1f) .fillMaxWidth() - ) { page -> - val tab = visibleTabs[page] - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - AnimatedContent( - targetState = Triple(tab, showDetails, showingCjitOnboarding), - transitionSpec = { - if (targetState.first != initialState.first) { - TabTransitionAnimations.tabContentTransition(isForward) - } else { - ContentTransform( - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None - ) - } - }, - contentKey = { (currentTab, details, onboarding) -> - "$currentTab-$details-$onboarding" - }, - label = "ReceiveTabContent", - modifier = Modifier.weight(1f) - ) { (targetTab, targetShowDetails, targetShowingCjitOnboarding) -> - when { - targetShowingCjitOnboarding -> { - CjitOnBoardingView( - modifier = Modifier.weight(1f) - ) - } - - targetShowDetails -> { - ReceiveDetailsView( - tab = targetTab, - walletState = walletState, - cjitInvoice = cjitInvoice, - modifier = Modifier.weight(1f) - ) - } - - else -> { - val cachedInvoice = invoicesByTab[targetTab].orEmpty() - - ReceiveQrView( - uri = cachedInvoice, - qrLogoPainter = painterResource(qrLogoRes), - onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { - onClickEditInvoice - } else { - onClickReceiveCjit - }, - tab = targetTab, - modifier = Modifier.fillMaxWidth() - ) + ) { + itemsIndexed( + items = visibleTabs, + key = { _, tab -> tab.name } + ) { _, tab -> + Box( + modifier = Modifier + .fillParentMaxWidth() + .fillParentMaxHeight() + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + when { + showingCjitOnboarding && tab == ReceiveTab.SPENDING -> { + CjitOnBoardingView( + modifier = Modifier.weight(1f) + ) + } + + showDetails -> { + ReceiveDetailsView( + tab = tab, + walletState = walletState, + cjitInvoice = cjitInvoice, + modifier = Modifier.weight(1f) + ) + } + + else -> { + val invoice = getInvoiceForTab( + tab = tab, + bip21 = walletState.bip21, + bolt11 = walletState.bolt11, + cjitInvoice = cjitInvoice, + isNodeRunning = walletState.nodeLifecycleState.isRunning(), + onchainAddress = walletState.onchainAddress + ) + + ReceiveQrView( + uri = invoice, + qrLogoPainter = painterResource(getQrLogoResource(tab)), + onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { + onClickEditInvoice + } else { + onClickReceiveCjit + }, + tab = tab, + modifier = Modifier.fillMaxWidth() + ) + } } } } From f44b3c76c3818f4dbd427c495a80e9858192eb47 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 13:57:01 -0300 Subject: [PATCH 57/68] fix: restore map --- .../wallets/receive/ReceiveQrScreen.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 739f29266..76b98de91 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -101,6 +101,26 @@ fun ReceiveQrScreen( } } + val invoicesByTab = remember( + visibleTabs, + walletState.bip21, + walletState.bolt11, + walletState.onchainAddress, + cjitInvoice, + walletState.nodeLifecycleState + ) { + visibleTabs.associateWith { tab -> + getInvoiceForTab( + tab = tab, + bip21 = walletState.bip21, + bolt11 = walletState.bolt11, + cjitInvoice = cjitInvoice, + isNodeRunning = walletState.nodeLifecycleState.isRunning(), + onchainAddress = walletState.onchainAddress + ) + } + } + // Determine initial tab index val initialTabIndex = remember(initialTab, visibleTabs) { if (initialTab != null) { @@ -155,7 +175,7 @@ fun ReceiveQrScreen( // Tab row CustomTabRowWithSpacing( tabs = visibleTabs, - currentTabIndex = lazyListState.firstVisibleItemIndex, + currentTabIndex = visibleTabs.indexOf(selectedTab), selectedColor = when (selectedTab) { ReceiveTab.SAVINGS -> Colors.Brand ReceiveTab.AUTO -> Colors.White @@ -212,14 +232,7 @@ fun ReceiveQrScreen( } else -> { - val invoice = getInvoiceForTab( - tab = tab, - bip21 = walletState.bip21, - bolt11 = walletState.bolt11, - cjitInvoice = cjitInvoice, - isNodeRunning = walletState.nodeLifecycleState.isRunning(), - onchainAddress = walletState.onchainAddress - ) + val invoice = invoicesByTab[tab].orEmpty() ReceiveQrView( uri = invoice, From 1e2e856a86432df3a9b2c3d2e47e2e3c713863e1 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 14:03:05 -0300 Subject: [PATCH 58/68] fix: horizontal spacing --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 76b98de91..1b64a736d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -196,7 +196,7 @@ fun ReceiveQrScreen( LazyRow( state = lazyListState, flingBehavior = snapBehavior, - horizontalArrangement = Arrangement.spacedBy(0.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), userScrollEnabled = true, modifier = Modifier .weight(1f) From a3b1c7df941e8a4412ea5121e965b45df9f72ded Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 14:24:27 -0300 Subject: [PATCH 59/68] fix: detail button update --- .../wallets/receive/ReceiveQrScreen.kt | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 1b64a736d..c6e0fb1d8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -3,6 +3,8 @@ package to.bitkit.ui.screens.wallets.receive import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,8 +18,6 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.gestures.snapping.SnapPosition -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState @@ -27,6 +27,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -145,16 +146,37 @@ fun ReceiveQrScreen( snapPosition = SnapPosition.Center ) - // Derive selectedTab from scroll position - val selectedTab = remember(lazyListState.firstVisibleItemIndex, visibleTabs) { - visibleTabs.getOrNull(lazyListState.firstVisibleItemIndex) - ?: visibleTabs.firstOrNull() - ?: ReceiveTab.SAVINGS + // Calculate current tab index based on scroll position for smooth indicator updates + val currentTabIndex by remember { + derivedStateOf { + val layoutInfo = lazyListState.layoutInfo + if (layoutInfo.visibleItemsInfo.isEmpty()) { + lazyListState.firstVisibleItemIndex + } else { + val viewportMidpoint = layoutInfo.viewportStartOffset + + (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2 + + layoutInfo.visibleItemsInfo + .minByOrNull { item -> + val itemMidpoint = item.offset + item.size / 2 + kotlin.math.abs(itemMidpoint - viewportMidpoint) + } + ?.index ?: lazyListState.firstVisibleItemIndex + } + } + } + + // Derive selectedTab from real-time currentTabIndex for smooth color updates + val selectedTab by remember { + derivedStateOf { + visibleTabs.getOrNull(currentTabIndex) + ?: visibleTabs.firstOrNull() + ?: ReceiveTab.SAVINGS + } } - val showingCjitOnboarding = remember(selectedTab, walletState, cjitInvoice, hasUsableChannels) { - selectedTab == ReceiveTab.SPENDING && - !hasUsableChannels && + val showingCjitOnboarding = remember(walletState, cjitInvoice, hasUsableChannels) { + !hasUsableChannels && walletState.nodeLifecycleState.isRunning() && cjitInvoice.isNullOrEmpty() } @@ -175,7 +197,7 @@ fun ReceiveQrScreen( // Tab row CustomTabRowWithSpacing( tabs = visibleTabs, - currentTabIndex = visibleTabs.indexOf(selectedTab), + currentTabIndex = currentTabIndex, selectedColor = when (selectedTab) { ReceiveTab.SAVINGS -> Colors.Brand ReceiveTab.AUTO -> Colors.White @@ -255,16 +277,17 @@ fun ReceiveQrScreen( Spacer(Modifier.height(24.dp)) AnimatedVisibility(visible = walletState.nodeLifecycleState.isRunning()) { + val showCjitButton = showingCjitOnboarding && selectedTab == ReceiveTab.SPENDING PrimaryButton( text = stringResource( when { - showingCjitOnboarding -> R.string.wallet__receive__cjit + showCjitButton -> R.string.wallet__receive__cjit showDetails -> R.string.wallet__receive_show_qr else -> R.string.wallet__receive_show_details } ), icon = { - if (showingCjitOnboarding) { + if (showCjitButton) { Icon( painter = painterResource(R.drawable.ic_lightning_alt), tint = Colors.Purple, @@ -273,7 +296,7 @@ fun ReceiveQrScreen( } }, onClick = { - if (showingCjitOnboarding) { + if (showCjitButton) { onClickReceiveCjit() showDetails = false } else { From 3c70daf441b991212c1893eedd0b814e8e6e0478 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 14:32:12 -0300 Subject: [PATCH 60/68] refactor: improve state derivation --- .../wallets/receive/ReceiveQrScreen.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index c6e0fb1d8..f42eebe0e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -146,11 +146,11 @@ fun ReceiveQrScreen( snapPosition = SnapPosition.Center ) - // Calculate current tab index based on scroll position for smooth indicator updates - val currentTabIndex by remember { + // Calculate current tab based on scroll position for smooth indicator and color updates + val selectedTab by remember { derivedStateOf { val layoutInfo = lazyListState.layoutInfo - if (layoutInfo.visibleItemsInfo.isEmpty()) { + val currentIndex = if (layoutInfo.visibleItemsInfo.isEmpty()) { lazyListState.firstVisibleItemIndex } else { val viewportMidpoint = layoutInfo.viewportStartOffset + @@ -163,15 +163,17 @@ fun ReceiveQrScreen( } ?.index ?: lazyListState.firstVisibleItemIndex } + + visibleTabs.getOrNull(currentIndex) + ?: visibleTabs.firstOrNull() + ?: ReceiveTab.SAVINGS } } - // Derive selectedTab from real-time currentTabIndex for smooth color updates - val selectedTab by remember { + // Derive index from selectedTab for tab row indicator + val currentTabIndex by remember { derivedStateOf { - visibleTabs.getOrNull(currentTabIndex) - ?: visibleTabs.firstOrNull() - ?: ReceiveTab.SAVINGS + visibleTabs.indexOf(selectedTab).coerceAtLeast(0) } } @@ -292,6 +294,7 @@ fun ReceiveQrScreen( painter = painterResource(R.drawable.ic_lightning_alt), tint = Colors.Purple, contentDescription = null + ) } }, From e5716d3392b1b66907e88f72c44b3c9d433f08c6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 14:34:21 -0300 Subject: [PATCH 61/68] refactor: remove box wrapper --- .../wallets/receive/ReceiveQrScreen.kt | 68 +++++++++---------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index f42eebe0e..e3f8c17e6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -230,46 +230,42 @@ fun ReceiveQrScreen( items = visibleTabs, key = { _, tab -> tab.name } ) { _, tab -> - Box( + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillParentMaxWidth() .fillParentMaxHeight() ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - when { - showingCjitOnboarding && tab == ReceiveTab.SPENDING -> { - CjitOnBoardingView( - modifier = Modifier.weight(1f) - ) - } - - showDetails -> { - ReceiveDetailsView( - tab = tab, - walletState = walletState, - cjitInvoice = cjitInvoice, - modifier = Modifier.weight(1f) - ) - } - - else -> { - val invoice = invoicesByTab[tab].orEmpty() - - ReceiveQrView( - uri = invoice, - qrLogoPainter = painterResource(getQrLogoResource(tab)), - onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { - onClickEditInvoice - } else { - onClickReceiveCjit - }, - tab = tab, - modifier = Modifier.fillMaxWidth() - ) - } + when { + showingCjitOnboarding && tab == ReceiveTab.SPENDING -> { + CjitOnBoardingView( + modifier = Modifier.weight(1f) + ) + } + + showDetails -> { + ReceiveDetailsView( + tab = tab, + walletState = walletState, + cjitInvoice = cjitInvoice, + modifier = Modifier.weight(1f) + ) + } + + else -> { + val invoice = invoicesByTab[tab].orEmpty() + + ReceiveQrView( + uri = invoice, + qrLogoPainter = painterResource(getQrLogoResource(tab)), + onClickEditInvoice = if (cjitInvoice.isNullOrEmpty()) { + onClickEditInvoice + } else { + onClickReceiveCjit + }, + tab = tab, + modifier = Modifier.fillMaxWidth() + ) } } } From 3c005c84d53e70640e23acdb9ed8b1a5aed72fe2 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 14:38:51 -0300 Subject: [PATCH 62/68] refactor: extract selection color --- .../wallets/receive/ReceiveQrScreen.kt | 24 ++++--------------- .../ui/screens/wallets/receive/ReceiveTab.kt | 9 +++++++ 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index e3f8c17e6..f923a0de0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -200,11 +200,7 @@ fun ReceiveQrScreen( CustomTabRowWithSpacing( tabs = visibleTabs, currentTabIndex = currentTabIndex, - selectedColor = when (selectedTab) { - ReceiveTab.SAVINGS -> Colors.Brand - ReceiveTab.AUTO -> Colors.White - ReceiveTab.SPENDING -> Colors.Purple - }, + selectedColor = selectedTab.accentColor, onTabChange = { tab -> haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) val newIndex = visibleTabs.indexOf(tab) @@ -360,11 +356,7 @@ private fun ReceiveQrView( Icon( painter = painterResource(R.drawable.ic_pencil_simple), contentDescription = null, - tint = when (tab) { - ReceiveTab.SAVINGS -> Colors.Brand - ReceiveTab.AUTO -> Colors.Brand - ReceiveTab.SPENDING -> Colors.Purple - }, + tint = tab.accentColor, modifier = Modifier.size(18.dp) ) }, @@ -387,11 +379,7 @@ private fun ReceiveQrView( Icon( painter = painterResource(R.drawable.ic_copy), contentDescription = null, - tint = when (tab) { - ReceiveTab.SAVINGS -> Colors.Brand - ReceiveTab.AUTO -> Colors.Brand - ReceiveTab.SPENDING -> Colors.Purple - }, + tint = tab.accentColor, modifier = Modifier.size(18.dp) ) }, @@ -412,11 +400,7 @@ private fun ReceiveQrView( Icon( painter = painterResource(R.drawable.ic_share), contentDescription = null, - tint = when (tab) { - ReceiveTab.SAVINGS -> Colors.Brand - ReceiveTab.AUTO -> Colors.Brand - ReceiveTab.SPENDING -> Colors.Purple - }, + tint = tab.accentColor, modifier = Modifier.size(18.dp) ) }, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt index d7e2d7dde..1d1c8611c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveTab.kt @@ -1,9 +1,11 @@ package to.bitkit.ui.screens.wallets.receive import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import to.bitkit.R import to.bitkit.ui.screens.wallets.activity.components.TabItem +import to.bitkit.ui.theme.Colors enum class ReceiveTab : TabItem { SAVINGS, @@ -17,4 +19,11 @@ enum class ReceiveTab : TabItem { AUTO -> stringResource(R.string.wallet__receive_tab_auto) SPENDING -> stringResource(R.string.wallet__receive_tab_spending) } + + val accentColor: Color + get() = when (this) { + SAVINGS -> Colors.Brand + AUTO -> Colors.Brand + SPENDING -> Colors.Purple + } } From abfabae5f32ff729475a7dcf7f1bb9990a281a81 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 3 Dec 2025 14:56:47 -0300 Subject: [PATCH 63/68] chore: imports --- app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt index 66e46fef1..2019b5ba7 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -36,7 +35,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp From 7ff13421f60c903c0818bb55128d03902b0296aa Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 4 Dec 2025 06:17:59 -0300 Subject: [PATCH 64/68] fix: remove column horizontal padding --- .../bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index f923a0de0..0b52707ca 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -191,9 +192,7 @@ fun ReceiveQrScreen( .keepScreenOn() ) { SheetTopBar(stringResource(R.string.wallet__receive_bitcoin)) - Column( - modifier = Modifier.padding(horizontal = 16.dp) - ) { + Column { Spacer(Modifier.height(16.dp)) // Tab row @@ -207,7 +206,8 @@ fun ReceiveQrScreen( scope.launch { lazyListState.animateScrollToItem(newIndex) } - } + }, + modifier = Modifier.padding(horizontal = 16.dp) ) Spacer(Modifier.height(24.dp)) @@ -217,6 +217,7 @@ fun ReceiveQrScreen( state = lazyListState, flingBehavior = snapBehavior, horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 16.dp), userScrollEnabled = true, modifier = Modifier .weight(1f) From e9df62a9fc1d09ff0992d8810b7e1643148aa6df Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 4 Dec 2025 06:26:52 -0300 Subject: [PATCH 65/68] fix: button padding --- .../to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 0b52707ca..3578e2670 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -300,7 +300,9 @@ fun ReceiveQrScreen( } }, fullWidth = true, - modifier = Modifier.testTag( + modifier = Modifier + .padding(horizontal = 16.dp) + .testTag( if (showDetails) { "QRCode" } else { From ba46ca251ac5fb22b66120a8925f585882624082 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 4 Dec 2025 07:47:22 -0300 Subject: [PATCH 66/68] refactor: simplify code --- .../wallets/receive/ReceiveQrScreen.kt | 82 ++++++++----------- 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 3578e2670..340ad7389 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -28,12 +28,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -48,6 +49,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.ChannelDetails import to.bitkit.R @@ -77,6 +80,7 @@ import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.MainUiState +@OptIn(FlowPreview::class) @Composable fun ReceiveQrScreen( cjitInvoice: String?, @@ -123,24 +127,9 @@ fun ReceiveQrScreen( } } - // Determine initial tab index - val initialTabIndex = remember(initialTab, visibleTabs) { - if (initialTab != null) { - visibleTabs.indexOf(initialTab).coerceAtLeast(0) - } else { - when { - !cjitInvoice.isNullOrEmpty() -> visibleTabs.indexOf(ReceiveTab.SPENDING) - hasUsableChannels -> visibleTabs.indexOf(ReceiveTab.AUTO) - else -> visibleTabs.indexOf(ReceiveTab.SAVINGS) - }.coerceAtLeast(0) - } - } - // LazyRow state with snap behavior val scope = rememberCoroutineScope() - val lazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = initialTabIndex - ) + val lazyListState = rememberLazyListState() val snapBehavior = rememberSnapFlingBehavior( lazyListState = lazyListState, @@ -148,33 +137,29 @@ fun ReceiveQrScreen( ) // Calculate current tab based on scroll position for smooth indicator and color updates - val selectedTab by remember { - derivedStateOf { - val layoutInfo = lazyListState.layoutInfo - val currentIndex = if (layoutInfo.visibleItemsInfo.isEmpty()) { - lazyListState.firstVisibleItemIndex - } else { - val viewportMidpoint = layoutInfo.viewportStartOffset + - (layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset) / 2 - - layoutInfo.visibleItemsInfo - .minByOrNull { item -> - val itemMidpoint = item.offset + item.size / 2 - kotlin.math.abs(itemMidpoint - viewportMidpoint) - } - ?.index ?: lazyListState.firstVisibleItemIndex - } + var selectedTab by remember { + mutableStateOf(initialTab ?: ReceiveTab.SAVINGS) + } - visibleTabs.getOrNull(currentIndex) - ?: visibleTabs.firstOrNull() - ?: ReceiveTab.SAVINGS - } + LaunchedEffect(lazyListState, visibleTabs.size) { + snapshotFlow { lazyListState.firstVisibleItemIndex } + .distinctUntilChanged() + .collect { index -> + if (index < visibleTabs.size && index > -1) { + val tab = visibleTabs[index] + selectedTab = tab + } + } } - // Derive index from selectedTab for tab row indicator - val currentTabIndex by remember { - derivedStateOf { - visibleTabs.indexOf(selectedTab).coerceAtLeast(0) + // Auto-switch to AUTO tab when it becomes available for the first time + LaunchedEffect(hasUsableChannels) { + if (hasUsableChannels && visibleTabs.contains(ReceiveTab.AUTO)) { + val autoIndex = visibleTabs.indexOf(ReceiveTab.AUTO) + if (autoIndex != -1) { + lazyListState.animateScrollToItem(autoIndex) + selectedTab = ReceiveTab.AUTO + } } } @@ -198,11 +183,12 @@ fun ReceiveQrScreen( // Tab row CustomTabRowWithSpacing( tabs = visibleTabs, - currentTabIndex = currentTabIndex, + currentTabIndex = visibleTabs.indexOf(selectedTab), selectedColor = selectedTab.accentColor, onTabChange = { tab -> haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) val newIndex = visibleTabs.indexOf(tab) + selectedTab = tab scope.launch { lazyListState.animateScrollToItem(newIndex) } @@ -303,12 +289,12 @@ fun ReceiveQrScreen( modifier = Modifier .padding(horizontal = 16.dp) .testTag( - if (showDetails) { - "QRCode" - } else { - "ShowDetails" - } - ) + if (showDetails) { + "QRCode" + } else { + "ShowDetails" + } + ) ) } From ca693a373ebcb70e540167b8e4afa9819ff350bd Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 4 Dec 2025 08:13:33 -0300 Subject: [PATCH 67/68] fix: prevent from display auto invoice without lightning --- .../wallets/receive/ReceiveInvoiceUtils.kt | 12 ++++- .../receive/ReceiveInvoiceUtilsTest.kt | 52 ++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt index 9d8445d39..e5ac5e324 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtils.kt @@ -27,7 +27,7 @@ fun getInvoiceForTab( } ReceiveTab.AUTO -> { - bip21.takeIf { isNodeRunning }.orEmpty() + bip21.takeIf { isNodeRunning && containsLightningParameter(bip21) }.orEmpty() } ReceiveTab.SPENDING -> { @@ -57,6 +57,16 @@ fun removeLightningFromBip21(bip21: String, fallbackAddress: String): String { return withoutLightning.ifBlank { fallbackAddress } } +/** + * Checks if a BIP21 URI contains a lightning parameter. + * + * @param bip21 The BIP21 URI to check + * @return true if the URI contains a lightning parameter, false otherwise + */ +private fun containsLightningParameter(bip21: String): Boolean { + return Regex("[?&]lightning=[^&]*").containsMatchIn(bip21) +} + /** * Returns the appropriate QR code logo resource for the selected tab. * diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt index 73859869e..9813d2a7c 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/receive/ReceiveInvoiceUtilsTest.kt @@ -88,7 +88,7 @@ class ReceiveInvoiceUtilsTest { } @Test - fun `getInvoiceForTab AUTO returns full BIP21 when node is running`() { + fun `getInvoiceForTab AUTO returns full BIP21 when node running and has lightning`() { val bip21 = "bitcoin:$testAddress?amount=0.001&lightning=$testBolt11" val result = getInvoiceForTab( @@ -104,7 +104,7 @@ class ReceiveInvoiceUtilsTest { } @Test - fun `getInvoiceForTab AUTO returns empty when node is not running`() { + fun `getInvoiceForTab AUTO returns empty when has lightning but node not running`() { val bip21 = "bitcoin:$testAddress?amount=0.001&lightning=$testBolt11" val result = getInvoiceForTab( @@ -119,6 +119,54 @@ class ReceiveInvoiceUtilsTest { assertEquals("", result) } + @Test + fun `getInvoiceForTab AUTO returns empty when BIP21 has no lightning even if node running`() { + val bip21WithoutLightning = "bitcoin:$testAddress?amount=0.001&message=Test" + + val result = getInvoiceForTab( + tab = ReceiveTab.AUTO, + bip21 = bip21WithoutLightning, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals("", result) + } + + @Test + fun `getInvoiceForTab AUTO returns empty when no lightning and node not running`() { + val bip21WithoutLightning = "bitcoin:$testAddress?amount=0.001&message=Test" + + val result = getInvoiceForTab( + tab = ReceiveTab.AUTO, + bip21 = bip21WithoutLightning, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = false, + onchainAddress = testAddress + ) + + assertEquals("", result) + } + + @Test + fun `getInvoiceForTab AUTO detects lightning when it is the first parameter`() { + val bip21LightningFirst = "bitcoin:$testAddress?lightning=$testBolt11&amount=0.001" + + val result = getInvoiceForTab( + tab = ReceiveTab.AUTO, + bip21 = bip21LightningFirst, + bolt11 = testBolt11, + cjitInvoice = null, + isNodeRunning = true, + onchainAddress = testAddress + ) + + assertEquals(bip21LightningFirst, result) + } + @Test fun `getInvoiceForTab SPENDING returns CJIT invoice when available and node running`() { val bip21 = "bitcoin:$testAddress?lightning=$testBolt11" From 09e6b2e71fd5f9595087e3d690cbf4a66cc84148 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 4 Dec 2025 08:20:27 -0300 Subject: [PATCH 68/68] fix: auto tab color --- .../bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 340ad7389..36c04c85f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -69,6 +69,9 @@ import to.bitkit.ui.components.Tooltip import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.activity.components.CustomTabRowWithSpacing +import to.bitkit.ui.screens.wallets.receive.ReceiveTab.AUTO +import to.bitkit.ui.screens.wallets.receive.ReceiveTab.SAVINGS +import to.bitkit.ui.screens.wallets.receive.ReceiveTab.SPENDING import to.bitkit.ui.shared.effects.SetMaxBrightness import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground @@ -184,7 +187,11 @@ fun ReceiveQrScreen( CustomTabRowWithSpacing( tabs = visibleTabs, currentTabIndex = visibleTabs.indexOf(selectedTab), - selectedColor = selectedTab.accentColor, + selectedColor = when (selectedTab) { + SAVINGS -> Colors.Brand + AUTO -> Colors.White + SPENDING -> Colors.Purple + }, onTabChange = { tab -> haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) val newIndex = visibleTabs.indexOf(tab)