From b59596b7452ba95cf59c4fde5aabc36529769ced Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 13:27:54 +0100 Subject: [PATCH 01/20] fix: ensure single active scan job --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 2fdc4fc85..5349c83ac 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -190,6 +190,9 @@ class AppViewModel @Inject constructor( private val _quickPayData = MutableStateFlow(null) val quickPayData = _quickPayData.asStateFlow() + private var activeScanJob: Job? = null + private var activeScanInput: String? = null + private val _sendEffect = MutableSharedFlow(extraBufferCapacity = 1) val sendEffect = _sendEffect.asSharedFlow() private fun setSendEffect(effect: SendEffect) = viewModelScope.launch { _sendEffect.emit(effect) } @@ -985,12 +988,36 @@ class AppViewModel @Inject constructor( ) } - private fun onAddressContinue(data: String) { - viewModelScope.launch { - handleScan(data) + private fun launchScan(source: ScanSource, data: String, delayMs: Long = 0) { + val normalized = data.removeLightningSchemes() + val scanId = if (data.length > 24) "${data.take(11)}…${data.takeLast(11)}" else data + + if (normalized == activeScanInput && activeScanJob?.isActive == true) { + Logger.info("Skipping duplicate scan from '${source.label}': '$scanId'", context = TAG) + return + } + + activeScanJob?.let { + Logger.info("Cancelling prior scan for new '${source.label}': '$scanId'", context = TAG) + it.cancel() + } + + activeScanInput = normalized + Logger.debug("Scan from '${source.label}': '$scanId'", context = TAG) + activeScanJob = viewModelScope.launch { + try { + if (delayMs > 0) delay(delayMs) + handleScan(data) + } finally { + activeScanInput = null + } } } + private fun onAddressContinue(data: String) { + launchScan(source = ScanSource.ADDRESS_CONTINUE, data = data) + } + private suspend fun onAmountChange(amount: ULong) { _sendUiState.update { it.copy( @@ -1136,9 +1163,7 @@ class AppViewModel @Inject constructor( ) return } - viewModelScope.launch { - handleScan(data) - } + launchScan(source = ScanSource.PASTE, data = data) } private fun onScanClick() { @@ -1146,10 +1171,7 @@ class AppViewModel @Inject constructor( } fun onScanResult(data: String, delayMs: Long = 0) { - viewModelScope.launch { - delay(delayMs) - handleScan(data) - } + launchScan(source = ScanSource.SCAN_RESULT, data = data, delayMs = delayMs) } private suspend fun handleScan(result: String) = withContext(bgDispatcher) { @@ -2314,9 +2336,7 @@ class AppViewModel @Inject constructor( if (!walletRepo.walletExists()) return@launch - val data = uri.toString() - delay(SCREEN_TRANSITION_DELAY_MS) - handleScan(data) + launchScan(source = ScanSource.DEEPLINK, data = uri.toString(), delayMs = SCREEN_TRANSITION_DELAY_MS) } // TODO Temporary fix while these schemes can't be decoded https://github.com/synonymdev/bitkit-core/issues/70 @@ -2359,6 +2379,13 @@ class AppViewModel @Inject constructor( } } + private enum class ScanSource(val label: String) { + PASTE("paste"), + SCAN_RESULT("scan result"), + ADDRESS_CONTINUE("address continue"), + DEEPLINK("deeplink"), + } + companion object { private const val TAG = "AppViewModel" private const val SEND_AMOUNT_WARNING_THRESHOLD = 100.0 From be91d2aae2c3f22f2b715c777c7b62164b79ac74 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 15:00:11 +0100 Subject: [PATCH 02/20] fix: debounce click on pressable buttons --- .../java/to/bitkit/ui/components/Button.kt | 10 ++++--- .../bitkit/ui/components/RectangleButton.kt | 4 ++- .../ui/shared/modifiers/ClickableAlpha.kt | 26 +++++++++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/Button.kt b/app/src/main/java/to/bitkit/ui/components/Button.kt index d5f15083f..fd0e62bc9 100644 --- a/app/src/main/java/to/bitkit/ui/components/Button.kt +++ b/app/src/main/java/to/bitkit/ui/components/Button.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.ui.shared.modifiers.alphaFeedback +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.shared.util.primaryButtonStyle import to.bitkit.ui.theme.AppButtonDefaults import to.bitkit.ui.theme.AppThemeSurface @@ -66,11 +67,12 @@ fun PrimaryButton( color: Color? = null, enableGradient: Boolean = true, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) val buttonShape = MaterialTheme.shapes.large Button( - onClick = onClick, + onClick = debouncedClick, enabled = enabled && !isLoading, colors = AppButtonDefaults.primaryColors.copy( containerColor = Color.Transparent, @@ -136,10 +138,11 @@ fun SecondaryButton( enabled: Boolean = true, fullWidth: Boolean = true, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) val border = BorderStroke(2.dp, if (enabled) Colors.Gray4 else Color.Transparent) OutlinedButton( - onClick = onClick, + onClick = debouncedClick, enabled = enabled && !isLoading, colors = AppButtonDefaults.secondaryColors, contentPadding = contentPadding, @@ -195,9 +198,10 @@ fun TertiaryButton( enabled: Boolean = true, fullWidth: Boolean = true, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) TextButton( - onClick = onClick, + onClick = debouncedClick, enabled = enabled && !isLoading, colors = AppButtonDefaults.tertiaryColors, contentPadding = contentPadding, diff --git a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt index 1d5f9d727..b0f8e36ea 100644 --- a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes @@ -44,8 +45,9 @@ fun RectangleButton( iconSize: Dp = 20.dp, onClick: () -> Unit = {}, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) Button( - onClick = onClick, + onClick = debouncedClick, colors = ButtonDefaults.buttonColors( containerColor = Colors.Gray6, ), diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index 555d79d06..6447828c4 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -1,5 +1,6 @@ package to.bitkit.ui.shared.modifiers +import android.os.SystemClock import androidx.compose.animation.core.Animatable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.runtime.Composable @@ -24,6 +25,26 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.unit.Constraints import kotlinx.coroutines.launch +private const val CLICK_DEBOUNCE_MS = 300L + +private class ClickDebouncer(private val debounceMs: Long = CLICK_DEBOUNCE_MS) { + private var lastClickTime = 0L + + fun tryClick(onClick: () -> Unit) { + val now = SystemClock.uptimeMillis() + if (now - lastClickTime >= debounceMs) { + lastClickTime = now + onClick() + } + } +} + +@Composable +fun rememberDebouncedClick(debounceMs: Long = CLICK_DEBOUNCE_MS, onClick: () -> Unit): () -> Unit { + val debouncer = remember { ClickDebouncer(debounceMs) } + return { debouncer.tryClick(onClick) } +} + /** * Adjusts the alpha of a composable when it is pressed and makes it clickable. * When pressed, the alpha is reduced to provide visual feedback. @@ -64,6 +85,7 @@ private class ClickableAlphaNode( ) : DelegatingNode(), LayoutModifierNode, SemanticsModifierNode { private val animatable = Animatable(1f) + private val debouncer = ClickDebouncer() init { delegate( @@ -77,7 +99,7 @@ private class ClickableAlphaNode( } }, onTap = { - onClick() + debouncer.tryClick(onClick) coroutineScope.launch { animatable.animateTo(pressedAlpha) animatable.animateTo(1f) @@ -101,7 +123,7 @@ private class ClickableAlphaNode( override fun SemanticsPropertyReceiver.applySemantics() { role = Role.Button onClick { - onClick() + debouncer.tryClick(onClick) true } } From 51669eb64d9b16687c5c7572cd2e6806ac860aa1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 20:02:20 +0100 Subject: [PATCH 03/20] fix: debounce nav icon clicks --- app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt index 7b72984ad..535c249e7 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ui.LocalDrawerState import to.bitkit.ui.components.Title +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface @OptIn(ExperimentalMaterial3Api::class) @@ -76,8 +77,9 @@ fun BackNavIcon( onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) IconButton( - onClick = onClick, + onClick = debouncedClick, modifier = modifier.testTag("NavigationBack") ) { Icon( @@ -97,8 +99,9 @@ fun DrawerNavIcon( val scope = androidx.compose.runtime.rememberCoroutineScope() if (drawerState != null || isPreview) { + val debouncedClick = rememberDebouncedClick { scope.launch { drawerState?.open() } } IconButton( - onClick = { scope.launch { drawerState?.open() } }, + onClick = debouncedClick, modifier = modifier.testTag("HeaderMenu") ) { Icon( @@ -115,8 +118,9 @@ fun ScanNavIcon( onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) IconButton( - onClick = onClick, + onClick = debouncedClick, modifier = modifier.testTag("NavigationAction") ) { Icon( From 049db6249d936511a45d59832d0b424c6354d188 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 21:36:04 +0100 Subject: [PATCH 04/20] fix: debounce tab bar and drawer clicks --- app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt | 2 +- app/src/main/java/to/bitkit/ui/components/TabBar.kt | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt index 79b694d41..303c1dc33 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -263,7 +263,7 @@ private fun DrawerItem( modifier = modifier .then( if (onClick != null) { - Modifier.clickable { onClick() } + Modifier.clickableAlpha { onClick() } } else { Modifier } diff --git a/app/src/main/java/to/bitkit/ui/components/TabBar.kt b/app/src/main/java/to/bitkit/ui/components/TabBar.kt index fd629f34e..1e816d99c 100644 --- a/app/src/main/java/to/bitkit/ui/components/TabBar.kt +++ b/app/src/main/java/to/bitkit/ui/components/TabBar.kt @@ -1,7 +1,6 @@ package to.bitkit.ui.components import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column @@ -83,7 +82,7 @@ fun BoxScope.TabBar( .weight(1f) .height(60.dp) .clip(buttonLeftShape) - .clickable { onSendClick() } + .clickableAlpha { onSendClick() } .testTag("Send") ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -104,7 +103,7 @@ fun BoxScope.TabBar( .weight(1f) .height(60.dp) .clip(buttonRightShape) - .clickable { onReceiveClick() } + .clickableAlpha { onReceiveClick() } .testTag("Receive") ) { Row(verticalAlignment = Alignment.CenterVertically) { From 0524f51747795ddadc0c5a3da8100bed3a1fa62c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 22:17:48 +0100 Subject: [PATCH 05/20] fix: debounce remaining clickables Co-Authored-By: Claude Opus 4.6 --- .../to/bitkit/ui/components/QrCodeImage.kt | 18 +++++++++++------- .../screens/transfer/SavingsAdvancedScreen.kt | 4 ++-- .../screens/widgets/headlines/HeadlineCard.kt | 4 ++-- .../bitkit/ui/settings/ChannelOrdersScreen.kt | 5 ++--- .../ui/settings/backups/ShowMnemonicScreen.kt | 4 ++-- 5 files changed, 19 insertions(+), 16 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 2a54dab4b..47f31b375 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween 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.aspectRatio import androidx.compose.foundation.layout.fillMaxSize @@ -46,6 +45,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ext.setClipboardText +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -95,12 +95,16 @@ fun QrCodeImage( contentDescription = content, contentScale = ContentScale.Inside, modifier = Modifier - .clickable(enabled = tipMessage.isNotBlank()) { - coroutineScope.launch { - context.setClipboardText(copyContent ?: content) - tooltipState.show() - } - } + .clickableAlpha( + onClick = if (tipMessage.isNotBlank()) { + { + coroutineScope.launch { + context.setClipboardText(copyContent ?: content) + tooltipState.show() + } + } + } else null + ) .then(testTag?.let { Modifier.testTag(it) } ?: Modifier) ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt index b72503af6..638d3fa87 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAdvancedScreen.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.screens.transfer -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -37,6 +36,7 @@ import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -165,7 +165,7 @@ fun ChannelItem( onClick: () -> Unit, ) { Column( - modifier = Modifier.clickable { onClick() } + modifier = Modifier.clickableAlpha { onClick() } ) { Row( horizontalArrangement = Arrangement.SpaceBetween, diff --git a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt index d3dd27650..29ba96630 100644 --- a/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt +++ b/app/src/main/java/to/bitkit/ui/screens/widgets/headlines/HeadlineCard.kt @@ -2,7 +2,6 @@ package to.bitkit.ui.screens.widgets.headlines import android.content.Intent 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 @@ -34,6 +33,7 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMB import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodyS +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -54,7 +54,7 @@ fun HeadlineCard( modifier = modifier .clip(shape = MaterialTheme.shapes.medium) .background(Colors.White10) - .clickable { + .clickableAlpha { val intent = Intent(Intent.ACTION_VIEW, link.toUri()) context.startActivity(intent) } diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt index 756371b46..1f47e4358 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.settings -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -342,7 +341,7 @@ private fun OrderCard(model: IBtOrder, onClick: (String) -> Unit) { colors = cardColors, modifier = Modifier .fillMaxWidth() - .clickable { onClick(model.id) } + .clickableAlpha { onClick(model.id) } ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -404,7 +403,7 @@ private fun CJitCard(model: IcJitEntry, onClick: (String) -> Unit) { colors = cardColors, modifier = Modifier .fillMaxWidth() - .clickable { onClick(model.id) } + .clickableAlpha { onClick(model.id) } ) { Column( verticalArrangement = Arrangement.spacedBy(16.dp), diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 0468834aa..d9745715c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith 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.Spacer @@ -50,6 +49,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SheetSize import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.effects.BlockScreenshots +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.util.gradientBackground @@ -157,7 +157,7 @@ private fun ShowMnemonicContent( .fillMaxWidth() .clip(MaterialTheme.shapes.medium) .background(color = Colors.White10) - .clickable(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) + .clickableAlpha(onClick = if (showMnemonic && mnemonic.isNotEmpty()) onCopyClick else null) .padding(32.dp) .testTag("backup_mnemonic_words_box") ) { From 1fff2a14d5c8881915f391cc526e78c4ae4b3ae8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 22:18:55 +0100 Subject: [PATCH 06/20] fix: debounce scrim and overlay clicks Co-Authored-By: Claude Opus 4.6 --- app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt | 4 +++- app/src/main/java/to/bitkit/ui/components/SheetHost.kt | 4 +++- .../main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt index 303c1dc33..9a20fb430 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -48,6 +48,7 @@ import to.bitkit.ui.Routes import to.bitkit.ui.navigateIfNotCurrent import to.bitkit.ui.navigateToHome import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.shared.util.blockPointerInputPassthrough import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -235,6 +236,7 @@ private fun Scrim( onClick: () -> Unit, modifier: Modifier = Modifier, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) AnimatedVisibility( visible = visible, modifier = modifier @@ -246,7 +248,7 @@ private fun Scrim( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = onClick, + onClick = debouncedClick, ) ) } diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 3d99fe204..52dcf017b 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.Colors @@ -125,6 +126,7 @@ private fun Scrim( bottomSheetState: SheetState, onClick: () -> Unit, ) { + val debouncedClick = rememberDebouncedClick(onClick = onClick) val isBottomSheetVisible = bottomSheetState.targetValue != SheetValue.Hidden val scrimAlpha by animateFloatAsState( targetValue = if (isBottomSheetVisible) 0.5f else 0f, @@ -139,7 +141,7 @@ private fun Scrim( .clickable( interactionSource = null, indication = null, - onClick = onClick, + onClick = debouncedClick, ) ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt index 3fd7172a8..4f6182316 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt @@ -31,6 +31,7 @@ import to.bitkit.ui.components.RectangleButton import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -86,6 +87,7 @@ fun FundingScreen( modifier = Modifier.testTag("FundTransfer") ) if (balances.channelFundableBalance == 0uL) { + val debouncedClick = rememberDebouncedClick { showNoFundsAlert = true } Box( modifier = Modifier .matchParentSize() @@ -93,7 +95,7 @@ fun FundingScreen( enabled = balances.channelFundableBalance == 0uL, interactionSource = null, indication = null, - onClick = { showNoFundsAlert = true } + onClick = debouncedClick, ) .testTag("FundTransfer") ) From 456fb6e769ccf545554a54a4593536dcfff81ce8 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 22:19:37 +0100 Subject: [PATCH 07/20] fix: debounce icon and text buttons Co-Authored-By: Claude Opus 4.6 --- app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt | 3 ++- app/src/main/java/to/bitkit/ui/components/EmptyWalletView.kt | 5 ++--- app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt | 3 ++- app/src/main/java/to/bitkit/ui/components/ToastView.kt | 3 ++- app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt | 5 +++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index e9f40bb29..780513be1 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -69,6 +69,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes @@ -438,7 +439,7 @@ private fun PeerCard( maxLines = 1, ) } - IconButton(onClick = { onDisconnectPeer(peer.peerDetails) }) { + IconButton(onClick = rememberDebouncedClick { onDisconnectPeer(peer.peerDetails) }) { Icon( imageVector = Icons.Default.RemoveCircleOutline, contentDescription = stringResource(R.string.common__close), diff --git a/app/src/main/java/to/bitkit/ui/components/EmptyWalletView.kt b/app/src/main/java/to/bitkit/ui/components/EmptyWalletView.kt index 13877de92..f0dc1f342 100644 --- a/app/src/main/java/to/bitkit/ui/components/EmptyWalletView.kt +++ b/app/src/main/java/to/bitkit/ui/components/EmptyWalletView.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -66,9 +67,7 @@ fun EmptyStateView( } if (onClose != null) { IconButton( - onClick = { - onClose() - }, + onClick = rememberDebouncedClick(onClick = onClose), modifier = Modifier .size(40.dp) .align(Alignment.TopEnd) diff --git a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt index 8c1a36fb8..771a91152 100644 --- a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt +++ b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.models.Suggestion import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors @@ -127,7 +128,7 @@ fun SuggestionCard( if (onClose != null) { IconButton( - onClick = onClose, + onClick = rememberDebouncedClick(onClick = onClose), modifier = Modifier .size(16.dp) .testTag("SuggestionDismiss") diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index aef3abf9d..28a21cfeb 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -54,6 +54,7 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import kotlin.math.roundToInt @@ -243,7 +244,7 @@ fun ToastView( contentAlignment = Alignment.TopEnd ) { IconButton( - onClick = onDismiss, + onClick = rememberDebouncedClick(onClick = onDismiss), modifier = Modifier .size(48.dp) .padding(16.dp) diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt index 462a69df6..f17b9b1ca 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppAlertDialog.kt @@ -17,6 +17,7 @@ import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Title +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -67,7 +68,7 @@ fun AppAlertDialog( onDismissRequest = onDismissRequest, confirmButton = { TextButton( - onClick = onConfirm, + onClick = rememberDebouncedClick(onClick = onConfirm), modifier = Modifier.testTag("DialogConfirm") ) { BodyMSB(text = confirmText) @@ -75,7 +76,7 @@ fun AppAlertDialog( }, dismissButton = { TextButton( - onClick = onDismiss, + onClick = rememberDebouncedClick(onClick = onDismiss), modifier = Modifier.testTag("DialogCancel") ) { BodyMSB(text = dismissText, color = Colors.White64) From 9e1f536d175916ee066106f2a0839aae0a6567ca Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 22:20:00 +0100 Subject: [PATCH 08/20] fix: send buttons press feedback Co-Authored-By: Claude Opus 4.6 --- app/src/main/java/to/bitkit/ui/components/RectangleButton.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt index b0f8e36ea..94f2677a9 100644 --- a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.ui.shared.modifiers.alphaFeedback import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -56,6 +57,7 @@ fun RectangleButton( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 0.dp), modifier = modifier .alpha(if (enabled) 1f else 0.5f) + .alphaFeedback(enabled = enabled) .height(80.dp) .fillMaxWidth() ) { From 1d2023c0556e7a354bb1c4a3f6fbb43e2f269f00 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 9 Mar 2026 22:22:30 +0100 Subject: [PATCH 09/20] fix: lint issues from debounce changes Co-Authored-By: Claude Opus 4.6 --- app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt | 4 +++- app/src/main/java/to/bitkit/ui/components/SheetHost.kt | 2 +- 2 files changed, 4 insertions(+), 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 47f31b375..1a0a9fa20 100644 --- a/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt +++ b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt @@ -103,7 +103,9 @@ fun QrCodeImage( tooltipState.show() } } - } else null + } else { + null + } ) .then(testTag?.let { Modifier.testTag(it) } ?: Modifier) ) diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 52dcf017b..b27c1c367 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -26,8 +26,8 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute -import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.sheets.SendRoute +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.Colors From 99f6a42ab73c731d88c8fbd06c126b127e3343fc Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Mar 2026 00:53:49 +0100 Subject: [PATCH 10/20] fix: prevent duplicate nav on multi tap --- AGENTS.md | 1 + app/src/main/java/to/bitkit/ui/ContentView.kt | 257 +++++++----------- .../main/java/to/bitkit/ui/MainActivity.kt | 12 +- .../bitkit/ui/components/AuthCheckScreen.kt | 9 +- .../to/bitkit/ui/components/DrawerMenu.kt | 20 +- .../java/to/bitkit/ui/components/SheetHost.kt | 2 +- .../ui/screens/settings/DevSettingsScreen.kt | 15 +- .../bitkit/ui/screens/wallets/HomeScreen.kt | 45 +-- .../screens/wallets/receive/ReceiveSheet.kt | 21 +- .../ui/settings/AdvancedSettingsScreen.kt | 15 +- .../ui/settings/BackupSettingsScreen.kt | 3 +- .../to/bitkit/ui/settings/SettingsScreen.kt | 3 +- .../ui/settings/appStatus/AppStatusScreen.kt | 9 +- .../settings/general/GeneralSettingsScreen.kt | 5 +- .../settings/lightning/ChannelDetailScreen.kt | 3 +- .../lightning/LightningConnectionsScreen.kt | 3 +- .../ui/settings/pin/DisablePinScreen.kt | 4 +- .../ui/settings/support/SupportScreen.kt | 5 +- .../ui/shared/modifiers/ClickableAlpha.kt | 2 +- .../java/to/bitkit/ui/sheets/BackupSheet.kt | 20 +- .../java/to/bitkit/ui/sheets/GiftSheet.kt | 3 +- .../main/java/to/bitkit/ui/sheets/PinSheet.kt | 11 +- .../java/to/bitkit/ui/sheets/SendSheet.kt | 45 +-- .../java/to/bitkit/viewmodels/AppViewModel.kt | 12 +- 24 files changed, 245 insertions(+), 280 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1600ddb18..2cacfd68f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -194,6 +194,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS use `remember` for expensive Compose computations - ALWAYS add modifiers to the last place in the argument list when calling composable functions - NEVER add parameters with default values BEFORE the `modifier` parameter in composable functions - modifier must be the FIRST optional parameter +- ALWAYS use `navController.navigateTo(route)` for simple navigation; NEVER use raw `navController.navigate(route)` — `navigateTo` prevents duplicate destinations - ALWAYS prefer `VerticalSpacer`, `HorizontalSpacer`, `FillHeight` and `FillWidth` over `Spacer` when applicable - PREFER declaring small dependant classes, constants, interfaces or top-level functions in the same file with the core class where these are used - ALWAYS create data classes for state AFTER viewModel class in same file diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 26b41298f..99da8b69d 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -32,7 +32,7 @@ import androidx.navigation.NavController import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController -import androidx.navigation.NavOptions +import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState @@ -260,7 +260,11 @@ fun ContentView( LaunchedEffect(appViewModel) { appViewModel.mainScreenEffect.collect { when (it) { - is MainScreenEffect.Navigate -> navController.navigate(it.route, navOptions = it.navOptions) + is MainScreenEffect.Navigate -> if (it.clearStack) { + navController.navigateTo(it.route) { popUpTo(0) { inclusive = true } } + } else { + navController.navigateTo(it.route) + } is MainScreenEffect.ProcessClipboardAutoRead -> { val isOnHome = navController.currentDestination?.hasRoute() == true if (!isOnHome) { @@ -385,7 +389,7 @@ fun ContentView( ReceiveSheet( walletState = walletState, navigateToExternalConnection = { - navController.navigate(ExternalConnection()) + navController.navigateTo(ExternalConnection()) appViewModel.hideSheet() } ) @@ -419,7 +423,7 @@ fun ContentView( BackgroundPaymentsIntroSheet( onContinue = { appViewModel.dismissTimedSheet() - navController.navigate(Routes.BackgroundPaymentsSettings) + navController.navigateTo(Routes.BackgroundPaymentsSettings) settingsViewModel.setBgPaymentsIntroSeen(true) }, ) @@ -429,7 +433,7 @@ fun ContentView( QuickPayIntroSheet( onContinue = { appViewModel.dismissTimedSheet() - navController.navigate(Routes.QuickPaySettings) + navController.navigateTo(Routes.QuickPaySettings) }, ) } @@ -567,7 +571,7 @@ private fun RootNavHost( composableWithDefaultTransitions { SavingsIntroScreen( onContinueClick = { - navController.navigate(Routes.SavingsAvailability) + navController.navigateTo(Routes.SavingsAvailability) settingsViewModel.setHasSeenSavingsIntro(true) }, onBackClick = { navController.popBackStack() }, @@ -577,13 +581,13 @@ private fun RootNavHost( SavingsAvailabilityScreen( onBackClick = { navController.popBackStack() }, onCancelClick = { navController.navigateToHome() }, - onContinueClick = { navController.navigate(Routes.SavingsConfirm) }, + onContinueClick = { navController.navigateTo(Routes.SavingsConfirm) }, ) } composableWithDefaultTransitions { SavingsConfirmScreen( - onConfirm = { navController.navigate(Routes.SavingsProgress) }, - onAdvancedClick = { navController.navigate(Routes.SavingsAdvanced) }, + onConfirm = { navController.navigateTo(Routes.SavingsProgress) }, + onAdvancedClick = { navController.navigateTo(Routes.SavingsAdvanced) }, onBackClick = { navController.popBackStack() }, ) } @@ -605,7 +609,7 @@ private fun RootNavHost( composableWithDefaultTransitions { SpendingIntroScreen( onContinueClick = { - navController.navigate(Routes.SpendingAmount) + navController.navigateTo(Routes.SpendingAmount) settingsViewModel.setHasSeenSpendingIntro(true) }, onBackClick = { navController.popBackStack() }, @@ -615,7 +619,7 @@ private fun RootNavHost( SpendingAmountScreen( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, - onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, + onOrderCreated = { navController.navigateTo(Routes.SpendingConfirm) }, toastException = { appViewModel.toast(it) }, toast = { title, description -> appViewModel.toast( @@ -631,9 +635,9 @@ private fun RootNavHost( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, - onLearnMoreClick = { navController.navigate(Routes.TransferLiquidity) }, - onAdvancedClick = { navController.navigate(Routes.SpendingAdvanced) }, - onConfirm = { navController.navigate(Routes.SettingUp) }, + onLearnMoreClick = { navController.navigateTo(Routes.TransferLiquidity) }, + onAdvancedClick = { navController.navigateTo(Routes.SpendingAdvanced) }, + onConfirm = { navController.navigateTo(Routes.SettingUp) }, ) } composableWithDefaultTransitions { @@ -677,7 +681,7 @@ private fun RootNavHost( appViewModel.showSheet(Sheet.Receive) } }, - onAdvanced = { navController.navigate(Routes.FundingAdvanced) }, + onAdvanced = { navController.navigateTo(Routes.FundingAdvanced) }, onBackClick = { navController.popBackStack() }, isGeoBlocked = isGeoBlocked, ) @@ -685,7 +689,7 @@ private fun RootNavHost( composableWithDefaultTransitions { FundingAdvancedScreen( onLnurl = { navController.navigateToScanner() }, - onManual = { navController.navigate(Routes.ExternalNav) }, + onManual = { navController.navigateTo(Routes.ExternalNav) }, onBackClick = { navController.popBackStack() }, ) } @@ -701,7 +705,7 @@ private fun RootNavHost( route = route, savedStateHandle = it.savedStateHandle, viewModel = viewModel, - onNodeConnected = { navController.navigate(Routes.ExternalAmount) }, + onNodeConnected = { navController.navigateTo(Routes.ExternalAmount) }, onScanClick = { navController.navigateToScanner(isCalledForResult = true) }, onBackClick = { navController.popBackStack() }, ) @@ -712,7 +716,7 @@ private fun RootNavHost( ExternalAmountScreen( viewModel = viewModel, - onContinue = { navController.navigate(Routes.ExternalConfirm) }, + onContinue = { navController.navigateTo(Routes.ExternalConfirm) }, onBackClick = { navController.popBackStack() }, ) } @@ -724,7 +728,7 @@ private fun RootNavHost( viewModel = viewModel, onConfirm = { walletViewModel.refreshState() - navController.navigate(Routes.ExternalSuccess) + navController.navigateTo(Routes.ExternalSuccess) }, onBackClick = { navController.popBackStack() }, ) @@ -732,7 +736,7 @@ private fun RootNavHost( composableWithDefaultTransitions { LnurlChannelScreen( route = it.toRoute(), - onConnected = { navController.navigate(Routes.ExternalSuccess) }, + onConnected = { navController.navigateTo(Routes.ExternalSuccess) }, onBack = { navController.popBackStack() }, onClose = { navController.navigateToHome() }, ) @@ -861,7 +865,7 @@ private fun NavGraphBuilder.settings( onBack = { navController.popBackStack() }, onContinue = { settingsViewModel.setQuickPayIntroSeen(true) - navController.navigate(Routes.QuickPaySettings) + navController.navigateTo(Routes.QuickPaySettings) } ) } @@ -920,7 +924,7 @@ private fun NavGraphBuilder.profile( ProfileIntroScreen( onContinue = { settingsViewModel.setHasSeenProfileIntro(true) - navController.navigate(Routes.CreateProfile) + navController.navigateTo(Routes.CreateProfile) }, onBackClick = { navController.popBackStack() } ) @@ -941,7 +945,7 @@ private fun NavGraphBuilder.shop( ShopIntroScreen( onContinue = { settingsViewModel.setHasSeenShopIntro(true) - navController.navigate(Routes.ShopDiscover) + navController.navigateTo(Routes.ShopDiscover) }, onBackClick = { navController.popBackStack() @@ -952,7 +956,7 @@ private fun NavGraphBuilder.shop( ShopDiscoverScreen( onBack = { navController.popBackStack() }, navigateWebView = { page, title -> - navController.navigate(Routes.ShopWebView(page = page, title = title)) + navController.navigateTo(Routes.ShopWebView(page = page, title = title)) } ) } @@ -991,7 +995,7 @@ private fun NavGraphBuilder.generalSettings(navController: NavHostController) { BackgroundPaymentsIntroScreen( onBack = { navController.popBackStack() }, onContinue = { - navController.navigate(Routes.BackgroundPaymentsSettings) + navController.navigateTo(Routes.BackgroundPaymentsSettings) } ) } @@ -1186,7 +1190,7 @@ private fun NavGraphBuilder.activityItem( route = it.toRoute(), onExploreClick = { id -> navController.navigateToActivityExplore(id) }, onChannelClick = { channelId -> - navController.navigate(Routes.ChannelDetail(channelId)) + navController.navigateTo(Routes.ChannelDetail(channelId)) }, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, @@ -1267,7 +1271,7 @@ private fun NavGraphBuilder.recoveryMode( composableWithDefaultTransitions { RecoveryModeScreen( onNavigateToSeed = { - navController.navigate(Routes.RecoveryMnemonic) + navController.navigateTo(Routes.RecoveryMnemonic) }, appViewModel = appViewModel ) @@ -1297,9 +1301,9 @@ private fun NavGraphBuilder.support( onBack = { navController.popBackStack() }, navigateResultScreen = { isSuccess -> if (isSuccess) { - navController.navigate(Routes.ReportIssueSuccess) + navController.navigateTo(Routes.ReportIssueSuccess) } else { - navController.navigate(Routes.ReportIssueFailure) + navController.navigateTo(Routes.ReportIssueFailure) } } ) @@ -1332,7 +1336,7 @@ private fun NavGraphBuilder.widgets( WidgetsIntroScreen( onContinue = { settingsViewModel.setHasSeenWidgetsIntro(true) - navController.navigate(Routes.AddWidget) + navController.navigateTo(Routes.AddWidget) }, onBackClick = { navController.popBackStack() }, ) @@ -1341,12 +1345,12 @@ private fun NavGraphBuilder.widgets( AddWidgetsScreen( onWidgetSelected = { widgetType -> when (widgetType) { - WidgetType.BLOCK -> navController.navigate(Routes.BlocksPreview) - WidgetType.CALCULATOR -> navController.navigate(Routes.CalculatorPreview) - WidgetType.FACTS -> navController.navigate(Routes.FactsPreview) - WidgetType.NEWS -> navController.navigate(Routes.HeadlinesPreview) - WidgetType.PRICE -> navController.navigate(Routes.PricePreview) - WidgetType.WEATHER -> navController.navigate(Routes.WeatherPreview) + WidgetType.BLOCK -> navController.navigateTo(Routes.BlocksPreview) + WidgetType.CALCULATOR -> navController.navigateTo(Routes.CalculatorPreview) + WidgetType.FACTS -> navController.navigateTo(Routes.FactsPreview) + WidgetType.NEWS -> navController.navigateTo(Routes.HeadlinesPreview) + WidgetType.PRICE -> navController.navigateTo(Routes.PricePreview) + WidgetType.WEATHER -> navController.navigateTo(Routes.WeatherPreview) } }, fiatSymbol = LocalCurrencies.current.currencySymbol, @@ -1371,7 +1375,7 @@ private fun NavGraphBuilder.widgets( headlinesViewModel = viewModel, onClose = { navController.navigateToHome() }, onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.HeadlinesEdit) }, + navigateEditWidget = { navController.navigateTo(Routes.HeadlinesEdit) }, ) } composableWithDefaultTransitions { @@ -1382,7 +1386,7 @@ private fun NavGraphBuilder.widgets( headlinesViewModel = viewModel, onBack = { navController.popBackStack() }, navigatePreview = { - navController.navigate(Routes.HeadlinesPreview) + navController.navigateTo(Routes.HeadlinesPreview) } ) } @@ -1398,7 +1402,7 @@ private fun NavGraphBuilder.widgets( factsViewModel = viewModel, onClose = { navController.navigateToHome() }, onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.FactsEdit) }, + navigateEditWidget = { navController.navigateTo(Routes.FactsEdit) }, ) } composableWithDefaultTransitions { @@ -1408,7 +1412,7 @@ private fun NavGraphBuilder.widgets( FactsEditScreen( factsViewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.FactsPreview) } + navigatePreview = { navController.navigateTo(Routes.FactsPreview) } ) } } @@ -1423,7 +1427,7 @@ private fun NavGraphBuilder.widgets( blocksViewModel = viewModel, onClose = { navController.navigateToHome() }, onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.BlocksEdit) }, + navigateEditWidget = { navController.navigateTo(Routes.BlocksEdit) }, ) } composableWithDefaultTransitions { @@ -1433,7 +1437,7 @@ private fun NavGraphBuilder.widgets( BlocksEditScreen( blocksViewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.BlocksPreview) } + navigatePreview = { navController.navigateTo(Routes.BlocksPreview) } ) } } @@ -1448,7 +1452,7 @@ private fun NavGraphBuilder.widgets( weatherViewModel = viewModel, onClose = { navController.navigateToHome() }, onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.WeatherEdit) }, + navigateEditWidget = { navController.navigateTo(Routes.WeatherEdit) }, ) } composableWithDefaultTransitions { @@ -1458,7 +1462,7 @@ private fun NavGraphBuilder.widgets( WeatherEditScreen( weatherViewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.WeatherPreview) } + navigatePreview = { navController.navigateTo(Routes.WeatherPreview) } ) } } @@ -1473,7 +1477,7 @@ private fun NavGraphBuilder.widgets( priceViewModel = viewModel, onClose = { navController.navigateToHome() }, onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.PriceEdit) }, + navigateEditWidget = { navController.navigateTo(Routes.PriceEdit) }, ) } composableWithDefaultTransitions { @@ -1482,7 +1486,7 @@ private fun NavGraphBuilder.widgets( PriceEditScreen( viewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.PricePreview) } + navigatePreview = { navController.navigateTo(Routes.PricePreview) } ) } } @@ -1494,171 +1498,118 @@ private fun NavGraphBuilder.widgets( fun NavController.navigateToHome() { val popped = popBackStack(inclusive = false) if (!popped) { - navigate(Routes.Home) { - popUpTo(graph.startDestinationId) - launchSingleTop = true - } + navigateTo(Routes.Home) { popUpTo(graph.startDestinationId) } } } -fun NavController.navigateToAllActivity() { - navigate(Routes.AllActivity) { - launchSingleTop = true - } -} +fun NavController.navigateToAllActivity() = navigateTo(Routes.AllActivity) /** - * Navigates to the specified route only if not already on that route. + * Navigates to [route] with [launchSingleTop] always enabled to prevent + * duplicate destinations on the back stack (e.g. from double-taps). + * + * Use the optional [builder] to add extra nav options like `popUpTo`. */ -inline fun NavController.navigateIfNotCurrent(route: T) { - val isOnRoute = currentBackStackEntry?.destination?.hasRoute() ?: false - if (!isOnRoute) { - navigate(route) +inline fun NavController.navigateTo( + route: T, + noinline builder: NavOptionsBuilder.() -> Unit = {}, +) { + navigate(route) { + builder() + launchSingleTop = true } } -fun NavController.navigateToGeneralSettings() = navigate( - route = Routes.GeneralSettings, -) +fun NavController.navigateToGeneralSettings() = navigateTo(Routes.GeneralSettings) -fun NavController.navigateToSecuritySettings() = navigate( - route = Routes.SecuritySettings, -) +fun NavController.navigateToSecuritySettings() = navigateTo(Routes.SecuritySettings) -fun NavController.navigateToDisablePin() = navigate( - route = Routes.DisablePin, -) +fun NavController.navigateToDisablePin() = navigateTo(Routes.DisablePin) -fun NavController.navigateToChangePin() = navigate( - route = Routes.ChangePin, -) +fun NavController.navigateToChangePin() = navigateTo(Routes.ChangePin) -fun NavController.navigateToChangePinNew() = navigate( - route = Routes.ChangePinNew, -) +fun NavController.navigateToChangePinNew() = navigateTo(Routes.ChangePinNew) -fun NavController.navigateToChangePinConfirm(newPin: String) = navigate( - route = Routes.ChangePinConfirm(newPin), +fun NavController.navigateToChangePinConfirm(newPin: String) = navigateTo( + Routes.ChangePinConfirm(newPin), ) -fun NavController.navigateToChangePinResult() = navigate( - route = Routes.ChangePinResult, -) +fun NavController.navigateToChangePinResult() = navigateTo(Routes.ChangePinResult) fun NavController.navigateToAuthCheck( showLogoOnPin: Boolean = false, requirePin: Boolean = false, requireBiometrics: Boolean = false, onSuccessActionId: String, - navOptions: NavOptions? = null, -) = navigate( + builder: NavOptionsBuilder.() -> Unit = {}, +) = navigateTo( route = Routes.AuthCheck( showLogoOnPin = showLogoOnPin, requirePin = requirePin, requireBiometrics = requireBiometrics, onSuccessActionId = onSuccessActionId, ), - navOptions = navOptions, + builder = builder, ) -fun NavController.navigateToDefaultUnitSettings() = navigate( - route = Routes.DefaultUnitSettings, -) +fun NavController.navigateToDefaultUnitSettings() = navigateTo(Routes.DefaultUnitSettings) -fun NavController.navigateToLocalCurrencySettings() = navigate( - route = Routes.LocalCurrencySettings, -) +fun NavController.navigateToLocalCurrencySettings() = navigateTo(Routes.LocalCurrencySettings) -fun NavController.navigateToBackupSettings() = navigate( - route = Routes.BackupSettings, -) +fun NavController.navigateToBackupSettings() = navigateTo(Routes.BackupSettings) -fun NavController.navigateToOrderDetail(id: String) = navigate( - route = Routes.OrderDetail(id), -) +fun NavController.navigateToOrderDetail(id: String) = navigateTo(Routes.OrderDetail(id)) -fun NavController.navigateToCjitDetail(id: String) = navigate( - route = Routes.CjitDetail(id), -) +fun NavController.navigateToCjitDetail(id: String) = navigateTo(Routes.CjitDetail(id)) -fun NavController.navigateToDevSettings() = navigate( - route = Routes.DevSettings, -) +fun NavController.navigateToDevSettings() = navigateTo(Routes.DevSettings) -fun NavController.navigateToTransferSavingsIntro() = navigate( - route = Routes.SavingsIntro, -) +fun NavController.navigateToTransferSavingsIntro() = navigateTo(Routes.SavingsIntro) -fun NavController.navigateToTransferSavingsAvailability() = navigate( - route = Routes.SavingsAvailability, -) +fun NavController.navigateToTransferSavingsAvailability() = navigateTo(Routes.SavingsAvailability) -fun NavController.navigateToTransferSpendingIntro() = navigate( - route = Routes.SpendingIntro, -) +fun NavController.navigateToTransferSpendingIntro() = navigateTo(Routes.SpendingIntro) -fun NavController.navigateToTransferSpendingAmount() = navigate( - route = Routes.SpendingAmount, -) +fun NavController.navigateToTransferSpendingAmount() = navigateTo(Routes.SpendingAmount) -fun NavController.navigateToTransferIntro() = navigate( - route = Routes.TransferIntro, -) +fun NavController.navigateToTransferIntro() = navigateTo(Routes.TransferIntro) -fun NavController.navigateToTransferFunding() = navigate( - route = Routes.Funding, -) +fun NavController.navigateToTransferFunding() = navigateTo(Routes.Funding) -fun NavController.navigateToActivityItem(id: String) = navigate( - route = Routes.ActivityDetail(id), -) +fun NavController.navigateToActivityItem(id: String) = navigateTo(Routes.ActivityDetail(id)) -fun NavController.navigateToActivityExplore(id: String) = navigate( - route = Routes.ActivityExplore(id), -) +fun NavController.navigateToActivityExplore(id: String) = navigateTo(Routes.ActivityExplore(id)) fun NavController.navigateToScanner(isCalledForResult: Boolean = false) { if (isCalledForResult) { currentBackStackEntry?.savedStateHandle?.set(SCAN_REQUEST_KEY, true) } - navigate(Routes.QrScanner) + navigateTo(Routes.QrScanner) } -fun NavController.navigateToLogDetail(fileName: String) = navigate( - route = Routes.LogDetail(fileName), -) +fun NavController.navigateToLogDetail(fileName: String) = navigateTo(Routes.LogDetail(fileName)) -fun NavController.navigateToTransactionSpeedSettings() = navigate( - route = Routes.TransactionSpeedSettings, -) +fun NavController.navigateToTransactionSpeedSettings() = navigateTo(Routes.TransactionSpeedSettings) -fun NavController.navigateToCustomFeeSettings() = navigate( - route = Routes.CustomFeeSettings, -) +fun NavController.navigateToCustomFeeSettings() = navigateTo(Routes.CustomFeeSettings) -fun NavController.navigateToWidgetsSettings() = navigate( - route = Routes.WidgetsSettings, -) +fun NavController.navigateToWidgetsSettings() = navigateTo(Routes.WidgetsSettings) -fun NavController.navigateToQuickPaySettings(hasSeenIntro: Boolean = true) = navigate( - route = if (hasSeenIntro) Routes.QuickPaySettings else Routes.QuickPayIntro, -) +fun NavController.navigateToQuickPaySettings(hasSeenIntro: Boolean = true) { + if (hasSeenIntro) { + navigateTo(Routes.QuickPaySettings) + } else { + navigateTo(Routes.QuickPayIntro) + } +} -fun NavController.navigateToTagsSettings() = navigate( - route = Routes.TagsSettings, -) +fun NavController.navigateToTagsSettings() = navigateTo(Routes.TagsSettings) -fun NavController.navigateToLanguageSettings() = navigate( - route = Routes.LanguageSettings, -) +fun NavController.navigateToLanguageSettings() = navigateTo(Routes.LanguageSettings) -fun NavController.navigateToAdvancedSettings() = navigate( - route = Routes.AdvancedSettings, -) +fun NavController.navigateToAdvancedSettings() = navigateTo(Routes.AdvancedSettings) -fun NavController.navigateToAboutSettings() = navigate( - route = Routes.AboutSettings, -) +fun NavController.navigateToAboutSettings() = navigateTo(Routes.AboutSettings) // endregion @Stable diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index db4b23dee..421853b1b 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -237,17 +237,17 @@ private fun OnboardingNav( composable { TermsOfUseScreen( onNavigateToIntro = { - startupNavController.navigate(StartupRoutes.Intro) + startupNavController.navigateTo(StartupRoutes.Intro) } ) } composableWithDefaultTransitions { IntroScreen( onStartClick = { - startupNavController.navigate(StartupRoutes.Slides()) + startupNavController.navigateTo(StartupRoutes.Slides()) }, onSkipClick = { - startupNavController.navigate(StartupRoutes.Slides(StartupRoutes.LAST_SLIDE_INDEX)) + startupNavController.navigateTo(StartupRoutes.Slides(StartupRoutes.LAST_SLIDE_INDEX)) }, ) } @@ -257,7 +257,7 @@ private fun OnboardingNav( OnboardingSlidesScreen( currentTab = route.tab, isGeoBlocked = isGeoBlocked, - onAdvancedSetupClick = { startupNavController.navigate(StartupRoutes.Advanced) }, + onAdvancedSetupClick = { startupNavController.navigateTo(StartupRoutes.Advanced) }, onCreateClick = { scope.launch { runCatching { @@ -270,7 +270,7 @@ private fun OnboardingNav( } }, onRestoreClick = { - startupNavController.navigate( + startupNavController.navigateTo( StartupRoutes.WarningMultipleDevices ) }, @@ -282,7 +282,7 @@ private fun OnboardingNav( startupNavController.popBackStack() }, onConfirmClick = { - startupNavController.navigate(StartupRoutes.Restore) + startupNavController.navigateTo(StartupRoutes.Restore) } ) } diff --git a/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt b/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt index 2e0916aaf..133fdc7d2 100644 --- a/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt +++ b/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt @@ -4,9 +4,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController -import androidx.navigation.navOptions import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel +import to.bitkit.ui.navigateTo import to.bitkit.ui.settingsViewModel @Composable @@ -44,10 +44,9 @@ fun AuthCheckScreen( } AuthCheckAction.NAV_TO_RESET -> { - navController.navigate( - route = Routes.ResetAndRestoreSettings, - navOptions = navOptions { popUpTo(Routes.BackupSettings) } - ) + navController.navigateTo(Routes.ResetAndRestoreSettings) { + popUpTo(Routes.BackupSettings) + } } } }, diff --git a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt index 9a20fb430..10bedbf00 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -45,7 +45,7 @@ import androidx.navigation.compose.rememberNavController import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ui.Routes -import to.bitkit.ui.navigateIfNotCurrent +import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToHome import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.rememberDebouncedClick @@ -102,16 +102,16 @@ fun DrawerMenu( drawerState = drawerState, onClickAddWidget = { if (!hasSeenWidgetsIntro) { - rootNavController.navigateIfNotCurrent(Routes.WidgetsIntro) + rootNavController.navigateTo(Routes.WidgetsIntro) } else { - rootNavController.navigateIfNotCurrent(Routes.AddWidget) + rootNavController.navigateTo(Routes.AddWidget) } }, onClickShop = { if (!hasSeenShopIntro) { - rootNavController.navigateIfNotCurrent(Routes.ShopIntro) + rootNavController.navigateTo(Routes.ShopIntro) } else { - rootNavController.navigateIfNotCurrent(Routes.ShopDiscover) + rootNavController.navigateTo(Routes.ShopDiscover) } }, ) @@ -151,7 +151,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__activity), iconRes = R.drawable.ic_heartbeat, onClick = { - rootNavController.navigateIfNotCurrent(Routes.AllActivity) + rootNavController.navigateTo(Routes.AllActivity) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerActivity") @@ -161,7 +161,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__contacts), iconRes = R.drawable.ic_users, onClick = { - rootNavController.navigateIfNotCurrent(Routes.Contacts) + rootNavController.navigateTo(Routes.Contacts) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerContacts") @@ -171,7 +171,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__profile), iconRes = R.drawable.ic_user_square, onClick = { - rootNavController.navigateIfNotCurrent(Routes.Profile) + rootNavController.navigateTo(Routes.Profile) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerProfile") @@ -201,7 +201,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__settings), iconRes = R.drawable.ic_settings, onClick = { - rootNavController.navigateIfNotCurrent(Routes.Settings) + rootNavController.navigateTo(Routes.Settings) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerSettings") @@ -214,7 +214,7 @@ private fun Menu( modifier = Modifier .fillMaxWidth() .clickableAlpha { - rootNavController.navigateIfNotCurrent(Routes.AppStatus) + rootNavController.navigateTo(Routes.AppStatus) scope.launch { drawerState.close() } } ) { diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index b27c1c367..4bbc8aa3a 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -24,10 +24,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.sheets.SendRoute -import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.Colors diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index 9fcd1b9a9..74b480188 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -21,6 +21,7 @@ import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsTextButtonRow +import to.bitkit.ui.navigateTo import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -49,14 +50,14 @@ fun DevSettingsScreen( .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) ) { - SettingsButtonRow("Fee Settings") { navController.navigate(Routes.FeeSettings) } - SettingsButtonRow("Channel Orders") { navController.navigate(Routes.ChannelOrdersSettings) } - SettingsButtonRow("LDK") { navController.navigate(Routes.LdkDebug) } - SettingsButtonRow("VSS") { navController.navigate(Routes.VssDebug) } - SettingsButtonRow("Probing Tool") { navController.navigate(Routes.ProbingTool) } + SettingsButtonRow("Fee Settings") { navController.navigateTo(Routes.FeeSettings) } + SettingsButtonRow("Channel Orders") { navController.navigateTo(Routes.ChannelOrdersSettings) } + SettingsButtonRow("LDK") { navController.navigateTo(Routes.LdkDebug) } + SettingsButtonRow("VSS") { navController.navigateTo(Routes.VssDebug) } + SettingsButtonRow("Probing Tool") { navController.navigateTo(Routes.ProbingTool) } SectionHeader("LOGS") - SettingsButtonRow("Logs") { navController.navigate(Routes.Logs) } + SettingsButtonRow("Logs") { navController.navigateTo(Routes.Logs) } SettingsTextButtonRow( title = "Export Logs", onClick = { @@ -71,7 +72,7 @@ fun DevSettingsScreen( if (Env.network == Network.REGTEST) { SectionHeader("REGTEST") - SettingsButtonRow("Blocktank Regtest") { navController.navigate(Routes.RegtestSettings) } + SettingsButtonRow("Blocktank Regtest") { navController.navigateTo(Routes.RegtestSettings) } } SectionHeader("APP CACHE") diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 1543b821d..8d92cfe56 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -87,6 +87,7 @@ import to.bitkit.ui.components.TopBarSpacer import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.WalletBalanceView import to.bitkit.ui.currencyViewModel +import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToActivityItem import to.bitkit.ui.navigateToAllActivity import to.bitkit.ui.navigateToTransferFunding @@ -170,7 +171,7 @@ fun HomeScreen( onClickSuggestion = { suggestion -> when (suggestion) { Suggestion.BUY -> { - rootNavController.navigate(Routes.BuyIntro) + rootNavController.navigateTo(Routes.BuyIntro) } Suggestion.LIGHTNING -> { @@ -190,7 +191,7 @@ fun HomeScreen( } Suggestion.SUPPORT -> { - rootNavController.navigate(Routes.Support) + rootNavController.navigateTo(Routes.Support) } Suggestion.INVITE -> { @@ -203,51 +204,51 @@ fun HomeScreen( } Suggestion.PROFILE -> { - rootNavController.navigate(Routes.Profile) + rootNavController.navigateTo(Routes.Profile) } Suggestion.SHOP -> { if (!hasSeenShopIntro) { - rootNavController.navigate(Routes.ShopIntro) + rootNavController.navigateTo(Routes.ShopIntro) } else { - rootNavController.navigate(Routes.ShopDiscover) + rootNavController.navigateTo(Routes.ShopDiscover) } } Suggestion.QUICK_PAY -> { if (!quickPayIntroSeen) { - rootNavController.navigate(Routes.QuickPayIntro) + rootNavController.navigateTo(Routes.QuickPayIntro) } else { - rootNavController.navigate(Routes.QuickPaySettings) + rootNavController.navigateTo(Routes.QuickPaySettings) } } Suggestion.NOTIFICATIONS -> { if (bgPaymentsIntroSeen) { - rootNavController.navigate(Routes.BackgroundPaymentsSettings) + rootNavController.navigateTo(Routes.BackgroundPaymentsSettings) } else { - rootNavController.navigate(Routes.BackgroundPaymentsIntro) + rootNavController.navigateTo(Routes.BackgroundPaymentsIntro) } } } }, onClickAddWidget = { if (!hasSeenWidgetsIntro) { - rootNavController.navigate(Routes.WidgetsIntro) + rootNavController.navigateTo(Routes.WidgetsIntro) } else { - rootNavController.navigate(Routes.AddWidget) + rootNavController.navigateTo(Routes.AddWidget) } }, onClickEditWidgetList = homeViewModel::onClickEditWidgetList, onClickEditWidget = { widgetType -> homeViewModel.disableEditMode() when (widgetType) { - WidgetType.BLOCK -> rootNavController.navigate(Routes.BlocksPreview) - WidgetType.CALCULATOR -> rootNavController.navigate(Routes.CalculatorPreview) - WidgetType.FACTS -> rootNavController.navigate(Routes.FactsPreview) - WidgetType.NEWS -> rootNavController.navigate(Routes.HeadlinesPreview) - WidgetType.PRICE -> rootNavController.navigate(Routes.PricePreview) - WidgetType.WEATHER -> rootNavController.navigate(Routes.WeatherPreview) + WidgetType.BLOCK -> rootNavController.navigateTo(Routes.BlocksPreview) + WidgetType.CALCULATOR -> rootNavController.navigateTo(Routes.CalculatorPreview) + WidgetType.FACTS -> rootNavController.navigateTo(Routes.FactsPreview) + WidgetType.NEWS -> rootNavController.navigateTo(Routes.HeadlinesPreview) + WidgetType.PRICE -> rootNavController.navigateTo(Routes.PricePreview) + WidgetType.WEATHER -> rootNavController.navigateTo(Routes.WeatherPreview) } }, onClickDeleteWidget = { widgetType -> @@ -343,7 +344,7 @@ private fun Content( sats = balances.totalOnchainSats.toLong(), icon = painterResource(id = R.drawable.ic_btc_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Savings) } + .clickableAlpha { walletNavController.navigateTo(Routes.Savings) } .padding(vertical = 4.dp) .testTag("ActivitySavings") ) @@ -354,7 +355,7 @@ private fun Content( sats = balances.totalLightningSats.toLong(), icon = painterResource(id = R.drawable.ic_ln_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Spending) } + .clickableAlpha { walletNavController.navigateTo(Routes.Spending) } .padding(vertical = 4.dp) .testTag("ActivitySpending") ) @@ -474,7 +475,9 @@ private fun Content( icon = banner.type.icon, onClick = { when (banner.type) { - ActivityBannerType.SPENDING -> rootNavController.navigate(Routes.SettingUp) + ActivityBannerType.SPENDING -> rootNavController.navigateTo( + Routes.SettingUp + ) ActivityBannerType.SAVINGS -> Unit } }, @@ -631,7 +634,7 @@ private fun TopBar( TopAppBar( title = {}, actions = { - AppStatus(onClick = { rootNavController.navigate(Routes.AppStatus) }) + AppStatus(onClick = { rootNavController.navigateTo(Routes.AppStatus) }) HorizontalSpacer(4.dp) IconButton( onClick = { scope.launch { drawerState.open() } }, 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 ab026d9a6..dd19fc938 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 @@ -18,6 +18,7 @@ import androidx.navigation.compose.rememberNavController import kotlinx.serialization.Serializable import to.bitkit.repositories.LightningState import to.bitkit.repositories.WalletState +import to.bitkit.ui.navigateTo import to.bitkit.ui.openNotificationSettings import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.shared.modifiers.sheetHeight @@ -70,20 +71,20 @@ fun ReceiveSheet( lightningState = lightningState, onClickReceiveCjit = { if (lightningState.isGeoBlocked) { - navController.navigate(ReceiveRoute.GeoBlock) + navController.navigateTo(ReceiveRoute.GeoBlock) } else { showCreateCjit.value = true - navController.navigate(ReceiveRoute.Amount) + navController.navigateTo(ReceiveRoute.Amount) } }, - onClickEditInvoice = { navController.navigate(ReceiveRoute.EditInvoice) }, + onClickEditInvoice = { navController.navigateTo(ReceiveRoute.EditInvoice) }, ) } composableWithDefaultTransitions { ReceiveAmountScreen( onCjitCreated = { entry -> cjitEntryDetails.value = entry - navController.navigate(ReceiveRoute.Confirm) + navController.navigateTo(ReceiveRoute.Confirm) }, onBack = { navController.popBackStack() }, ) @@ -98,10 +99,10 @@ fun ReceiveSheet( cjitEntryDetails.value?.let { entryDetails -> ReceiveConfirmScreen( entry = entryDetails, - onLearnMore = { navController.navigate(ReceiveRoute.Liquidity) }, + onLearnMore = { navController.navigateTo(ReceiveRoute.Liquidity) }, onContinue = { invoice -> cjitInvoice.value = invoice - navController.navigate(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } + navController.navigateTo(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } }, onBack = { navController.popBackStack() }, ) @@ -111,10 +112,10 @@ fun ReceiveSheet( cjitEntryDetails.value?.let { entryDetails -> ReceiveConfirmScreen( entry = entryDetails, - onLearnMore = { navController.navigate(ReceiveRoute.LiquidityAdditional) }, + onLearnMore = { navController.navigateTo(ReceiveRoute.LiquidityAdditional) }, onContinue = { invoice -> cjitInvoice.value = invoice - navController.navigate(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } + navController.navigateTo(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } }, isAdditional = true, onBack = { navController.popBackStack() }, @@ -158,12 +159,12 @@ fun ReceiveSheet( walletUiState = walletUiState, onBack = { navController.popBackStack() }, updateInvoice = wallet::updateBip21Invoice, - onClickAddTag = { navController.navigate(ReceiveRoute.AddTag) }, + onClickAddTag = { navController.navigateTo(ReceiveRoute.AddTag) }, onClickTag = wallet::removeTag, onDescriptionUpdate = wallet::updateBip21Description, navigateReceiveConfirm = { entry -> cjitEntryDetails.value = entry - navController.navigate(ReceiveRoute.ConfirmIncreaseInbound) + navController.navigateTo(ReceiveRoute.ConfirmIncreaseInbound) } ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt index dfba44064..db5235cca 100644 --- a/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt @@ -24,6 +24,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue +import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar @@ -44,25 +45,25 @@ fun AdvancedSettingsScreen( selectedAddressTypeName = selectedAddressTypeName, onBack = { navController.popBackStack() }, onCoinSelectionClick = { - navController.navigate(Routes.CoinSelectPreference) + navController.navigateTo(Routes.CoinSelectPreference) }, onAddressTypePreferenceClick = { - navController.navigate(Routes.AddressTypePreference) + navController.navigateTo(Routes.AddressTypePreference) }, onLightningConnectionsClick = { - navController.navigate(Routes.LightningConnections) + navController.navigateTo(Routes.LightningConnections) }, onLightningNodeClick = { - navController.navigate(Routes.NodeInfo) + navController.navigateTo(Routes.NodeInfo) }, onElectrumServerClick = { - navController.navigate(Routes.ElectrumConfig) + navController.navigateTo(Routes.ElectrumConfig) }, onRgsServerClick = { - navController.navigate(Routes.RgsServer) + navController.navigateTo(Routes.RgsServer) }, onAddressViewerClick = { - navController.navigate(Routes.AddressViewer) + navController.navigateTo(Routes.AddressViewer) }, onSuggestionsResetClick = { showResetSuggestionsDialog = true }, onResetSuggestionsDialogConfirm = { diff --git a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt index 329dc6b1b..cbafab83e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt @@ -42,6 +42,7 @@ import to.bitkit.ui.components.FillWidth import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow +import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToAuthCheck import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -73,7 +74,7 @@ fun BackupSettingsScreen( if (isPinEnabled) { navController.navigateToAuthCheck(onSuccessActionId = AuthCheckAction.NAV_TO_RESET) } else { - navController.navigate(Routes.ResetAndRestoreSettings) + navController.navigateTo(Routes.ResetAndRestoreSettings) } }, onRetryBackup = { category -> viewModel.retryBackup(category) }, diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index f08ac96af..587df3ef9 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -30,6 +30,7 @@ import to.bitkit.models.Toast import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SettingsButtonRow +import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToAboutSettings import to.bitkit.ui.navigateToAdvancedSettings import to.bitkit.ui.navigateToBackupSettings @@ -62,7 +63,7 @@ fun SettingsScreen( onSecurityClick = { navController.navigateToSecuritySettings() }, onBackupClick = { navController.navigateToBackupSettings() }, onAdvancedClick = { navController.navigateToAdvancedSettings() }, - onSupportClick = { navController.navigate(Routes.Support) }, + onSupportClick = { navController.navigateTo(Routes.Support) }, onAboutClick = { navController.navigateToAboutSettings() }, onDevClick = { navController.navigateToDevSettings() }, onBackClick = { navController.popBackStack() }, diff --git a/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt b/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt index 3f1e8c174..2640b4b51 100644 --- a/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt @@ -37,6 +37,7 @@ import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.HorizontalSpacer import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.navigateTo import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -59,10 +60,10 @@ fun AppStatusScreen( uiState = uiState, onBack = { navController.popBackStack() }, onInternetClick = { context.startActivityAppSettings() }, - onElectrumClick = { navController.navigate(Routes.ElectrumConfig) }, - onNodeClick = { navController.navigate(Routes.NodeInfo) }, - onChannelsClick = { navController.navigate(Routes.LightningConnections) }, - onBackupClick = { navController.navigate(Routes.BackupSettings) }, + onElectrumClick = { navController.navigateTo(Routes.ElectrumConfig) }, + onNodeClick = { navController.navigateTo(Routes.NodeInfo) }, + onChannelsClick = { navController.navigateTo(Routes.LightningConnections) }, + onBackupClick = { navController.navigateTo(Routes.BackupSettings) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt index 1e188b51c..1bafb0693 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt @@ -24,6 +24,7 @@ import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.Routes import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue +import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToDefaultUnitSettings import to.bitkit.ui.navigateToLanguageSettings import to.bitkit.ui.navigateToLocalCurrencySettings @@ -69,9 +70,9 @@ fun GeneralSettingsScreen( onLanguageSettingsClick = { navController.navigateToLanguageSettings() }, onBgPaymentsClick = { if (bgPaymentsIntroSeen || notificationsGranted) { - navController.navigate(Routes.BackgroundPaymentsSettings) + navController.navigateTo(Routes.BackgroundPaymentsSettings) } else { - navController.navigate(Routes.BackgroundPaymentsIntro) + navController.navigateTo(Routes.BackgroundPaymentsIntro) } }, selectedLanguage = languageUiState.selectedLanguage.displayName, diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 9e4168fb9..708b258e7 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -70,6 +70,7 @@ import to.bitkit.ui.components.MoneyCaptionB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.navigateTo import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -117,7 +118,7 @@ fun ChannelDetailScreen( }, onSupport = { order, channel -> contactSupport(order, channel, uiState.nodeId, context) }, onCloseConnection = { channelDetailId -> - navController.navigate(Routes.CloseConnection(channelId = channelDetailId)) + navController.navigateTo(Routes.CloseConnection(channelId = channelDetailId)) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index 1f5cfd43c..d5bad2e2f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -56,6 +56,7 @@ import to.bitkit.ui.components.SyncNodeView import to.bitkit.ui.components.TertiaryButton import to.bitkit.ui.components.Title import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToTransferFunding import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn @@ -94,7 +95,7 @@ fun LightningConnectionsScreen( viewModel.zipLogsForSharing { uri -> context.shareZipFile(uri) } }, onClickChannel = { channelUi -> - navController.navigate(Routes.ChannelDetail(channelUi.details.channelId)) + navController.navigateTo(Routes.ChannelDetail(channelUi.details.channelId)) }, onRefresh = { viewModel.onPullToRefresh() diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt index 0999d011d..1c68a0e10 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import androidx.navigation.navOptions import to.bitkit.R import to.bitkit.ui.Routes import to.bitkit.ui.components.AuthCheckAction @@ -39,8 +38,7 @@ fun DisablePinScreen( navController.navigateToAuthCheck( requirePin = true, onSuccessActionId = AuthCheckAction.DISABLE_PIN, - navOptions = navOptions { popUpTo(Routes.SecuritySettings) }, - ) + ) { popUpTo(Routes.SecuritySettings) } }, onBackClick = { navController.popBackStack() }, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt b/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt index 488c6a56a..d56085155 100644 --- a/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt @@ -23,6 +23,7 @@ import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.settings.Links import to.bitkit.ui.components.settings.SettingsButtonRow +import to.bitkit.ui.navigateTo import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -37,12 +38,12 @@ fun SupportScreen( Content( onBack = { navController.popBackStack() }, - onClickReportIssue = { navController.navigate(Routes.ReportIssue) }, + onClickReportIssue = { navController.navigateTo(Routes.ReportIssue) }, onClickHelpCenter = { val intent = Intent(Intent.ACTION_VIEW, Env.BITKIT_HELP_CENTER.toUri()) context.startActivity(intent) }, - onClickAppStatus = { navController.navigate(Routes.AppStatus) }, + onClickAppStatus = { navController.navigateTo(Routes.AppStatus) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index 6447828c4..d456637be 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.semantics.role import androidx.compose.ui.unit.Constraints import kotlinx.coroutines.launch -private const val CLICK_DEBOUNCE_MS = 300L +private const val CLICK_DEBOUNCE_MS = 500L private class ClickDebouncer(private val debounceMs: Long = CLICK_DEBOUNCE_MS) { private var lastClickTime = 0L diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt index 0ccaf924b..2598137cf 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt @@ -17,6 +17,7 @@ import kotlinx.serialization.Serializable import to.bitkit.ui.LocalBalances import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.SheetSize +import to.bitkit.ui.navigateTo import to.bitkit.ui.settings.backups.BackupContract import to.bitkit.ui.settings.backups.BackupIntroScreen import to.bitkit.ui.settings.backups.BackupNavSheetViewModel @@ -54,22 +55,25 @@ fun BackupSheet( LaunchedEffect(Unit) { viewModel.effects.collect { effect -> when (effect) { - BackupContract.SideEffect.NavigateToShowPassphrase -> navController.navigate(BackupRoute.ShowPassphrase) - BackupContract.SideEffect.NavigateToConfirmMnemonic -> navController.navigate( + BackupContract.SideEffect.NavigateToShowPassphrase -> navController.navigateTo( + BackupRoute.ShowPassphrase + ) + + BackupContract.SideEffect.NavigateToConfirmMnemonic -> navController.navigateTo( BackupRoute.ConfirmMnemonic ) - BackupContract.SideEffect.NavigateToConfirmPassphrase -> navController.navigate( + BackupContract.SideEffect.NavigateToConfirmPassphrase -> navController.navigateTo( BackupRoute.ConfirmPassphrase ) - BackupContract.SideEffect.NavigateToWarning -> navController.navigate(BackupRoute.Warning) - BackupContract.SideEffect.NavigateToSuccess -> navController.navigate(BackupRoute.Success) - BackupContract.SideEffect.NavigateToMultipleDevices -> navController.navigate( + BackupContract.SideEffect.NavigateToWarning -> navController.navigateTo(BackupRoute.Warning) + BackupContract.SideEffect.NavigateToSuccess -> navController.navigateTo(BackupRoute.Success) + BackupContract.SideEffect.NavigateToMultipleDevices -> navController.navigateTo( BackupRoute.MultipleDevices ) - BackupContract.SideEffect.NavigateToMetadata -> navController.navigate(BackupRoute.Metadata) + BackupContract.SideEffect.NavigateToMetadata -> navController.navigateTo(BackupRoute.Metadata) BackupContract.SideEffect.DismissSheet -> currentOnDismiss() } } @@ -89,7 +93,7 @@ fun BackupSheet( BackupIntroScreen( hasFunds = LocalBalances.current.totalSats > 0u, onClose = currentOnDismiss, - onConfirm = { navController.navigate(BackupRoute.ShowMnemonic) }, + onConfirm = { navController.navigateTo(BackupRoute.ShowMnemonic) }, ) } composableWithDefaultTransitions { diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt index 6562e2dca..dd3bc8d99 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt @@ -14,6 +14,7 @@ import androidx.navigation.compose.rememberNavController import to.bitkit.R import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.ui.components.Sheet +import to.bitkit.ui.navigateTo import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.utils.composableWithDefaultTransitions import to.bitkit.viewmodels.AppViewModel @@ -46,7 +47,7 @@ fun GiftSheet( viewModel.navigationEvent.collect { route -> when (route) { is GiftRoute.Success -> appViewModel.hideSheet() - else -> navController.navigate(route) { + else -> navController.navigateTo(route) { popUpTo(GiftRoute.Loading) { inclusive = false } } } diff --git a/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt index dd51453ee..6c9fafd20 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt @@ -10,6 +10,7 @@ import androidx.navigation.toRoute import kotlinx.serialization.Serializable import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.SheetSize +import to.bitkit.ui.navigateTo import to.bitkit.ui.settings.pin.PinBiometricsScreen import to.bitkit.ui.settings.pin.PinChooseScreen import to.bitkit.ui.settings.pin.PinConfirmScreen @@ -39,14 +40,14 @@ fun PinSheet( composableWithDefaultTransitions { PinPromptScreen( showLaterButton = it.toRoute().showLaterButton, - onContinue = { navController.navigate(PinRoute.Choose) }, + onContinue = { navController.navigateTo(PinRoute.Choose) }, onLater = onDismiss, ) } composableWithDefaultTransitions { PinChooseScreen( onPinChosen = { pin -> - navController.navigate(PinRoute.Confirm(pin)) + navController.navigateTo(PinRoute.Confirm(pin)) }, onBack = { navController.popBackStack() }, ) @@ -54,16 +55,16 @@ fun PinSheet( composableWithDefaultTransitions { PinConfirmScreen( originalPin = it.toRoute().pin, - onPinConfirmed = { navController.navigate(PinRoute.Biometrics) }, + onPinConfirmed = { navController.navigateTo(PinRoute.Biometrics) }, onBack = { navController.popBackStack() }, ) } composableWithDefaultTransitions { PinBiometricsScreen( onContinue = { isBioOn -> - navController.navigate(PinRoute.Result(isBioOn)) + navController.navigateTo(PinRoute.Result(isBioOn)) }, - onSkip = { navController.navigate(PinRoute.Result(isBioOn = false)) }, + onSkip = { navController.navigateTo(PinRoute.Result(isBioOn = false)) }, onBack = onDismiss, ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt index 5af5e4449..86bd6e479 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt @@ -20,6 +20,7 @@ import kotlinx.serialization.Serializable import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType +import to.bitkit.ui.navigateTo import to.bitkit.ui.screens.scanner.QrScanningScreen import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.screens.wallets.send.PIN_CHECK_RESULT_KEY @@ -73,26 +74,28 @@ fun SendSheet( LaunchedEffect(appViewModel, navController) { appViewModel.sendEffect.collect { when (it) { - is SendEffect.NavigateToAmount -> navController.navigate(SendRoute.Amount) - is SendEffect.NavigateToAddress -> navController.navigate(SendRoute.Address) - is SendEffect.NavigateToScan -> navController.navigate(SendRoute.QrScanner) - is SendEffect.NavigateToCoinSelection -> navController.navigate(SendRoute.CoinSelection) - is SendEffect.NavigateToConfirm -> navController.navigate(SendRoute.Confirm) + is SendEffect.NavigateToAmount -> navController.navigateTo(SendRoute.Amount) + is SendEffect.NavigateToAddress -> navController.navigateTo(SendRoute.Address) + is SendEffect.NavigateToScan -> navController.navigateTo(SendRoute.QrScanner) + is SendEffect.NavigateToCoinSelection -> navController.navigateTo(SendRoute.CoinSelection) + is SendEffect.NavigateToConfirm -> navController.navigateTo(SendRoute.Confirm) is SendEffect.PopBack -> navController.popBackStack(it.route, inclusive = false) is SendEffect.PaymentSuccess -> { appViewModel.clearClipboardForAutoRead() - navController.navigate(SendRoute.Success) { + navController.navigateTo(SendRoute.Success) { popUpTo(navController.graph.id) { inclusive = true } } } - is SendEffect.NavigateToQuickPay -> navController.navigate(SendRoute.QuickPay) - is SendEffect.NavigateToWithdrawConfirm -> navController.navigate(SendRoute.WithdrawConfirm) - is SendEffect.NavigateToWithdrawError -> navController.navigate(SendRoute.WithdrawError) - is SendEffect.NavigateToFee -> navController.navigate(SendRoute.FeeRate) - is SendEffect.NavigateToFeeCustom -> navController.navigate(SendRoute.FeeCustom) - is SendEffect.NavigateToComingSoon -> navController.navigate(SendRoute.ComingSoon) - is SendEffect.NavigateToPending -> navController.navigate( + is SendEffect.NavigateToQuickPay -> navController.navigateTo(SendRoute.QuickPay) + is SendEffect.NavigateToWithdrawConfirm -> navController.navigateTo( + SendRoute.WithdrawConfirm + ) + is SendEffect.NavigateToWithdrawError -> navController.navigateTo(SendRoute.WithdrawError) + is SendEffect.NavigateToFee -> navController.navigateTo(SendRoute.FeeRate) + is SendEffect.NavigateToFeeCustom -> navController.navigateTo(SendRoute.FeeCustom) + is SendEffect.NavigateToComingSoon -> navController.navigateTo(SendRoute.ComingSoon) + is SendEffect.NavigateToPending -> navController.navigateTo( SendRoute.Pending(it.paymentHash, it.amount) ) { popUpTo(startDestination) { inclusive = true } } } @@ -187,9 +190,9 @@ fun SendSheet( } }, onEvent = { e -> appViewModel.setSendEvent(e) }, - onClickAddTag = { navController.navigate(SendRoute.AddTag) }, + onClickAddTag = { navController.navigateTo(SendRoute.AddTag) }, onClickTag = { tag -> appViewModel.removeTag(tag) }, - onNavigateToPin = { navController.navigate(SendRoute.PinCheck) }, + onNavigateToPin = { navController.navigateTo(SendRoute.PinCheck) }, ) } composableWithDefaultTransitions { @@ -218,8 +221,8 @@ fun SendSheet( WithdrawErrorScreen( uiState = uiState, onBack = { navController.popBackStack() }, - onClickScan = { navController.navigate(SendRoute.QrScanner) }, - onClickSupport = { navController.navigate(SendRoute.Support) }, + onClickScan = { navController.navigateTo(SendRoute.QrScanner) }, + onClickSupport = { navController.navigateTo(SendRoute.Support) }, ) } // TODO navigate to main support screen, not inside SEND sheet @@ -269,12 +272,12 @@ fun SendSheet( ) }, onPaymentPending = { paymentHash, amount -> - navController.navigate(SendRoute.Pending(paymentHash, amount)) { + navController.navigateTo(SendRoute.Pending(paymentHash, amount)) { popUpTo(startDestination) { inclusive = true } } }, onShowError = { errorMessage -> - navController.navigate(SendRoute.Error(errorMessage)) + navController.navigateTo(SendRoute.Error(errorMessage)) } ) } @@ -294,7 +297,7 @@ fun SendSheet( ) }, onPaymentError = { - navController.navigate(SendRoute.Error()) { + navController.navigateTo(SendRoute.Error()) { popUpTo { inclusive = true } } }, @@ -314,7 +317,7 @@ fun SendSheet( SendErrorScreen( message = route.message, onRetry = { - navController.navigate(SendRoute.Recipient) { + navController.navigateTo(SendRoute.Recipient) { popUpTo(navController.graph.id) { inclusive = true } } }, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 5349c83ac..05449ac2c 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -10,8 +10,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.NavOptions -import androidx.navigation.navOptions import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.ActivityFilter import com.synonym.bitkitcore.FeeRates @@ -2326,9 +2324,7 @@ class AppViewModel @Inject constructor( mainScreenEffect( MainScreenEffect.Navigate( route = Routes.RecoveryMode, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } + clearStack = true, ) ) return@launch @@ -2368,9 +2364,7 @@ class AppViewModel @Inject constructor( mainScreenEffect( MainScreenEffect.Navigate( route = Routes.CriticalUpdate, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } + clearStack = true, ) ) } @@ -2464,7 +2458,7 @@ sealed class SendEffect { sealed class MainScreenEffect { data class Navigate( val route: Routes, - val navOptions: NavOptions? = null, + val clearStack: Boolean = false, ) : MainScreenEffect() data object WipeWallet : MainScreenEffect() From 194540a1b9b400b643a2b34dda85e4e051a82e76 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Mar 2026 19:59:38 +0100 Subject: [PATCH 11/20] fix: stable debounced click lambda Co-Authored-By: Claude Opus 4.6 --- .../ui/shared/modifiers/ClickableAlpha.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index d456637be..e56def6b8 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -4,8 +4,10 @@ import android.os.SystemClock import androidx.compose.animation.core.Animatable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode @@ -30,19 +32,22 @@ private const val CLICK_DEBOUNCE_MS = 500L private class ClickDebouncer(private val debounceMs: Long = CLICK_DEBOUNCE_MS) { private var lastClickTime = 0L - fun tryClick(onClick: () -> Unit) { + fun tryClick(onClick: () -> Unit): Boolean { val now = SystemClock.uptimeMillis() if (now - lastClickTime >= debounceMs) { lastClickTime = now onClick() + return true } + return false } } @Composable fun rememberDebouncedClick(debounceMs: Long = CLICK_DEBOUNCE_MS, onClick: () -> Unit): () -> Unit { val debouncer = remember { ClickDebouncer(debounceMs) } - return { debouncer.tryClick(onClick) } + val currentOnClick by rememberUpdatedState(onClick) + return remember(debouncer) { { debouncer.tryClick(currentOnClick) } } } /** @@ -99,10 +104,11 @@ private class ClickableAlphaNode( } }, onTap = { - debouncer.tryClick(onClick) - coroutineScope.launch { - animatable.animateTo(pressedAlpha) - animatable.animateTo(1f) + if (debouncer.tryClick(onClick)) { + coroutineScope.launch { + animatable.animateTo(pressedAlpha) + animatable.animateTo(1f) + } } } ) From 98733579982804cdec1f46f8e5ba7936f5c8359a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Mar 2026 19:59:44 +0100 Subject: [PATCH 12/20] fix: hoist regex and use invokeOnCompletion Co-Authored-By: Claude Opus 4.6 --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 05449ac2c..4d723dfc6 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1003,13 +1003,9 @@ class AppViewModel @Inject constructor( activeScanInput = normalized Logger.debug("Scan from '${source.label}': '$scanId'", context = TAG) activeScanJob = viewModelScope.launch { - try { - if (delayMs > 0) delay(delayMs) - handleScan(data) - } finally { - activeScanInput = null - } - } + if (delayMs > 0) delay(delayMs) + handleScan(data) + }.also { it.invokeOnCompletion { activeScanInput = null } } } private fun onAddressContinue(data: String) { @@ -2336,13 +2332,8 @@ class AppViewModel @Inject constructor( } // TODO Temporary fix while these schemes can't be decoded https://github.com/synonymdev/bitkit-core/issues/70 - private fun String.removeLightningSchemes(): String { - return this - .replace(Regex("^lightning:", RegexOption.IGNORE_CASE), "") - .replace(Regex("^lnurl:", RegexOption.IGNORE_CASE), "") - .replace(Regex("^lnurlw:", RegexOption.IGNORE_CASE), "") - .replace(Regex("^lnurlc:", RegexOption.IGNORE_CASE), "") - .replace(Regex("^lnurlp:", RegexOption.IGNORE_CASE), "") + private fun String.removeLightningSchemes(): String = LIGHTNING_SCHEME_PATTERNS.fold(this) { acc, regex -> + acc.replace(regex, "") } fun checkTimedSheets() = timedSheetManager.onHomeScreenEntered() @@ -2382,6 +2373,8 @@ class AppViewModel @Inject constructor( companion object { private const val TAG = "AppViewModel" + private val LIGHTNING_SCHEME_PATTERNS = listOf("lightning", "lnurl", "lnurlw", "lnurlc", "lnurlp") + .map { Regex("^$it:", RegexOption.IGNORE_CASE) } private const val SEND_AMOUNT_WARNING_THRESHOLD = 100.0 private const val TEN_USD = 10 private const val MAX_BALANCE_FRACTION = 0.5 From e5e18dd27083efda42158dd84bf49cd056642a58 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Mar 2026 20:00:17 +0100 Subject: [PATCH 13/20] fix: condense navigateToQuickPaySettings Co-Authored-By: Claude Opus 4.6 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 99da8b69d..b55fe7856 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -1595,13 +1595,8 @@ fun NavController.navigateToCustomFeeSettings() = navigateTo(Routes.CustomFeeSet fun NavController.navigateToWidgetsSettings() = navigateTo(Routes.WidgetsSettings) -fun NavController.navigateToQuickPaySettings(hasSeenIntro: Boolean = true) { - if (hasSeenIntro) { - navigateTo(Routes.QuickPaySettings) - } else { - navigateTo(Routes.QuickPayIntro) - } -} +fun NavController.navigateToQuickPaySettings(hasSeenIntro: Boolean = true) = + navigateTo(if (hasSeenIntro) Routes.QuickPaySettings else Routes.QuickPayIntro) fun NavController.navigateToTagsSettings() = navigateTo(Routes.TagsSettings) From 9732dffeb3b1e3d2d57f3bec2c67257103172ee2 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Mar 2026 20:03:51 +0100 Subject: [PATCH 14/20] fix: suppress complexity detekt warnings Co-Authored-By: Claude Opus 4.6 --- app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt | 1 + app/src/main/java/to/bitkit/ui/components/Money.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt index fe8505b24..0ba472332 100644 --- a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt +++ b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt @@ -35,6 +35,7 @@ import to.bitkit.ui.shared.modifiers.swipeToHide import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +@Suppress("CyclomaticComplexMethod") @Composable fun BalanceHeaderView( sats: Long, diff --git a/app/src/main/java/to/bitkit/ui/components/Money.kt b/app/src/main/java/to/bitkit/ui/components/Money.kt index 920efa6c3..50e2999dd 100644 --- a/app/src/main/java/to/bitkit/ui/components/Money.kt +++ b/app/src/main/java/to/bitkit/ui/components/Money.kt @@ -109,6 +109,7 @@ fun MoneyCaptionB( } } +@Suppress("CyclomaticComplexMethod") @Composable fun rememberMoneyText( sats: Long, From d5e77886e6e1f2334613ac0c96702c8e2518935a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 10 Mar 2026 21:21:27 +0100 Subject: [PATCH 15/20] fix: address PR review feedback Co-Authored-By: Claude Opus 4.6 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 3 ++- .../java/to/bitkit/ui/scaffold/AppTopBar.kt | 2 +- .../ui/screens/transfer/FundingScreen.kt | 4 +-- .../ui/shared/modifiers/ClickableAlpha.kt | 12 +++++---- .../java/to/bitkit/viewmodels/AppViewModel.kt | 26 +++++++++++-------- 5 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index b55fe7856..3524974db 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -193,6 +193,7 @@ import to.bitkit.viewmodels.RestoreState import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel +import kotlin.time.Duration.Companion.milliseconds @Suppress("CyclomaticComplexMethod") @Composable @@ -1215,7 +1216,7 @@ private fun NavGraphBuilder.qrScanner( QrScanningScreen(navController = navController) { qrCode -> appViewModel.onScanResult( data = qrCode, - delayMs = TRANSITION_SHEET_MS, + startDelay = TRANSITION_SHEET_MS.milliseconds, ) } } diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt index 535c249e7..da14f46a3 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt @@ -98,8 +98,8 @@ fun DrawerNavIcon( val drawerState = LocalDrawerState.current val scope = androidx.compose.runtime.rememberCoroutineScope() + val debouncedClick = rememberDebouncedClick { scope.launch { drawerState?.open() } } if (drawerState != null || isPreview) { - val debouncedClick = rememberDebouncedClick { scope.launch { drawerState?.open() } } IconButton( onClick = debouncedClick, modifier = modifier.testTag("HeaderMenu") diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt index 4f6182316..55897f4d4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt @@ -77,6 +77,7 @@ fun FundingScreen( Column( verticalArrangement = Arrangement.spacedBy(8.dp), ) { + val debouncedNoFundsClick = rememberDebouncedClick { showNoFundsAlert = true } Box { RectangleButton( label = stringResource(R.string.lightning__funding__button1), @@ -87,7 +88,6 @@ fun FundingScreen( modifier = Modifier.testTag("FundTransfer") ) if (balances.channelFundableBalance == 0uL) { - val debouncedClick = rememberDebouncedClick { showNoFundsAlert = true } Box( modifier = Modifier .matchParentSize() @@ -95,7 +95,7 @@ fun FundingScreen( enabled = balances.channelFundableBalance == 0uL, interactionSource = null, indication = null, - onClick = debouncedClick, + onClick = debouncedNoFundsClick, ) .testTag("FundTransfer") ) diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index e56def6b8..494819a2f 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -26,15 +26,17 @@ import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.role import androidx.compose.ui.unit.Constraints import kotlinx.coroutines.launch +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds -private const val CLICK_DEBOUNCE_MS = 500L +private val CLICK_DEBOUNCE = 500.milliseconds -private class ClickDebouncer(private val debounceMs: Long = CLICK_DEBOUNCE_MS) { +private class ClickDebouncer(private val debounce: Duration = CLICK_DEBOUNCE) { private var lastClickTime = 0L fun tryClick(onClick: () -> Unit): Boolean { val now = SystemClock.uptimeMillis() - if (now - lastClickTime >= debounceMs) { + if (now - lastClickTime >= debounce.inWholeMilliseconds) { lastClickTime = now onClick() return true @@ -44,8 +46,8 @@ private class ClickDebouncer(private val debounceMs: Long = CLICK_DEBOUNCE_MS) { } @Composable -fun rememberDebouncedClick(debounceMs: Long = CLICK_DEBOUNCE_MS, onClick: () -> Unit): () -> Unit { - val debouncer = remember { ClickDebouncer(debounceMs) } +fun rememberDebouncedClick(debounce: Duration = CLICK_DEBOUNCE, onClick: () -> Unit): () -> Unit { + val debouncer = remember(debounce) { ClickDebouncer(debounce) } val currentOnClick by rememberUpdatedState(onClick) return remember(debouncer) { { debouncer.tryClick(currentOnClick) } } } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 4d723dfc6..74b79c56f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -133,6 +133,8 @@ import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet import java.math.BigDecimal import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) @@ -189,6 +191,8 @@ class AppViewModel @Inject constructor( val quickPayData = _quickPayData.asStateFlow() private var activeScanJob: Job? = null + + @Volatile private var activeScanInput: String? = null private val _sendEffect = MutableSharedFlow(extraBufferCapacity = 1) @@ -986,7 +990,7 @@ class AppViewModel @Inject constructor( ) } - private fun launchScan(source: ScanSource, data: String, delayMs: Long = 0) { + private fun launchScan(source: ScanSource, data: String, startDelay: Duration = Duration.ZERO) { val normalized = data.removeLightningSchemes() val scanId = if (data.length > 24) "${data.take(11)}…${data.takeLast(11)}" else data @@ -1003,9 +1007,9 @@ class AppViewModel @Inject constructor( activeScanInput = normalized Logger.debug("Scan from '${source.label}': '$scanId'", context = TAG) activeScanJob = viewModelScope.launch { - if (delayMs > 0) delay(delayMs) + if (startDelay > Duration.ZERO) delay(startDelay) handleScan(data) - }.also { it.invokeOnCompletion { activeScanInput = null } } + }.also { it.invokeOnCompletion { if (activeScanInput == normalized) activeScanInput = null } } } private fun onAddressContinue(data: String) { @@ -1164,8 +1168,8 @@ class AppViewModel @Inject constructor( setSendEffect(SendEffect.NavigateToScan) } - fun onScanResult(data: String, delayMs: Long = 0) { - launchScan(source = ScanSource.SCAN_RESULT, data = data, delayMs = delayMs) + fun onScanResult(data: String, startDelay: Duration = Duration.ZERO) { + launchScan(source = ScanSource.SCAN_RESULT, data = data, startDelay = startDelay) } private suspend fun handleScan(result: String) = withContext(bgDispatcher) { @@ -1663,7 +1667,7 @@ class AppViewModel @Inject constructor( @Suppress("LongMethod") private suspend fun proceedWithPayment() { - delay(SCREEN_TRANSITION_DELAY_MS) // wait for screen transitions when applicable + delay(SCREEN_TRANSITION_DELAY) // wait for screen transitions when applicable val amount = _sendUiState.value.amount @@ -2117,7 +2121,7 @@ class AppViewModel @Inject constructor( viewModelScope.launch { _currentSheet.value?.let { _currentSheet.update { null } - delay(SCREEN_TRANSITION_DELAY_MS) + delay(SCREEN_TRANSITION_DELAY) } _currentSheet.update { sheetType } } @@ -2316,7 +2320,7 @@ class AppViewModel @Inject constructor( private fun processDeeplink(uri: Uri) = viewModelScope.launch { if (uri.toString().contains("recovery-mode")) { lightningRepo.setRecoveryMode(enabled = true) - delay(SCREEN_TRANSITION_DELAY_MS) + delay(SCREEN_TRANSITION_DELAY) mainScreenEffect( MainScreenEffect.Navigate( route = Routes.RecoveryMode, @@ -2328,7 +2332,7 @@ class AppViewModel @Inject constructor( if (!walletRepo.walletExists()) return@launch - launchScan(source = ScanSource.DEEPLINK, data = uri.toString(), delayMs = SCREEN_TRANSITION_DELAY_MS) + launchScan(source = ScanSource.DEEPLINK, data = uri.toString(), startDelay = SCREEN_TRANSITION_DELAY) } // TODO Temporary fix while these schemes can't be decoded https://github.com/synonymdev/bitkit-core/issues/70 @@ -2343,7 +2347,7 @@ class AppViewModel @Inject constructor( fun dismissTimedSheet() = timedSheetManager.dismissCurrentSheet() private suspend fun checkCriticalAppUpdate() = withContext(bgDispatcher) { - delay(SCREEN_TRANSITION_DELAY_MS) + delay(SCREEN_TRANSITION_DELAY) runCatching { val androidReleaseInfo = appUpdaterService.getReleaseInfo().platforms.android @@ -2379,7 +2383,7 @@ class AppViewModel @Inject constructor( private const val TEN_USD = 10 private const val MAX_BALANCE_FRACTION = 0.5 private const val MAX_FEE_AMOUNT_RATIO = 0.5 - private const val SCREEN_TRANSITION_DELAY_MS = 300L + private val SCREEN_TRANSITION_DELAY = TRANSITION_SCREEN_MS.milliseconds private const val MIGRATION_LOADING_TIMEOUT_MS = 120_000L private const val POST_RESTORE_PRUNE_DELAY_MS = 30_000L private const val MIGRATION_AUTH_RESET_DELAY_MS = 500L From 5b9a00c51834e592e6784e138d87596ea757a1c2 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Mar 2026 00:09:32 +0100 Subject: [PATCH 16/20] refactor: inline rememberDebouncedClick's --- app/src/main/java/to/bitkit/ui/components/Button.kt | 9 +++------ app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt | 3 +-- .../main/java/to/bitkit/ui/components/RectangleButton.kt | 3 +-- app/src/main/java/to/bitkit/ui/components/SheetHost.kt | 3 +-- app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt | 6 ++---- .../java/to/bitkit/ui/screens/transfer/FundingScreen.kt | 3 +-- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/Button.kt b/app/src/main/java/to/bitkit/ui/components/Button.kt index fd0e62bc9..f64ef19db 100644 --- a/app/src/main/java/to/bitkit/ui/components/Button.kt +++ b/app/src/main/java/to/bitkit/ui/components/Button.kt @@ -67,12 +67,11 @@ fun PrimaryButton( color: Color? = null, enableGradient: Boolean = true, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) val buttonShape = MaterialTheme.shapes.large Button( - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), enabled = enabled && !isLoading, colors = AppButtonDefaults.primaryColors.copy( containerColor = Color.Transparent, @@ -138,11 +137,10 @@ fun SecondaryButton( enabled: Boolean = true, fullWidth: Boolean = true, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) val border = BorderStroke(2.dp, if (enabled) Colors.Gray4 else Color.Transparent) OutlinedButton( - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), enabled = enabled && !isLoading, colors = AppButtonDefaults.secondaryColors, contentPadding = contentPadding, @@ -198,10 +196,9 @@ fun TertiaryButton( enabled: Boolean = true, fullWidth: Boolean = true, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) TextButton( - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), enabled = enabled && !isLoading, colors = AppButtonDefaults.tertiaryColors, contentPadding = contentPadding, diff --git a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt index 10bedbf00..b6b24237a 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -236,7 +236,6 @@ private fun Scrim( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) AnimatedVisibility( visible = visible, modifier = modifier @@ -248,7 +247,7 @@ private fun Scrim( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), ) ) } diff --git a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt index 94f2677a9..8c0245f1f 100644 --- a/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt +++ b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt @@ -46,9 +46,8 @@ fun RectangleButton( iconSize: Dp = 20.dp, onClick: () -> Unit = {}, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) Button( - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), colors = ButtonDefaults.buttonColors( containerColor = Colors.Gray6, ), diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 4bbc8aa3a..185ad704a 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -126,7 +126,6 @@ private fun Scrim( bottomSheetState: SheetState, onClick: () -> Unit, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) val isBottomSheetVisible = bottomSheetState.targetValue != SheetValue.Hidden val scrimAlpha by animateFloatAsState( targetValue = if (isBottomSheetVisible) 0.5f else 0f, @@ -141,7 +140,7 @@ private fun Scrim( .clickable( interactionSource = null, indication = null, - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), ) ) } diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt index da14f46a3..5a7545923 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt @@ -77,9 +77,8 @@ fun BackNavIcon( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) IconButton( - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), modifier = modifier.testTag("NavigationBack") ) { Icon( @@ -118,9 +117,8 @@ fun ScanNavIcon( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val debouncedClick = rememberDebouncedClick(onClick = onClick) IconButton( - onClick = debouncedClick, + onClick = rememberDebouncedClick(onClick = onClick), modifier = modifier.testTag("NavigationAction") ) { Icon( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt index 55897f4d4..763c875ca 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt @@ -77,7 +77,6 @@ fun FundingScreen( Column( verticalArrangement = Arrangement.spacedBy(8.dp), ) { - val debouncedNoFundsClick = rememberDebouncedClick { showNoFundsAlert = true } Box { RectangleButton( label = stringResource(R.string.lightning__funding__button1), @@ -95,7 +94,7 @@ fun FundingScreen( enabled = balances.channelFundableBalance == 0uL, interactionSource = null, indication = null, - onClick = debouncedNoFundsClick, + onClick = rememberDebouncedClick { showNoFundsAlert = true }, ) .testTag("FundTransfer") ) From c0fa26bb600eb633fd95e49da7d07c6da9052244 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Mar 2026 00:43:37 +0100 Subject: [PATCH 17/20] refactor: replace clickable with clickableAlpha --- .../to/bitkit/ui/components/DrawerMenu.kt | 18 ++----------- .../java/to/bitkit/ui/components/SheetHost.kt | 9 ++----- .../java/to/bitkit/ui/components/TabBar.kt | 1 + .../components/settings/SettingsButtonRow.kt | 2 +- .../settings/SettingsTextButtonRow.kt | 2 +- .../java/to/bitkit/ui/scaffold/AppTopBar.kt | 3 ++- .../ui/screens/transfer/FundingScreen.kt | 26 ++++++++----------- .../ui/settings/backups/ShowMnemonicScreen.kt | 2 +- .../ui/shared/modifiers/ClickableAlpha.kt | 3 ++- 9 files changed, 23 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt index b6b24237a..881e396bc 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -5,8 +5,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,7 +23,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -48,7 +45,6 @@ import to.bitkit.ui.Routes import to.bitkit.ui.navigateTo import to.bitkit.ui.navigateToHome import to.bitkit.ui.shared.modifiers.clickableAlpha -import to.bitkit.ui.shared.modifiers.rememberDebouncedClick import to.bitkit.ui.shared.util.blockPointerInputPassthrough import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -244,11 +240,7 @@ private fun Scrim( modifier = Modifier .fillMaxSize() .background(bgScrim) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = rememberDebouncedClick(onClick = onClick), - ) + .clickableAlpha(pressedAlpha = 1f, onClick = onClick) ) } } @@ -262,13 +254,7 @@ private fun DrawerItem( ) { Column( modifier = modifier - .then( - if (onClick != null) { - Modifier.clickableAlpha { onClick() } - } else { - Modifier - } - ) + .clickableAlpha(onClick = onClick) .padding(horizontal = 16.dp) ) { VerticalSpacer(16.dp) diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 185ad704a..db7235487 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -4,7 +4,6 @@ import androidx.activity.compose.BackHandler 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.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize @@ -24,7 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch -import to.bitkit.ui.shared.modifiers.rememberDebouncedClick +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.sheets.SendRoute @@ -137,11 +136,7 @@ private fun Scrim( modifier = Modifier .fillMaxSize() .background(Colors.Black.copy(alpha = scrimAlpha)) - .clickable( - interactionSource = null, - indication = null, - onClick = rememberDebouncedClick(onClick = onClick), - ) + .clickableAlpha(pressedAlpha = 1f, onClick = onClick) ) } } diff --git a/app/src/main/java/to/bitkit/ui/components/TabBar.kt b/app/src/main/java/to/bitkit/ui/components/TabBar.kt index 1e816d99c..4ba3048ec 100644 --- a/app/src/main/java/to/bitkit/ui/components/TabBar.kt +++ b/app/src/main/java/to/bitkit/ui/components/TabBar.kt @@ -40,6 +40,7 @@ import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.rememberHazeState import to.bitkit.R +import to.bitkit.ui.shared.modifiers.alphaFeedback import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.primaryButtonStyle diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt b/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt index e93e4128b..4e2fadc25 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SettingsButtonRow.kt @@ -53,7 +53,7 @@ fun SettingsButtonRow( val alphaModifier = Modifier.then(if (!enabled) Modifier.alpha(0.5f) else Modifier) Column( modifier = modifier - .clickableAlpha(onClick = if (enabled) onClick else null) + .clickableAlpha(enabled = enabled, onClick = onClick) ) { Column(modifier = alphaModifier) { val rowHeight = when { diff --git a/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt b/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt index f423dcbfd..51f450bd4 100644 --- a/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/settings/SettingsTextButtonRow.kt @@ -54,7 +54,7 @@ fun SettingsTextButtonRow( modifier = Modifier .fillMaxWidth() .heightIn(min = height) - .clickableAlpha(onClick = if (enabled) onClick else null) + .clickableAlpha(enabled = enabled, onClick = onClick) ) { if (iconRes != null) { Icon( diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt index 5a7545923..8741a9bb9 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -95,7 +96,7 @@ fun DrawerNavIcon( ) { val isPreview = LocalInspectionMode.current val drawerState = LocalDrawerState.current - val scope = androidx.compose.runtime.rememberCoroutineScope() + val scope = rememberCoroutineScope() val debouncedClick = rememberDebouncedClick { scope.launch { drawerState?.open() } } if (drawerState != null || isPreview) { diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt index 763c875ca..ca5d5e026 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.screens.transfer -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,7 +30,7 @@ import to.bitkit.ui.components.RectangleButton import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.modifiers.rememberDebouncedClick +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -86,19 +85,16 @@ fun FundingScreen( onClick = onTransfer, modifier = Modifier.testTag("FundTransfer") ) - if (balances.channelFundableBalance == 0uL) { - Box( - modifier = Modifier - .matchParentSize() - .clickable( - enabled = balances.channelFundableBalance == 0uL, - interactionSource = null, - indication = null, - onClick = rememberDebouncedClick { showNoFundsAlert = true }, - ) - .testTag("FundTransfer") - ) - } + Box( + modifier = Modifier + .matchParentSize() + .clickableAlpha( + pressedAlpha = 1f, + enabled = balances.channelFundableBalance == 0uL, + onClick = { showNoFundsAlert = true }, + ) + .testTag("FundTransfer") + ) } RectangleButton( label = stringResource(R.string.lightning__funding__button2), diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index d9745715c..37ec2c4c8 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -157,7 +157,7 @@ private fun ShowMnemonicContent( .fillMaxWidth() .clip(MaterialTheme.shapes.medium) .background(color = Colors.White10) - .clickableAlpha(onClick = if (showMnemonic && mnemonic.isNotEmpty()) onCopyClick else null) + .clickableAlpha(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) .padding(32.dp) .testTag("backup_mnemonic_words_box") ) { diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index 494819a2f..a10fc873e 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -61,8 +61,9 @@ fun rememberDebouncedClick(debounce: Duration = CLICK_DEBOUNCE, onClick: () -> U */ fun Modifier.clickableAlpha( pressedAlpha: Float = 0.7f, + enabled: Boolean = true, onClick: (() -> Unit)?, -): Modifier = if (onClick != null) { +): Modifier = if (onClick != null && enabled) { this.then(ClickableAlphaElement(pressedAlpha, onClick)) } else { this From 076d322a19eeaf3cd0dbbc74092fd0119294c188 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Mar 2026 01:02:14 +0100 Subject: [PATCH 18/20] fix: add ripple to tab bar buttons --- .../java/to/bitkit/ui/components/TabBar.kt | 7 +++---- .../ui/shared/modifiers/ClickableAlpha.kt | 20 ++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/TabBar.kt b/app/src/main/java/to/bitkit/ui/components/TabBar.kt index 4ba3048ec..6771bb95e 100644 --- a/app/src/main/java/to/bitkit/ui/components/TabBar.kt +++ b/app/src/main/java/to/bitkit/ui/components/TabBar.kt @@ -40,7 +40,6 @@ import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.rememberHazeState import to.bitkit.R -import to.bitkit.ui.shared.modifiers.alphaFeedback import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.primaryButtonStyle @@ -83,7 +82,7 @@ fun BoxScope.TabBar( .weight(1f) .height(60.dp) .clip(buttonLeftShape) - .clickableAlpha { onSendClick() } + .clickableAlpha(ripple = true) { onSendClick() } .testTag("Send") ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -104,7 +103,7 @@ fun BoxScope.TabBar( .weight(1f) .height(60.dp) .clip(buttonRightShape) - .clickableAlpha { onReceiveClick() } + .clickableAlpha(ripple = true) { onReceiveClick() } .testTag("Receive") ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -179,7 +178,7 @@ fun BoxScope.TabBar( blendMode = BlendMode.DstIn ) } - .clickableAlpha { onScanClick() } + .clickableAlpha(ripple = true) { onScanClick() } .testTag("Scan") ) { Icon( diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index a10fc873e..89ceb755e 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -2,6 +2,7 @@ package to.bitkit.ui.shared.modifiers import android.os.SystemClock import androidx.compose.animation.core.Animatable +import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -55,18 +56,27 @@ fun rememberDebouncedClick(debounce: Duration = CLICK_DEBOUNCE, onClick: () -> U /** * Adjusts the alpha of a composable when it is pressed and makes it clickable. * When pressed, the alpha is reduced to provide visual feedback. - * If `onClick` is null, the clickable behavior is disabled. + * If `onClick` is null or `enabled` is false, the clickable behavior is disabled. + * + * Set `ripple` to true to show the standard Material ripple indication alongside + * the alpha animation (useful for list items, menu buttons, etc.). * * Analogue of `TouchableOpacity` in React Native. */ +@Composable fun Modifier.clickableAlpha( pressedAlpha: Float = 0.7f, enabled: Boolean = true, + ripple: Boolean = false, onClick: (() -> Unit)?, -): Modifier = if (onClick != null && enabled) { - this.then(ClickableAlphaElement(pressedAlpha, onClick)) -} else { - this +): Modifier = when { + onClick == null || !enabled -> this + ripple -> + this + .alphaFeedback(pressedAlpha) + .clickable(onClick = rememberDebouncedClick(onClick = onClick)) + + else -> this.then(ClickableAlphaElement(pressedAlpha, onClick)) } private data class ClickableAlphaElement( From efa49dea0e668264a005ff7e0a31f009484842b5 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 11 Mar 2026 23:05:26 +0100 Subject: [PATCH 19/20] fix: address review feedback for drawer and numpad Co-Authored-By: Claude Opus 4.6 --- .../to/bitkit/ui/components/DrawerMenu.kt | 24 ++++++++++++------- .../java/to/bitkit/ui/components/NumberPad.kt | 3 ++- .../ui/shared/modifiers/ClickableAlpha.kt | 23 +++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt index 881e396bc..4e5c4b176 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -50,6 +50,12 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.InterFontFamily +private inline fun NavController.navigateIfNotCurrent(route: T) { + if (currentBackStackEntry?.destination?.hasRoute() != true) { + navigateTo(route) + } +} + private const val Z_INDEX_SCRIM = 10f private const val Z_INDEX_MENU = 11f private val bgScrim = Colors.Black50 @@ -98,16 +104,16 @@ fun DrawerMenu( drawerState = drawerState, onClickAddWidget = { if (!hasSeenWidgetsIntro) { - rootNavController.navigateTo(Routes.WidgetsIntro) + rootNavController.navigateIfNotCurrent(Routes.WidgetsIntro) } else { - rootNavController.navigateTo(Routes.AddWidget) + rootNavController.navigateIfNotCurrent(Routes.AddWidget) } }, onClickShop = { if (!hasSeenShopIntro) { - rootNavController.navigateTo(Routes.ShopIntro) + rootNavController.navigateIfNotCurrent(Routes.ShopIntro) } else { - rootNavController.navigateTo(Routes.ShopDiscover) + rootNavController.navigateIfNotCurrent(Routes.ShopDiscover) } }, ) @@ -147,7 +153,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__activity), iconRes = R.drawable.ic_heartbeat, onClick = { - rootNavController.navigateTo(Routes.AllActivity) + rootNavController.navigateIfNotCurrent(Routes.AllActivity) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerActivity") @@ -157,7 +163,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__contacts), iconRes = R.drawable.ic_users, onClick = { - rootNavController.navigateTo(Routes.Contacts) + rootNavController.navigateIfNotCurrent(Routes.Contacts) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerContacts") @@ -167,7 +173,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__profile), iconRes = R.drawable.ic_user_square, onClick = { - rootNavController.navigateTo(Routes.Profile) + rootNavController.navigateIfNotCurrent(Routes.Profile) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerProfile") @@ -197,7 +203,7 @@ private fun Menu( label = stringResource(R.string.wallet__drawer__settings), iconRes = R.drawable.ic_settings, onClick = { - rootNavController.navigateTo(Routes.Settings) + rootNavController.navigateIfNotCurrent(Routes.Settings) scope.launch { drawerState.close() } }, modifier = Modifier.testTag("DrawerSettings") @@ -210,7 +216,7 @@ private fun Menu( modifier = Modifier .fillMaxWidth() .clickableAlpha { - rootNavController.navigateTo(Routes.AppStatus) + rootNavController.navigateIfNotCurrent(Routes.AppStatus) scope.launch { drawerState.close() } } ) { diff --git a/app/src/main/java/to/bitkit/ui/components/NumberPad.kt b/app/src/main/java/to/bitkit/ui/components/NumberPad.kt index fba65ef8e..1b1cc4a07 100644 --- a/app/src/main/java/to/bitkit/ui/components/NumberPad.kt +++ b/app/src/main/java/to/bitkit/ui/components/NumberPad.kt @@ -38,6 +38,7 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.previewAmountInputViewModel +import kotlin.time.Duration const val KEY_DELETE = "delete" const val KEY_000 = "000" @@ -228,7 +229,7 @@ fun NumberPadKey( modifier = modifier .height(height) .fillMaxWidth() - .clickableAlpha(ALPHA_PRESSED) { + .clickableAlpha(ALPHA_PRESSED, debounce = Duration.ZERO) { haptics.performHapticFeedback(haptic) onClick() }, diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index 89ceb755e..0864cd1d9 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -32,10 +32,10 @@ import kotlin.time.Duration.Companion.milliseconds private val CLICK_DEBOUNCE = 500.milliseconds -private class ClickDebouncer(private val debounce: Duration = CLICK_DEBOUNCE) { +private class ClickDebouncer { private var lastClickTime = 0L - fun tryClick(onClick: () -> Unit): Boolean { + fun tryClick(debounce: Duration = CLICK_DEBOUNCE, onClick: () -> Unit): Boolean { val now = SystemClock.uptimeMillis() if (now - lastClickTime >= debounce.inWholeMilliseconds) { lastClickTime = now @@ -48,9 +48,9 @@ private class ClickDebouncer(private val debounce: Duration = CLICK_DEBOUNCE) { @Composable fun rememberDebouncedClick(debounce: Duration = CLICK_DEBOUNCE, onClick: () -> Unit): () -> Unit { - val debouncer = remember(debounce) { ClickDebouncer(debounce) } + val debouncer = remember { ClickDebouncer() } val currentOnClick by rememberUpdatedState(onClick) - return remember(debouncer) { { debouncer.tryClick(currentOnClick) } } + return remember(debouncer, debounce) { { debouncer.tryClick(debounce, currentOnClick) } } } /** @@ -68,37 +68,42 @@ fun Modifier.clickableAlpha( pressedAlpha: Float = 0.7f, enabled: Boolean = true, ripple: Boolean = false, + debounce: Duration = CLICK_DEBOUNCE, onClick: (() -> Unit)?, ): Modifier = when { onClick == null || !enabled -> this ripple -> this .alphaFeedback(pressedAlpha) - .clickable(onClick = rememberDebouncedClick(onClick = onClick)) + .clickable(onClick = rememberDebouncedClick(debounce, onClick)) - else -> this.then(ClickableAlphaElement(pressedAlpha, onClick)) + else -> this.then(ClickableAlphaElement(pressedAlpha, debounce, onClick)) } private data class ClickableAlphaElement( val pressedAlpha: Float, + val debounce: Duration, val onClick: () -> Unit, ) : ModifierNodeElement() { - override fun create(): ClickableAlphaNode = ClickableAlphaNode(pressedAlpha, onClick) + override fun create(): ClickableAlphaNode = ClickableAlphaNode(pressedAlpha, debounce, onClick) override fun update(node: ClickableAlphaNode) { node.pressedAlpha = pressedAlpha + node.debounce = debounce node.onClick = onClick } override fun InspectorInfo.inspectableProperties() { name = "clickableAlpha" properties["pressedAlpha"] = pressedAlpha + properties["debounce"] = debounce properties["onClick"] = onClick } } private class ClickableAlphaNode( var pressedAlpha: Float, + var debounce: Duration, var onClick: () -> Unit, ) : DelegatingNode(), LayoutModifierNode, SemanticsModifierNode { @@ -117,7 +122,7 @@ private class ClickableAlphaNode( } }, onTap = { - if (debouncer.tryClick(onClick)) { + if (debouncer.tryClick(debounce, onClick)) { coroutineScope.launch { animatable.animateTo(pressedAlpha) animatable.animateTo(1f) @@ -142,7 +147,7 @@ private class ClickableAlphaNode( override fun SemanticsPropertyReceiver.applySemantics() { role = Role.Button onClick { - debouncer.tryClick(onClick) + debouncer.tryClick(debounce, onClick) true } } From 6fe4f4ac5ac57073e3f34510366bea46b85ba098 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 12 Mar 2026 21:26:52 +0100 Subject: [PATCH 20/20] fix: address pr review feedback Co-Authored-By: Claude Opus 4.6 --- .../main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt | 2 ++ app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt index 0864cd1d9..2343b3274 100644 --- a/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt +++ b/app/src/main/java/to/bitkit/ui/shared/modifiers/ClickableAlpha.kt @@ -127,6 +127,8 @@ private class ClickableAlphaNode( animatable.animateTo(pressedAlpha) animatable.animateTo(1f) } + } else { + coroutineScope.launch { animatable.animateTo(1f) } } } ) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 74b79c56f..57ae239c0 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1005,7 +1005,7 @@ class AppViewModel @Inject constructor( } activeScanInput = normalized - Logger.debug("Scan from '${source.label}': '$scanId'", context = TAG) + Logger.debug("Starting scan from '${source.label}': '$scanId'", context = TAG) activeScanJob = viewModelScope.launch { if (startDelay > Duration.ZERO) delay(startDelay) handleScan(data)