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..3524974db 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 @@ -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 @@ -260,7 +261,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 +390,7 @@ fun ContentView( ReceiveSheet( walletState = walletState, navigateToExternalConnection = { - navController.navigate(ExternalConnection()) + navController.navigateTo(ExternalConnection()) appViewModel.hideSheet() } ) @@ -419,7 +424,7 @@ fun ContentView( BackgroundPaymentsIntroSheet( onContinue = { appViewModel.dismissTimedSheet() - navController.navigate(Routes.BackgroundPaymentsSettings) + navController.navigateTo(Routes.BackgroundPaymentsSettings) settingsViewModel.setBgPaymentsIntroSeen(true) }, ) @@ -429,7 +434,7 @@ fun ContentView( QuickPayIntroSheet( onContinue = { appViewModel.dismissTimedSheet() - navController.navigate(Routes.QuickPaySettings) + navController.navigateTo(Routes.QuickPaySettings) }, ) } @@ -567,7 +572,7 @@ private fun RootNavHost( composableWithDefaultTransitions { SavingsIntroScreen( onContinueClick = { - navController.navigate(Routes.SavingsAvailability) + navController.navigateTo(Routes.SavingsAvailability) settingsViewModel.setHasSeenSavingsIntro(true) }, onBackClick = { navController.popBackStack() }, @@ -577,13 +582,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 +610,7 @@ private fun RootNavHost( composableWithDefaultTransitions { SpendingIntroScreen( onContinueClick = { - navController.navigate(Routes.SpendingAmount) + navController.navigateTo(Routes.SpendingAmount) settingsViewModel.setHasSeenSpendingIntro(true) }, onBackClick = { navController.popBackStack() }, @@ -615,7 +620,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 +636,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 +682,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 +690,7 @@ private fun RootNavHost( composableWithDefaultTransitions { FundingAdvancedScreen( onLnurl = { navController.navigateToScanner() }, - onManual = { navController.navigate(Routes.ExternalNav) }, + onManual = { navController.navigateTo(Routes.ExternalNav) }, onBackClick = { navController.popBackStack() }, ) } @@ -701,7 +706,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 +717,7 @@ private fun RootNavHost( ExternalAmountScreen( viewModel = viewModel, - onContinue = { navController.navigate(Routes.ExternalConfirm) }, + onContinue = { navController.navigateTo(Routes.ExternalConfirm) }, onBackClick = { navController.popBackStack() }, ) } @@ -724,7 +729,7 @@ private fun RootNavHost( viewModel = viewModel, onConfirm = { walletViewModel.refreshState() - navController.navigate(Routes.ExternalSuccess) + navController.navigateTo(Routes.ExternalSuccess) }, onBackClick = { navController.popBackStack() }, ) @@ -732,7 +737,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 +866,7 @@ private fun NavGraphBuilder.settings( onBack = { navController.popBackStack() }, onContinue = { settingsViewModel.setQuickPayIntroSeen(true) - navController.navigate(Routes.QuickPaySettings) + navController.navigateTo(Routes.QuickPaySettings) } ) } @@ -920,7 +925,7 @@ private fun NavGraphBuilder.profile( ProfileIntroScreen( onContinue = { settingsViewModel.setHasSeenProfileIntro(true) - navController.navigate(Routes.CreateProfile) + navController.navigateTo(Routes.CreateProfile) }, onBackClick = { navController.popBackStack() } ) @@ -941,7 +946,7 @@ private fun NavGraphBuilder.shop( ShopIntroScreen( onContinue = { settingsViewModel.setHasSeenShopIntro(true) - navController.navigate(Routes.ShopDiscover) + navController.navigateTo(Routes.ShopDiscover) }, onBackClick = { navController.popBackStack() @@ -952,7 +957,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 +996,7 @@ private fun NavGraphBuilder.generalSettings(navController: NavHostController) { BackgroundPaymentsIntroScreen( onBack = { navController.popBackStack() }, onContinue = { - navController.navigate(Routes.BackgroundPaymentsSettings) + navController.navigateTo(Routes.BackgroundPaymentsSettings) } ) } @@ -1186,7 +1191,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() }, @@ -1211,7 +1216,7 @@ private fun NavGraphBuilder.qrScanner( QrScanningScreen(navController = navController) { qrCode -> appViewModel.onScanResult( data = qrCode, - delayMs = TRANSITION_SHEET_MS, + startDelay = TRANSITION_SHEET_MS.milliseconds, ) } } @@ -1267,7 +1272,7 @@ private fun NavGraphBuilder.recoveryMode( composableWithDefaultTransitions { RecoveryModeScreen( onNavigateToSeed = { - navController.navigate(Routes.RecoveryMnemonic) + navController.navigateTo(Routes.RecoveryMnemonic) }, appViewModel = appViewModel ) @@ -1297,9 +1302,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 +1337,7 @@ private fun NavGraphBuilder.widgets( WidgetsIntroScreen( onContinue = { settingsViewModel.setHasSeenWidgetsIntro(true) - navController.navigate(Routes.AddWidget) + navController.navigateTo(Routes.AddWidget) }, onBackClick = { navController.popBackStack() }, ) @@ -1341,12 +1346,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 +1376,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 +1387,7 @@ private fun NavGraphBuilder.widgets( headlinesViewModel = viewModel, onBack = { navController.popBackStack() }, navigatePreview = { - navController.navigate(Routes.HeadlinesPreview) + navController.navigateTo(Routes.HeadlinesPreview) } ) } @@ -1398,7 +1403,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 +1413,7 @@ private fun NavGraphBuilder.widgets( FactsEditScreen( factsViewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.FactsPreview) } + navigatePreview = { navController.navigateTo(Routes.FactsPreview) } ) } } @@ -1423,7 +1428,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 +1438,7 @@ private fun NavGraphBuilder.widgets( BlocksEditScreen( blocksViewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.BlocksPreview) } + navigatePreview = { navController.navigateTo(Routes.BlocksPreview) } ) } } @@ -1448,7 +1453,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 +1463,7 @@ private fun NavGraphBuilder.widgets( WeatherEditScreen( weatherViewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.WeatherPreview) } + navigatePreview = { navController.navigateTo(Routes.WeatherPreview) } ) } } @@ -1473,7 +1478,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 +1487,7 @@ private fun NavGraphBuilder.widgets( PriceEditScreen( viewModel = viewModel, onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.PricePreview) } + navigatePreview = { navController.navigateTo(Routes.PricePreview) } ) } } @@ -1494,171 +1499,113 @@ 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) = + navigateTo(if (hasSeenIntro) Routes.QuickPaySettings else 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/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/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/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/Button.kt b/app/src/main/java/to/bitkit/ui/components/Button.kt index d5f15083f..f64ef19db 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 @@ -70,7 +71,7 @@ fun PrimaryButton( val buttonShape = MaterialTheme.shapes.large Button( - onClick = onClick, + onClick = rememberDebouncedClick(onClick = onClick), enabled = enabled && !isLoading, colors = AppButtonDefaults.primaryColors.copy( containerColor = Color.Transparent, @@ -139,7 +140,7 @@ fun SecondaryButton( 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 = rememberDebouncedClick(onClick = onClick), enabled = enabled && !isLoading, colors = AppButtonDefaults.secondaryColors, contentPadding = contentPadding, @@ -197,7 +198,7 @@ fun TertiaryButton( ) { val contentPadding = PaddingValues(horizontal = size.horizontalPadding.takeIf { text != null } ?: 0.dp) TextButton( - onClick = onClick, + 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 79b694d41..4e5c4b176 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 @@ -45,7 +42,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.util.blockPointerInputPassthrough @@ -53,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 @@ -243,11 +246,7 @@ private fun Scrim( modifier = Modifier .fillMaxSize() .background(bgScrim) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = onClick, - ) + .clickableAlpha(pressedAlpha = 1f, onClick = onClick) ) } } @@ -261,13 +260,7 @@ private fun DrawerItem( ) { Column( modifier = modifier - .then( - if (onClick != null) { - Modifier.clickable { onClick() } - } else { - Modifier - } - ) + .clickableAlpha(onClick = onClick) .padding(horizontal = 16.dp) ) { VerticalSpacer(16.dp) 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/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, 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/components/QrCodeImage.kt b/app/src/main/java/to/bitkit/ui/components/QrCodeImage.kt index 2a54dab4b..1a0a9fa20 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,18 @@ 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/components/RectangleButton.kt b/app/src/main/java/to/bitkit/ui/components/RectangleButton.kt index 1d5f9d727..8c0245f1f 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,8 @@ 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 import to.bitkit.ui.theme.Shapes @@ -45,7 +47,7 @@ fun RectangleButton( onClick: () -> Unit = {}, ) { Button( - onClick = onClick, + onClick = rememberDebouncedClick(onClick = onClick), colors = ButtonDefaults.buttonColors( containerColor = Colors.Gray6, ), @@ -54,6 +56,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() ) { 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..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,6 +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.clickableAlpha import to.bitkit.ui.sheets.BackupRoute import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.sheets.SendRoute @@ -136,11 +136,7 @@ private fun Scrim( modifier = Modifier .fillMaxSize() .background(Colors.Black.copy(alpha = scrimAlpha)) - .clickable( - interactionSource = null, - indication = null, - onClick = onClick, - ) + .clickableAlpha(pressedAlpha = 1f, onClick = onClick) ) } } 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/TabBar.kt b/app/src/main/java/to/bitkit/ui/components/TabBar.kt index fd629f34e..6771bb95e 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(ripple = true) { onSendClick() } .testTag("Send") ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -104,7 +103,7 @@ fun BoxScope.TabBar( .weight(1f) .height(60.dp) .clip(buttonRightShape) - .clickable { 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/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/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/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) 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..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 @@ -27,6 +28,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) @@ -77,7 +79,7 @@ fun BackNavIcon( modifier: Modifier = Modifier, ) { IconButton( - onClick = onClick, + onClick = rememberDebouncedClick(onClick = onClick), modifier = modifier.testTag("NavigationBack") ) { Icon( @@ -94,11 +96,12 @@ 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) { IconButton( - onClick = { scope.launch { drawerState?.open() } }, + onClick = debouncedClick, modifier = modifier.testTag("HeaderMenu") ) { Icon( @@ -116,7 +119,7 @@ fun ScanNavIcon( modifier: Modifier = Modifier, ) { IconButton( - onClick = onClick, + onClick = rememberDebouncedClick(onClick = onClick), modifier = modifier.testTag("NavigationAction") ) { Icon( 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/transfer/FundingScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/FundingScreen.kt index 3fd7172a8..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,6 +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.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -85,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 = { 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/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/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/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/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/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/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/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 0468834aa..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 @@ -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(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) .padding(32.dp) .testTag("backup_mnemonic_words_box") ) { 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 555d79d06..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 @@ -1,10 +1,14 @@ 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 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 @@ -23,47 +27,88 @@ 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 val CLICK_DEBOUNCE = 500.milliseconds + +private class ClickDebouncer { + private var lastClickTime = 0L + + fun tryClick(debounce: Duration = CLICK_DEBOUNCE, onClick: () -> Unit): Boolean { + val now = SystemClock.uptimeMillis() + if (now - lastClickTime >= debounce.inWholeMilliseconds) { + lastClickTime = now + onClick() + return true + } + return false + } +} + +@Composable +fun rememberDebouncedClick(debounce: Duration = CLICK_DEBOUNCE, onClick: () -> Unit): () -> Unit { + val debouncer = remember { ClickDebouncer() } + val currentOnClick by rememberUpdatedState(onClick) + return remember(debouncer, debounce) { { debouncer.tryClick(debounce, currentOnClick) } } +} /** * 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, + debounce: Duration = CLICK_DEBOUNCE, onClick: (() -> Unit)?, -): Modifier = if (onClick != null) { - this.then(ClickableAlphaElement(pressedAlpha, onClick)) -} else { - this +): Modifier = when { + onClick == null || !enabled -> this + ripple -> + this + .alphaFeedback(pressedAlpha) + .clickable(onClick = rememberDebouncedClick(debounce, 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 { private val animatable = Animatable(1f) + private val debouncer = ClickDebouncer() init { delegate( @@ -77,10 +122,13 @@ private class ClickableAlphaNode( } }, onTap = { - onClick() - coroutineScope.launch { - animatable.animateTo(pressedAlpha) - animatable.animateTo(1f) + if (debouncer.tryClick(debounce, onClick)) { + coroutineScope.launch { + animatable.animateTo(pressedAlpha) + animatable.animateTo(1f) + } + } else { + coroutineScope.launch { animatable.animateTo(1f) } } } ) @@ -101,7 +149,7 @@ private class ClickableAlphaNode( override fun SemanticsPropertyReceiver.applySemantics() { role = Role.Button onClick { - onClick() + debouncer.tryClick(debounce, onClick) true } } 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 2fdc4fc85..57ae239c0 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 @@ -135,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) @@ -190,6 +190,11 @@ class AppViewModel @Inject constructor( private val _quickPayData = MutableStateFlow(null) val quickPayData = _quickPayData.asStateFlow() + private var activeScanJob: Job? = null + + @Volatile + 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,10 +990,30 @@ class AppViewModel @Inject constructor( ) } - private fun onAddressContinue(data: String) { - viewModelScope.launch { - handleScan(data) + 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 + + 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("Starting scan from '${source.label}': '$scanId'", context = TAG) + activeScanJob = viewModelScope.launch { + if (startDelay > Duration.ZERO) delay(startDelay) + handleScan(data) + }.also { it.invokeOnCompletion { if (activeScanInput == normalized) activeScanInput = null } } + } + + private fun onAddressContinue(data: String) { + launchScan(source = ScanSource.ADDRESS_CONTINUE, data = data) } private suspend fun onAmountChange(amount: ULong) { @@ -1136,20 +1161,15 @@ class AppViewModel @Inject constructor( ) return } - viewModelScope.launch { - handleScan(data) - } + launchScan(source = ScanSource.PASTE, data = data) } private fun onScanClick() { setSendEffect(SendEffect.NavigateToScan) } - fun onScanResult(data: String, delayMs: Long = 0) { - viewModelScope.launch { - delay(delayMs) - handleScan(data) - } + 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) { @@ -1647,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 @@ -2101,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 } } @@ -2300,13 +2320,11 @@ 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, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } + clearStack = true, ) ) return@launch @@ -2314,19 +2332,12 @@ 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(), startDelay = SCREEN_TRANSITION_DELAY) } // 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() @@ -2336,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 @@ -2348,9 +2359,7 @@ class AppViewModel @Inject constructor( mainScreenEffect( MainScreenEffect.Navigate( route = Routes.CriticalUpdate, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } + clearStack = true, ) ) } @@ -2359,13 +2368,22 @@ 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 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 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 @@ -2437,7 +2455,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()