From 697bf9028d310362c854d04bfe16813ba68fac98 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 23 Dec 2025 14:09:52 +0100 Subject: [PATCH 1/3] refactor: Migrate navigation to nav3 --- app/build.gradle.kts | 6 +- app/detekt-baseline.xml | 13 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 1842 +---------------- .../main/java/to/bitkit/ui/MainActivity.kt | 140 +- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 6 +- .../bitkit/ui/components/AuthCheckScreen.kt | 55 +- .../to/bitkit/ui/components/DrawerMenu.kt | 115 +- .../java/to/bitkit/ui/components/SheetHost.kt | 133 -- .../main/java/to/bitkit/ui/nav/DeepLinks.kt | 181 ++ .../main/java/to/bitkit/ui/nav/Navigator.kt | 59 + app/src/main/java/to/bitkit/ui/nav/Routes.kt | 527 +++++ .../to/bitkit/ui/nav/SheetSceneStrategy.kt | 149 ++ .../main/java/to/bitkit/ui/nav/Transitions.kt | 83 + .../to/bitkit/ui/nav/entries/HomeEntries.kt | 309 +++ .../ui/nav/entries/OnboardingEntries.kt | 65 + .../bitkit/ui/nav/entries/SettingsEntries.kt | 289 +++ .../to/bitkit/ui/nav/entries/SheetEntries.kt | 896 ++++++++ .../bitkit/ui/nav/entries/TransferEntries.kt | 324 +++ .../to/bitkit/ui/nav/entries/WidgetEntries.kt | 302 +++ .../bitkit/ui/onboarding/TermsOfUseScreen.kt | 1 - .../ui/screens/scanner/QrScanningScreen.kt | 37 +- .../ui/screens/settings/DevSettingsScreen.kt | 18 +- .../ui/screens/settings/FeeSettingsScreen.kt | 6 +- .../ui/screens/settings/LdkDebugScreen.kt | 6 +- .../screens/transfer/SavingsProgressScreen.kt | 6 +- .../external/ExternalConnectionScreen.kt | 42 +- .../transfer/external/LnurlChannelScreen.kt | 28 +- .../external/LnurlChannelViewModel.kt | 15 +- .../bitkit/ui/screens/wallets/HomeScreen.kt | 121 +- .../ui/screens/wallets/SavingsWalletScreen.kt | 2 +- .../screens/wallets/SpendingWalletScreen.kt | 2 +- .../wallets/activity/ActivityDetailScreen.kt | 29 +- .../wallets/activity/ActivityExploreScreen.kt | 21 +- .../wallets/activity/AllActivityScreen.kt | 16 +- .../activity/DateRangeSelectorSheet.kt | 8 +- .../wallets/activity/TagSelectorSheet.kt | 6 +- .../components/ActivityListGrouped.kt | 2 +- .../activity/components/ActivityListSimple.kt | 2 +- .../activity/components/ActivityRow.kt | 5 +- .../screens/wallets/receive/ReceiveSheet.kt | 217 -- .../ui/screens/wallets/send/AddTagScreen.kt | 4 +- .../screens/wallets/send/SendAddressScreen.kt | 4 +- .../wallets/send/SendRecipientScreen.kt | 4 +- .../ui/settings/AdvancedSettingsScreen.kt | 23 +- .../ui/settings/BackupSettingsScreen.kt | 18 +- .../ui/settings/BlocktankRegtestScreen.kt | 17 +- .../bitkit/ui/settings/ChannelOrdersScreen.kt | 9 +- .../java/to/bitkit/ui/settings/LogsScreen.kt | 14 +- .../ui/settings/SecuritySettingsScreen.kt | 39 +- .../to/bitkit/ui/settings/SettingsScreen.kt | 28 +- .../settings/advanced/AddressViewerScreen.kt | 6 +- .../advanced/CoinSelectPreferenceScreen.kt | 6 +- .../settings/advanced/ElectrumConfigScreen.kt | 26 +- .../ui/settings/advanced/RgsServerScreen.kt | 26 +- .../ui/settings/appStatus/AppStatusScreen.kt | 16 +- .../BackgroundPaymentsSettings.kt | 10 +- .../settings/backups/ResetAndRestoreScreen.kt | 12 +- .../ui/settings/backups/ShowMnemonicScreen.kt | 4 +- .../general/DefaultUnitSettingsScreen.kt | 6 +- .../settings/general/GeneralSettingsScreen.kt | 33 +- .../general/LocalCurrencySettingsScreen.kt | 6 +- .../ui/settings/general/TagsSettingsScreen.kt | 8 +- .../settings/general/WidgetsSettingsScreen.kt | 6 +- .../settings/lightning/ChannelDetailScreen.kt | 10 +- .../lightning/CloseConnectionScreen.kt | 10 +- .../lightning/LightningConnectionsScreen.kt | 29 +- .../ui/settings/pin/ChangePinConfirmScreen.kt | 10 +- .../ui/settings/pin/ChangePinNewScreen.kt | 10 +- .../ui/settings/pin/ChangePinResultScreen.kt | 10 +- .../bitkit/ui/settings/pin/ChangePinScreen.kt | 10 +- .../ui/settings/pin/DisablePinScreen.kt | 19 +- .../ui/settings/support/SupportScreen.kt | 28 +- .../CustomFeeSettingsScreen.kt | 8 +- .../TransactionSpeedSettingsScreen.kt | 12 +- .../java/to/bitkit/ui/sheets/BackupSheet.kt | 180 -- .../to/bitkit/ui/sheets/ForceTransferSheet.kt | 8 +- .../java/to/bitkit/ui/sheets/GiftLoading.kt | 2 + .../java/to/bitkit/ui/sheets/GiftRoute.kt | 23 - .../java/to/bitkit/ui/sheets/GiftSheet.kt | 98 - .../java/to/bitkit/ui/sheets/GiftViewModel.kt | 18 +- .../to/bitkit/ui/sheets/LnurlAuthSheet.kt | 24 +- .../main/java/to/bitkit/ui/sheets/PinSheet.kt | 96 - .../java/to/bitkit/ui/sheets/SendSheet.kt | 342 --- .../main/java/to/bitkit/ui/theme/Defaults.kt | 4 - .../java/to/bitkit/ui/utils/Transitions.kt | 119 -- .../main/java/to/bitkit/utils/Bip21Utils.kt | 3 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 165 +- .../to/bitkit/viewmodels/WalletViewModel.kt | 18 + gradle/libs.versions.toml | 3 + 89 files changed, 3928 insertions(+), 3740 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/nav/DeepLinks.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/Navigator.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/Routes.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/SheetSceneStrategy.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/Transitions.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/entries/HomeEntries.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/entries/OnboardingEntries.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/entries/SettingsEntries.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/entries/TransferEntries.kt create mode 100644 app/src/main/java/to/bitkit/ui/nav/entries/WidgetEntries.kt delete mode 100644 app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt delete mode 100644 app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt delete mode 100644 app/src/main/java/to/bitkit/ui/sheets/GiftRoute.kt delete mode 100644 app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt delete mode 100644 app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt delete mode 100644 app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt delete mode 100644 app/src/main/java/to/bitkit/ui/utils/Transitions.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 11e7ebedd..deec7020b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -230,9 +230,9 @@ dependencies { implementation(libs.charts) implementation(libs.haze) implementation(libs.haze.materials) - // Compose Navigation - implementation(libs.navigation.compose) - androidTestImplementation(libs.navigation.testing) + // Navigation 3 + implementation(libs.navigation3.runtime) + implementation(libs.navigation3.ui) implementation(libs.hilt.navigation.compose) // Hilt - DI implementation(libs.hilt.android) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index c641b13bd..e5c6e158e 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -7,14 +7,13 @@ ComplexCondition:MapWebViewClient.kt$MapWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND ComplexCondition:ShopWebViewClient.kt$ShopWebViewClient$it.errorCode == ERROR_HOST_LOOKUP || it.errorCode == ERROR_CONNECT || it.errorCode == ERROR_TIMEOUT || it.errorCode == ERROR_FILE_NOT_FOUND CyclomaticComplexMethod:ActivityListGrouped.kt$private fun groupActivityItems(activityItems: List<Activity>): List<Any> - CyclomaticComplexMethod:ActivityRow.kt$@Composable fun ActivityRow( item: Activity, onClick: (String) -> Unit, testTag: String, ) + CyclomaticComplexMethod:ActivityRow.kt$@Composable fun ActivityRow( item: Activity, onClick: (Activity) -> Unit, testTag: String, ) CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private fun observeSendEvents() CyclomaticComplexMethod:AppViewModel.kt$AppViewModel$private suspend fun handleSanityChecks(amountSats: ULong) - CyclomaticComplexMethod:BlocktankRegtestScreen.kt$@Composable fun BlocktankRegtestScreen( navController: NavController, viewModel: BlocktankRegtestViewModel = hiltViewModel(), ) + CyclomaticComplexMethod:BlocktankRegtestScreen.kt$@Composable private fun BlocktankRegtestContent( onBack: () -> Unit, viewModel: BlocktankRegtestViewModel, ) CyclomaticComplexMethod:ConfirmMnemonicScreen.kt$@Composable fun ConfirmMnemonicScreen( uiState: BackupContract.UiState, onContinue: () -> Unit, onBack: () -> Unit, ) CyclomaticComplexMethod:HealthRepo.kt$HealthRepo$private fun collectState() - CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, rootNavController: NavController, walletNavController: NavHostController, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), ) - CyclomaticComplexMethod:SendSheet.kt$@Composable fun SendSheet( appViewModel: AppViewModel, walletViewModel: WalletViewModel, startDestination: SendRoute = SendRoute.Recipient, ) + CyclomaticComplexMethod:HomeScreen.kt$@Composable fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, navigator: Navigator, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), ) CyclomaticComplexMethod:SettingsButtonRow.kt$@Composable fun SettingsButtonRow( title: String, modifier: Modifier = Modifier, subtitle: String? = null, value: SettingsButtonValue = SettingsButtonValue.None, description: String? = null, iconRes: Int? = null, iconTint: Color = Color.Unspecified, iconSize: Dp = 32.dp, maxLinesSubtitle: Int = Int.MAX_VALUE, enabled: Boolean = true, loading: Boolean = false, onClick: () -> Unit, ) CyclomaticComplexMethod:Slider.kt$@Composable fun StepSlider( value: Int, steps: List<Int>, onValueChange: (Int) -> Unit, modifier: Modifier = Modifier, ) DestructuringDeclarationWithTooManyEntries:ActivityRow.kt$val (_, _, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current @@ -34,7 +33,6 @@ EnumNaming:BlocktankNotificationType.kt$BlocktankNotificationType$wakeToTimeout ForbiddenComment:ActivityDetailScreen.kt$/* TODO: Implement assign functionality */ ForbiddenComment:BoostTransactionViewModel.kt$BoostTransactionUiState$// TODO: Implement dynamic time estimation - ForbiddenComment:ContentView.kt$// TODO: display as sheet ForbiddenComment:ExternalNodeViewModel.kt$ExternalNodeViewModel$// TODO: pass customFeeRate to ldk-node when supported ForbiddenComment:LightningConnectionsViewModel.kt$LightningConnectionsViewModel$// TODO: sort channels to get consistent index; node.listChannels returns a list in random order ForbiddenComment:LightningService.kt$LightningService$// TODO: cleanup sensitive data after implementing a `SecureString` value holder for Keychain return values @@ -43,12 +41,11 @@ FunctionOnlyReturningConstant:ShopWebViewInterface.kt$ShopWebViewInterface$@JavascriptInterface fun isReady(): Boolean ImplicitDefaultLocale:BlocksService.kt$BlocksService$String.format("%.2f", blockInfo.difficulty / 1_000_000_000_000.0) ImplicitDefaultLocale:PriceService.kt$PriceService$String.format("%.2f", price) + ImportOrdering:TransferEntries.kt$import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.EntryProviderScope import androidx.navigation3.runtime.NavKey import to.bitkit.models.Toast import to.bitkit.ui.nav.Navigator import to.bitkit.ui.nav.Routes import to.bitkit.ui.screens.transfer.FundingAdvancedScreen import to.bitkit.ui.screens.transfer.FundingScreen import to.bitkit.ui.screens.transfer.LiquidityScreen import to.bitkit.ui.screens.transfer.SavingsAdvancedScreen import to.bitkit.ui.screens.transfer.SavingsAvailabilityScreen import to.bitkit.ui.screens.transfer.SavingsConfirmScreen import to.bitkit.ui.screens.transfer.SavingsIntroScreen import to.bitkit.ui.screens.transfer.SavingsProgressScreen import to.bitkit.ui.screens.transfer.SettingUpScreen import to.bitkit.ui.screens.transfer.SpendingAdvancedScreen import to.bitkit.ui.screens.transfer.SpendingAmountScreen import to.bitkit.ui.screens.transfer.SpendingConfirmScreen import to.bitkit.ui.screens.transfer.SpendingIntroScreen import to.bitkit.ui.screens.transfer.TransferIntroScreen import to.bitkit.ui.screens.transfer.external.ExternalAmountScreen import to.bitkit.ui.screens.transfer.external.ExternalConfirmScreen import to.bitkit.ui.screens.transfer.external.ExternalConnectionScreen import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen import to.bitkit.ui.screens.transfer.external.LnurlChannelScreen import to.bitkit.ui.screens.scanner.QrScanningScreen import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel InstanceOfCheckForException:LightningService.kt$LightningService$e is NodeException LargeClass:AppViewModel.kt$AppViewModel : ViewModel LargeClass:LightningRepo.kt$LightningRepo LongMethod:AppViewModel.kt$AppViewModel$private suspend fun proceedWithPayment() - LongMethod:ContentView.kt$@Suppress("LongParameterList") private fun NavGraphBuilder.home( walletViewModel: WalletViewModel, appViewModel: AppViewModel, activityListViewModel: ActivityListViewModel, settingsViewModel: SettingsViewModel, navController: NavHostController, drawerState: DrawerState, ) - LongMethod:ContentView.kt$private fun NavGraphBuilder.widgets( navController: NavHostController, settingsViewModel: SettingsViewModel, currencyViewModel: CurrencyViewModel, ) LongMethod:CoreService.kt$ActivityService$suspend fun generateRandomTestData(count: Int = 100) LongMethod:MainActivity.kt$MainActivity$override fun onCreate(savedInstanceState: Bundle?) LongParameterList:BiometricPrompt.kt$( activity: Context, title: String, cancelButtonText: String, onAuthSucceed: () -> Unit, onAuthFailed: (() -> Unit), onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit), ) @@ -112,6 +109,7 @@ MaxLineLength:HeadlineCard.kt$headline = "How Bitcoin changed El Salvador in more ways a big headline to test the text overflooooooow" MaxLineLength:LightningBalance.kt$is LightningBalance.ClaimableAwaitingConfirmations -> "Claimable Awaiting Confirmations (Height: $confirmationHeight)" MaxLineLength:LightningConnectionsScreen.kt$if (showClosed) R.string.lightning__conn_closed_hide else R.string.lightning__conn_closed_show + MaxLineLength:LightningRepo.kt$LightningRepo$"Cannot execute $operationName: Node is ${_lightningState.value.nodeLifecycleState} and not starting" MaxLineLength:LightningRepo.kt$LightningRepo$"accelerateByCpfp error originalTxId: $originalTxId, satsPerVByte: $satsPerVByte destinationAddress: $destinationAddress" MaxLineLength:LightningRepo.kt$LightningRepo$"accelerateByCpfp success, newDestinationTxId: $newDestinationTxId originalTxId: $originalTxId, satsPerVByte: $satsPerVByte destinationAddress: $destinationAddress" MaxLineLength:LightningRepo.kt$LightningRepo$"bumpFeeByRbf success, replacementTxId: $replacementTxId originalTxId: $originalTxId, satsPerVByte: $satsPerVByte" @@ -190,7 +188,6 @@ TooManyFunctions:BackupNavSheetViewModel.kt$BackupNavSheetViewModel : ViewModel TooManyFunctions:BlocktankRepo.kt$BlocktankRepo TooManyFunctions:CacheStore.kt$CacheStore - TooManyFunctions:ContentView.kt$to.bitkit.ui.ContentView.kt TooManyFunctions:CoreService.kt$ActivityService TooManyFunctions:CoreService.kt$BlocktankService TooManyFunctions:DevSettingsViewModel.kt$DevSettingsViewModel : ViewModel diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 0220b40ed..b934a0be9 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -1,181 +1,52 @@ package to.bitkit.ui -import android.content.Intent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -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.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.rememberHazeState +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import to.bitkit.env.Env import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.Toast -import to.bitkit.models.WidgetType -import to.bitkit.ui.Routes.ExternalConnection -import to.bitkit.ui.components.AuthCheckScreen import to.bitkit.ui.components.DrawerMenu -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.components.SheetHost import to.bitkit.ui.components.TabBar -import to.bitkit.ui.components.TimedSheetType +import to.bitkit.ui.nav.MS_NAV_DELAY +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.SheetSceneStrategy +import to.bitkit.ui.nav.Transitions +import to.bitkit.ui.nav.entries.homeEntries +import to.bitkit.ui.nav.entries.settingsEntries +import to.bitkit.ui.nav.entries.sheetEntries +import to.bitkit.ui.nav.entries.transferEntries +import to.bitkit.ui.nav.entries.widgetEntries import to.bitkit.ui.onboarding.InitializingWalletView import to.bitkit.ui.onboarding.WalletRestoreErrorView import to.bitkit.ui.onboarding.WalletRestoreSuccessView -import to.bitkit.ui.screens.CriticalUpdateScreen -import to.bitkit.ui.screens.profile.CreateProfileScreen -import to.bitkit.ui.screens.profile.ProfileIntroScreen -import to.bitkit.ui.screens.recovery.RecoveryMnemonicScreen -import to.bitkit.ui.screens.recovery.RecoveryModeScreen -import to.bitkit.ui.screens.scanner.QrScanningScreen -import to.bitkit.ui.screens.scanner.SCAN_REQUEST_KEY -import to.bitkit.ui.screens.settings.DevSettingsScreen -import to.bitkit.ui.screens.settings.FeeSettingsScreen -import to.bitkit.ui.screens.settings.LdkDebugScreen -import to.bitkit.ui.screens.shop.ShopIntroScreen -import to.bitkit.ui.screens.shop.shopDiscover.ShopDiscoverScreen -import to.bitkit.ui.screens.shop.shopWebView.ShopWebViewScreen -import to.bitkit.ui.screens.transfer.FundingAdvancedScreen -import to.bitkit.ui.screens.transfer.FundingScreen -import to.bitkit.ui.screens.transfer.LiquidityScreen -import to.bitkit.ui.screens.transfer.SavingsAdvancedScreen -import to.bitkit.ui.screens.transfer.SavingsAvailabilityScreen -import to.bitkit.ui.screens.transfer.SavingsConfirmScreen -import to.bitkit.ui.screens.transfer.SavingsIntroScreen -import to.bitkit.ui.screens.transfer.SavingsProgressScreen -import to.bitkit.ui.screens.transfer.SettingUpScreen -import to.bitkit.ui.screens.transfer.SpendingAdvancedScreen -import to.bitkit.ui.screens.transfer.SpendingAmountScreen -import to.bitkit.ui.screens.transfer.SpendingConfirmScreen -import to.bitkit.ui.screens.transfer.SpendingIntroScreen -import to.bitkit.ui.screens.transfer.TransferIntroScreen -import to.bitkit.ui.screens.transfer.external.ExternalAmountScreen -import to.bitkit.ui.screens.transfer.external.ExternalConfirmScreen -import to.bitkit.ui.screens.transfer.external.ExternalConnectionScreen -import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen -import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel -import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen -import to.bitkit.ui.screens.transfer.external.LnurlChannelScreen -import to.bitkit.ui.screens.wallets.HomeScreen -import to.bitkit.ui.screens.wallets.SavingsWalletScreen -import to.bitkit.ui.screens.wallets.SpendingWalletScreen -import to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen -import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen -import to.bitkit.ui.screens.wallets.activity.AllActivityScreen -import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorSheet -import to.bitkit.ui.screens.wallets.activity.TagSelectorSheet -import to.bitkit.ui.screens.wallets.receive.ReceiveSheet -import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen -import to.bitkit.ui.screens.widgets.AddWidgetsScreen -import to.bitkit.ui.screens.widgets.WidgetsIntroScreen -import to.bitkit.ui.screens.widgets.blocks.BlocksEditScreen -import to.bitkit.ui.screens.widgets.blocks.BlocksPreviewScreen -import to.bitkit.ui.screens.widgets.blocks.BlocksViewModel -import to.bitkit.ui.screens.widgets.calculator.CalculatorPreviewScreen -import to.bitkit.ui.screens.widgets.facts.FactsEditScreen -import to.bitkit.ui.screens.widgets.facts.FactsPreviewScreen -import to.bitkit.ui.screens.widgets.facts.FactsViewModel -import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditScreen -import to.bitkit.ui.screens.widgets.headlines.HeadlinesPreviewScreen -import to.bitkit.ui.screens.widgets.headlines.HeadlinesViewModel -import to.bitkit.ui.screens.widgets.price.PriceEditScreen -import to.bitkit.ui.screens.widgets.price.PricePreviewScreen -import to.bitkit.ui.screens.widgets.price.PriceViewModel -import to.bitkit.ui.screens.widgets.weather.WeatherEditScreen -import to.bitkit.ui.screens.widgets.weather.WeatherPreviewScreen -import to.bitkit.ui.screens.widgets.weather.WeatherViewModel -import to.bitkit.ui.settings.AboutScreen -import to.bitkit.ui.settings.AdvancedSettingsScreen -import to.bitkit.ui.settings.BackupSettingsScreen -import to.bitkit.ui.settings.BlocktankRegtestScreen -import to.bitkit.ui.settings.CJitDetailScreen -import to.bitkit.ui.settings.ChannelOrdersScreen -import to.bitkit.ui.settings.LanguageSettingsScreen -import to.bitkit.ui.settings.LogDetailScreen -import to.bitkit.ui.settings.LogsScreen -import to.bitkit.ui.settings.OrderDetailScreen -import to.bitkit.ui.settings.SecuritySettingsScreen -import to.bitkit.ui.settings.SettingsScreen -import to.bitkit.ui.settings.advanced.AddressViewerScreen -import to.bitkit.ui.settings.advanced.CoinSelectPreferenceScreen -import to.bitkit.ui.settings.advanced.ElectrumConfigScreen -import to.bitkit.ui.settings.advanced.RgsServerScreen -import to.bitkit.ui.settings.appStatus.AppStatusScreen -import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsIntroScreen -import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsSettings -import to.bitkit.ui.settings.backups.ResetAndRestoreScreen -import to.bitkit.ui.settings.general.DefaultUnitSettingsScreen -import to.bitkit.ui.settings.general.GeneralSettingsScreen -import to.bitkit.ui.settings.general.LocalCurrencySettingsScreen -import to.bitkit.ui.settings.general.TagsSettingsScreen -import to.bitkit.ui.settings.general.WidgetsSettingsScreen -import to.bitkit.ui.settings.lightning.ChannelDetailScreen -import to.bitkit.ui.settings.lightning.CloseConnectionScreen -import to.bitkit.ui.settings.lightning.LightningConnectionsScreen import to.bitkit.ui.settings.lightning.LightningConnectionsViewModel -import to.bitkit.ui.settings.pin.ChangePinConfirmScreen -import to.bitkit.ui.settings.pin.ChangePinNewScreen -import to.bitkit.ui.settings.pin.ChangePinResultScreen -import to.bitkit.ui.settings.pin.ChangePinScreen -import to.bitkit.ui.settings.pin.DisablePinScreen -import to.bitkit.ui.settings.quickPay.QuickPayIntroScreen -import to.bitkit.ui.settings.quickPay.QuickPaySettingsScreen -import to.bitkit.ui.settings.support.ReportIssueResultScreen -import to.bitkit.ui.settings.support.ReportIssueScreen -import to.bitkit.ui.settings.support.SupportScreen -import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen -import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen -import to.bitkit.ui.sheets.BackgroundPaymentsIntroSheet -import to.bitkit.ui.sheets.BackupRoute -import to.bitkit.ui.sheets.BackupSheet -import to.bitkit.ui.sheets.ForceTransferSheet -import to.bitkit.ui.sheets.GiftSheet -import to.bitkit.ui.sheets.HighBalanceWarningSheet -import to.bitkit.ui.sheets.LnurlAuthSheet -import to.bitkit.ui.sheets.PinSheet -import to.bitkit.ui.sheets.QuickPayIntroSheet -import to.bitkit.ui.sheets.SendSheet -import to.bitkit.ui.sheets.UpdateSheet -import to.bitkit.ui.theme.TRANSITION_SHEET_MS import to.bitkit.ui.utils.AutoReadClipboardHandler -import to.bitkit.ui.utils.RequestNotificationPermissions -import to.bitkit.ui.utils.Transitions -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.ui.utils.navigationWithDefaultTransitions import to.bitkit.utils.Logger import to.bitkit.viewmodels.ActivityListViewModel import to.bitkit.viewmodels.AppViewModel @@ -184,11 +55,12 @@ import to.bitkit.viewmodels.BlocktankViewModel import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.MainScreenEffect import to.bitkit.viewmodels.RestoreState +import to.bitkit.viewmodels.SendEffect import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel -@Suppress("CyclomaticComplexMethod") +@Suppress("CyclomaticComplexMethod", "LongMethod") @Composable fun ContentView( appViewModel: AppViewModel, @@ -201,10 +73,11 @@ fun ContentView( backupsViewModel: BackupsViewModel, modifier: Modifier = Modifier, ) { - val navController = rememberNavController() + val backStack = rememberNavBackStack(Routes.Home) + val navigator = remember(backStack) { Navigator(backStack) } + val lightningConnectionsViewModel = hiltViewModel() val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val context = LocalContext.current val lifecycle = LocalLifecycleOwner.current.lifecycle val walletUiState by walletViewModel.walletState.collectAsStateWithLifecycle() @@ -253,12 +126,11 @@ fun ContentView( LaunchedEffect(appViewModel) { appViewModel.mainScreenEffect.collect { when (it) { - is MainScreenEffect.Navigate -> navController.navigate(it.route, navOptions = it.navOptions) + is MainScreenEffect.Navigate -> navigator.navigate(it.route) is MainScreenEffect.ProcessClipboardAutoRead -> { - val isOnHome = navController.currentDestination?.hasRoute() == true - if (!isOnHome) { - navController.navigateToHome() - delay(100) // Small delay to ensure navigation completes + if (!navigator.isAtHome()) { + navigator.navigateToHome() + delay(MS_NAV_DELAY) } appViewModel.onScanResult(it.data) } @@ -268,6 +140,30 @@ fun ContentView( } } + // Handle Send flow navigation effects + LaunchedEffect(appViewModel, navigator) { + appViewModel.sendEffect.collect { effect -> + when (effect) { + is SendEffect.NavigateToAddress -> navigator.navigate(Routes.SendAddress) + is SendEffect.NavigateToAmount -> navigator.navigate(Routes.SendAmount()) + is SendEffect.NavigateToScan -> navigator.navigate(Routes.SendQrScanner) + is SendEffect.NavigateToCoinSelection -> navigator.navigate(Routes.SendCoinSelection) + is SendEffect.NavigateToConfirm -> navigator.navigate(Routes.SendConfirm) + is SendEffect.NavigateToQuickPay -> navigator.navigate(Routes.SendQuickPay) + is SendEffect.NavigateToWithdrawConfirm -> navigator.navigate(Routes.SendWithdrawConfirm) + is SendEffect.NavigateToWithdrawError -> navigator.navigate(Routes.SendWithdrawError) + is SendEffect.NavigateToFee -> navigator.navigate(Routes.SendFeeRate) + is SendEffect.NavigateToFeeCustom -> navigator.navigate(Routes.SendFeeCustom) + is SendEffect.PaymentSuccess -> { + appViewModel.clearClipboardForAutoRead() + navigator.navigate(Routes.SendSuccess) + } + + is SendEffect.PopBack -> navigator.popBackTo(effect.route) + } + } + } + var walletIsInitializing by remember { mutableStateOf(nodeLifecycleState == NodeLifecycleState.Initializing) } var walletInitShouldFinish by remember { mutableStateOf(false) } @@ -294,7 +190,6 @@ fun ContentView( var restoreRetryCount by remember { mutableIntStateOf(0) } if (walletIsInitializing) { - // TODO ADAPT THIS LOGIC TO WORK WITH LightningNodeService if (nodeLifecycleState is NodeLifecycleState.ErrorStarting) { WalletRestoreErrorView( retryCount = restoreRetryCount, @@ -352,128 +247,79 @@ fun ContentView( LocalBalances provides balance, LocalCurrencies provides currencies, ) { - AutoReadClipboardHandler() - val hasSeenWidgetsIntro by settingsViewModel.hasSeenWidgetsIntro.collectAsStateWithLifecycle() val hasSeenShopIntro by settingsViewModel.hasSeenShopIntro.collectAsStateWithLifecycle() - val currentSheet by appViewModel.currentSheet.collectAsStateWithLifecycle() - Box( - modifier = modifier.fillMaxSize() - ) { - SheetHost( - shouldExpand = currentSheet != null, - onDismiss = { appViewModel.hideSheet() }, - sheets = { - when (val sheet = currentSheet) { - null -> Unit - is Sheet.Send -> { - SendSheet( - appViewModel = appViewModel, - walletViewModel = walletViewModel, - startDestination = sheet.route, - ) - } - - is Sheet.Receive -> { - val walletUiState by walletViewModel.uiState.collectAsState() - ReceiveSheet( - walletState = walletUiState, - navigateToExternalConnection = { - navController.navigate(ExternalConnection()) - appViewModel.hideSheet() - } - ) - } + AutoReadClipboardHandler() - is Sheet.ActivityDateRangeSelector -> DateRangeSelectorSheet() - is Sheet.ActivityTagSelector -> TagSelectorSheet() - is Sheet.Pin -> PinSheet(sheet, appViewModel) - is Sheet.Backup -> BackupSheet(sheet, onDismiss = { appViewModel.hideSheet() }) - is Sheet.LnurlAuth -> LnurlAuthSheet(sheet, appViewModel) - Sheet.ForceTransfer -> ForceTransferSheet(appViewModel, transferViewModel) - is Sheet.Gift -> GiftSheet(sheet, appViewModel) - is Sheet.TimedSheet -> { - when (sheet.type) { - TimedSheetType.APP_UPDATE -> { - UpdateSheet(onCancel = { appViewModel.dismissTimedSheet() }) - } + Box(modifier = modifier.fillMaxSize()) { + Box(modifier = Modifier.fillMaxSize()) { + NavDisplay( + backStack = backStack, + modifier = Modifier.fillMaxSize(), + sceneStrategy = SheetSceneStrategy(), + transitionSpec = Transitions.screenDefault, + popTransitionSpec = Transitions.screenDefaultPop, + predictivePopTransitionSpec = Transitions.screenDefaultPredictivePop, + entryProvider = entryProvider { + homeEntries( + navigator = navigator, + drawerState = drawerState, + walletViewModel = walletViewModel, + appViewModel = appViewModel, + activityListViewModel = activityListViewModel, + settingsViewModel = settingsViewModel, + ) - TimedSheetType.BACKUP -> { - BackupSheet( - sheet = Sheet.Backup(BackupRoute.Intro), - onDismiss = { appViewModel.dismissTimedSheet() } - ) - } + settingsEntries( + navigator = navigator, + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, + currencyViewModel = currencyViewModel, + lightningConnectionsViewModel = lightningConnectionsViewModel, + ) - TimedSheetType.NOTIFICATIONS -> { - BackgroundPaymentsIntroSheet( - onContinue = { - appViewModel.dismissTimedSheet(skipQueue = true) - navController.navigate(Routes.BackgroundPaymentsSettings) - settingsViewModel.setBgPaymentsIntroSeen(true) - }, - ) - } + transferEntries( + navigator = navigator, + appViewModel = appViewModel, + walletViewModel = walletViewModel, + transferViewModel = transferViewModel, + settingsViewModel = settingsViewModel, + ) - TimedSheetType.QUICK_PAY -> { - QuickPayIntroSheet( - onContinue = { - appViewModel.dismissTimedSheet(skipQueue = true) - navController.navigate(Routes.QuickPaySettings) - }, - ) - } + widgetEntries( + navigator = navigator, + currencyViewModel = currencyViewModel, + settingsViewModel = settingsViewModel, + ) - TimedSheetType.HIGH_BALANCE -> { - HighBalanceWarningSheet( - understoodClick = { appViewModel.dismissTimedSheet() }, - learnMoreClick = { - val intent = - Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri()) - context.startActivity(intent) - appViewModel.dismissTimedSheet(skipQueue = true) - } - ) - } - } - } + sheetEntries( + navigator = navigator, + appViewModel = appViewModel, + walletViewModel = walletViewModel, + activityListViewModel = activityListViewModel, + transferViewModel = transferViewModel, + ) } - } - ) { - Box(modifier = Modifier.fillMaxSize()) { - RootNavHost( - navController = navController, - drawerState = drawerState, - walletViewModel = walletViewModel, - appViewModel = appViewModel, - activityListViewModel = activityListViewModel, - settingsViewModel = settingsViewModel, - currencyViewModel = currencyViewModel, - transferViewModel = transferViewModel, - ) + ) - val navBackStackEntry by navController.currentBackStackEntryAsState() - val currentRoute = navBackStackEntry?.destination?.route - val showTabBar = currentRoute in listOf( - Routes.Home::class.qualifiedName, - Routes.AllActivity::class.qualifiedName, + AnimatedVisibility( + visible = navigator.shouldShowTabBar(), + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + modifier = Modifier.align(Alignment.BottomCenter), + ) { + TabBar( + onSendClick = { navigator.navigate(Routes.SendRecipient) }, + onReceiveClick = { navigator.navigate(Routes.ReceiveQr) }, + onScanClick = { navigator.navigate(Routes.QrScanner) }, ) - - if (showTabBar) { - TabBar( - onSendClick = { appViewModel.showSheet(Sheet.Send()) }, - onReceiveClick = { appViewModel.showSheet(Sheet.Receive) }, - onScanClick = { navController.navigateToScanner() }, - modifier = Modifier.align(Alignment.BottomCenter) - ) - } } } DrawerMenu( drawerState = drawerState, - rootNavController = navController, + navigator = navigator, hasSeenWidgetsIntro = hasSeenWidgetsIntro, hasSeenShopIntro = hasSeenShopIntro, modifier = Modifier.align(Alignment.TopEnd), @@ -481,1477 +327,3 @@ fun ContentView( } } } - -@Composable -private fun RootNavHost( - navController: NavHostController, - drawerState: DrawerState, - walletViewModel: WalletViewModel, - appViewModel: AppViewModel, - activityListViewModel: ActivityListViewModel, - settingsViewModel: SettingsViewModel, - currencyViewModel: CurrencyViewModel, - transferViewModel: TransferViewModel, -) { - val scope = rememberCoroutineScope() - - NavHost(navController, startDestination = Routes.Home) { - home( - walletViewModel = walletViewModel, - appViewModel = appViewModel, - activityListViewModel = activityListViewModel, - settingsViewModel = settingsViewModel, - navController = navController, - drawerState = drawerState, - ) - allActivity( - activityListViewModel = activityListViewModel, - navController = navController, - ) - settings(navController, settingsViewModel) - profile(navController, settingsViewModel) - shop(navController, settingsViewModel, appViewModel) - generalSettings(navController) - advancedSettings(navController) - aboutSettings(navController) - transactionSpeedSettings(navController) - securitySettings(navController) - disablePin(navController) - changePin(navController) - changePinNew(navController) - changePinConfirm(navController) - changePinResult(navController) - defaultUnitSettings(currencyViewModel, navController) - localCurrencySettings(currencyViewModel, navController) - backupSettings(navController) - resetAndRestoreSettings(navController) - channelOrdersSettings(navController) - orderDetailSettings(navController) - cjitDetailSettings(navController) - lightningConnections(navController) - activityItem(activityListViewModel, navController) - qrScanner(appViewModel, navController) - authCheck(navController) - logs(navController) - suggestions(navController) - support(navController) - widgets(navController, settingsViewModel, currencyViewModel) - update() - recoveryMode(navController, appViewModel) - - // TODO extract transferNavigation - navigationWithDefaultTransitions( - startDestination = Routes.TransferIntro, - ) { - composableWithDefaultTransitions { - TransferIntroScreen( - onContinueClick = { - navController.navigateToTransferFunding() - settingsViewModel.setHasSeenTransferIntro(true) - }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - SavingsIntroScreen( - onContinueClick = { - navController.navigate(Routes.SavingsAvailability) - settingsViewModel.setHasSeenSavingsIntro(true) - }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - SavingsAvailabilityScreen( - onBackClick = { navController.popBackStack() }, - onCancelClick = { navController.navigateToHome() }, - onContinueClick = { navController.navigate(Routes.SavingsConfirm) }, - ) - } - composableWithDefaultTransitions { - SavingsConfirmScreen( - onConfirm = { navController.navigate(Routes.SavingsProgress) }, - onAdvancedClick = { navController.navigate(Routes.SavingsAdvanced) }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - SavingsAdvancedScreen( - onContinueClick = { navController.popBackStack(inclusive = false) }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - SavingsProgressScreen( - app = appViewModel, - wallet = walletViewModel, - transfer = transferViewModel, - onContinueClick = { navController.popBackStack(inclusive = true) }, - ) - } - composableWithDefaultTransitions { - SpendingIntroScreen( - onContinueClick = { - navController.navigate(Routes.SpendingAmount) - settingsViewModel.setHasSeenSpendingIntro(true) - }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - SpendingAmountScreen( - viewModel = transferViewModel, - onBackClick = { navController.popBackStack() }, - onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, - toastException = { appViewModel.toast(it) }, - toast = { title, description -> - appViewModel.toast( - type = Toast.ToastType.ERROR, - title = title, - description = description - ) - }, - ) - } - composableWithDefaultTransitions { - SpendingConfirmScreen( - viewModel = transferViewModel, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - onLearnMoreClick = { navController.navigate(Routes.TransferLiquidity) }, - onAdvancedClick = { navController.navigate(Routes.SpendingAdvanced) }, - onConfirm = { navController.navigate(Routes.SettingUp) }, - ) - } - composableWithDefaultTransitions { - SpendingAdvancedScreen( - viewModel = transferViewModel, - onBackClick = { navController.popBackStack() }, - onOrderCreated = { navController.popBackStack(inclusive = false) }, - ) - } - composableWithDefaultTransitions { - LiquidityScreen( - onBackClick = { navController.popBackStack() }, - onContinueClick = { navController.popBackStack() } - ) - } - composableWithDefaultTransitions { - SettingUpScreen( - viewModel = transferViewModel, - onContinueClick = { - navController.navigateToHome() - } - ) - } - composableWithDefaultTransitions { - val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsState() - val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() - - FundingScreen( - onTransfer = { - if (!hasSeenSpendingIntro) { - navController.navigateToTransferSpendingIntro() - } else { - navController.navigateToTransferSpendingAmount() - } - }, - onFund = { - scope.launch { - // TODO show receive sheet -> ReceiveAmount - navController.navigateToHome() - delay(500) // Wait for nav to actually finish - appViewModel.showSheet(Sheet.Receive) - } - }, - onAdvanced = { navController.navigate(Routes.FundingAdvanced) }, - onBackClick = { navController.popBackStack() }, - isGeoBlocked = isGeoBlocked, - ) - } - composableWithDefaultTransitions { - FundingAdvancedScreen( - onLnurl = { navController.navigateToScanner() }, - onManual = { navController.navigate(Routes.ExternalNav) }, - onBackClick = { navController.popBackStack() }, - ) - } - navigationWithDefaultTransitions( - startDestination = ExternalConnection(), - ) { - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } - val route = it.toRoute() - val viewModel = hiltViewModel(parentEntry) - - ExternalConnectionScreen( - route = route, - savedStateHandle = it.savedStateHandle, - viewModel = viewModel, - onNodeConnected = { navController.navigate(Routes.ExternalAmount) }, - onScanClick = { navController.navigateToScanner(isCalledForResult = true) }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } - val viewModel = hiltViewModel(parentEntry) - - ExternalAmountScreen( - viewModel = viewModel, - onContinue = { navController.navigate(Routes.ExternalConfirm) }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } - val viewModel = hiltViewModel(parentEntry) - - ExternalConfirmScreen( - viewModel = viewModel, - onConfirm = { - walletViewModel.refreshState() - navController.navigate(Routes.ExternalSuccess) - }, - onNetworkFeeClick = { navController.navigate(Routes.ExternalFeeCustom) }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - LnurlChannelScreen( - route = it.toRoute(), - onConnected = { navController.navigate(Routes.ExternalSuccess) }, - onBack = { navController.popBackStack() }, - onClose = { navController.navigateToHome() }, - ) - } - composableWithDefaultTransitions { - ExternalSuccessScreen( - onContinue = { navController.popBackStack(inclusive = true) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } - val viewModel = hiltViewModel(parentEntry) - - ExternalFeeCustomScreen( - viewModel = viewModel, - onBack = { navController.popBackStack() }, - ) - } - } - } - } -} - -// region destinations -@Suppress("LongParameterList") -private fun NavGraphBuilder.home( - walletViewModel: WalletViewModel, - appViewModel: AppViewModel, - activityListViewModel: ActivityListViewModel, - settingsViewModel: SettingsViewModel, - navController: NavHostController, - drawerState: DrawerState, -) { - composable { - val uiState by walletViewModel.uiState.collectAsStateWithLifecycle() - val isRecoveryMode by walletViewModel.isRecoveryMode.collectAsStateWithLifecycle() - val hazeState = rememberHazeState() - - RequestNotificationPermissions( - showPermissionDialog = !isRecoveryMode, - onPermissionChange = { granted -> - settingsViewModel.setNotificationPreference(granted) - } - ) - Box( - modifier = Modifier - .fillMaxSize() - .hazeSource(hazeState) - ) { - HomeScreen( - mainUiState = uiState, - drawerState = drawerState, - rootNavController = navController, - walletNavController = navController, - settingsViewModel = settingsViewModel, - walletViewModel = walletViewModel, - appViewModel = appViewModel, - activityListViewModel = activityListViewModel, - ) - } - } - composable( - enterTransition = { Transitions.slideInHorizontally }, - exitTransition = { Transitions.slideOutHorizontally }, - ) { - val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() - val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() - val onchainActivities by activityListViewModel.onchainActivities.collectAsStateWithLifecycle() - - SavingsWalletScreen( - isGeoBlocked = isGeoBlocked, - onchainActivities = onchainActivities.orEmpty(), - onAllActivityButtonClick = { navController.navigateToAllActivity() }, - onActivityItemClick = { navController.navigateToActivityItem(it) }, - onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, - onTransferToSpendingClick = { - if (!hasSeenSpendingIntro) { - navController.navigateToTransferSpendingIntro() - } else { - navController.navigateToTransferSpendingAmount() - } - }, - onBackClick = { navController.popBackStack() }, - ) - } - composable( - enterTransition = { Transitions.slideInHorizontally }, - exitTransition = { Transitions.slideOutHorizontally }, - ) { - val hasSeenSavingsIntro by settingsViewModel.hasSeenSavingsIntro.collectAsStateWithLifecycle() - val uiState by walletViewModel.uiState.collectAsStateWithLifecycle() - val lightningActivities by activityListViewModel.lightningActivities.collectAsStateWithLifecycle() - - SpendingWalletScreen( - uiState = uiState, - lightningActivities = lightningActivities.orEmpty(), - onAllActivityButtonClick = { navController.navigateToAllActivity() }, - onActivityItemClick = { navController.navigateToActivityItem(it) }, - onEmptyActivityRowClick = { appViewModel.showSheet(Sheet.Receive) }, - onTransferToSavingsClick = { - if (!hasSeenSavingsIntro) { - navController.navigateToTransferSavingsIntro() - } else { - navController.navigateToTransferSavingsAvailability() - } - }, - onBackClick = { navController.popBackStack() }, - ) - } -} - -private fun NavGraphBuilder.allActivity( - activityListViewModel: ActivityListViewModel, - navController: NavHostController, -) { - composableWithDefaultTransitions { - AllActivityScreen( - viewModel = activityListViewModel, - onBack = { - activityListViewModel.clearFilters() - navController.navigateToHome() - }, - onActivityItemClick = { id -> navController.navigateToActivityItem(id) }, - ) - } -} - -private fun NavGraphBuilder.settings( - navController: NavHostController, - settingsViewModel: SettingsViewModel, -) { - composableWithDefaultTransitions { - SettingsScreen(navController) - } - // TODO: display as sheet - composableWithDefaultTransitions { - QuickPayIntroScreen( - onBack = { navController.popBackStack() }, - onContinue = { - settingsViewModel.setQuickPayIntroSeen(true) - navController.navigate(Routes.QuickPaySettings) - } - ) - } - composableWithDefaultTransitions { - QuickPaySettingsScreen( - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - DevSettingsScreen(navController) - } - composableWithDefaultTransitions { - LdkDebugScreen(navController) - } - composableWithDefaultTransitions { - FeeSettingsScreen(navController) - } - composableWithDefaultTransitions { - BlocktankRegtestScreen(navController) - } - composableWithDefaultTransitions { - LanguageSettingsScreen( - onBackClick = { navController.popBackStack() }, - ) - } -} - -private fun NavGraphBuilder.profile( - navController: NavHostController, - settingsViewModel: SettingsViewModel, -) { - composableWithDefaultTransitions { - ProfileIntroScreen( - onContinue = { - settingsViewModel.setHasSeenProfileIntro(true) - navController.navigate(Routes.CreateProfile) - }, - onBackClick = { navController.popBackStack() } - ) - } - composableWithDefaultTransitions { - CreateProfileScreen( - onBack = { navController.popBackStack() }, - ) - } -} - -private fun NavGraphBuilder.shop( - navController: NavHostController, - settingsViewModel: SettingsViewModel, - appViewModel: AppViewModel, -) { - composableWithDefaultTransitions { - ShopIntroScreen( - onContinue = { - settingsViewModel.setHasSeenShopIntro(true) - navController.navigate(Routes.ShopDiscover) - }, - onBackClick = { - navController.popBackStack() - } - ) - } - composableWithDefaultTransitions { - ShopDiscoverScreen( - onBack = { navController.popBackStack() }, - navigateWebView = { page, title -> - navController.navigate(Routes.ShopWebView(page = page, title = title)) - } - ) - } - composableWithDefaultTransitions { - ShopWebViewScreen( - onClose = { navController.navigateToHome() }, - onBack = { navController.popBackStack() }, - page = it.toRoute().page, - title = it.toRoute().title, - onPaymentIntent = { data -> - appViewModel.onScanResult(data) - } - ) - } -} - -private fun NavGraphBuilder.generalSettings(navController: NavHostController) { - composableWithDefaultTransitions { - GeneralSettingsScreen(navController) - } - - composableWithDefaultTransitions { - WidgetsSettingsScreen(navController) - } - - composableWithDefaultTransitions { - TagsSettingsScreen(navController) - } - composableWithDefaultTransitions { - BackgroundPaymentsSettings( - onBack = { navController.popBackStack() }, - ) - } - - composableWithDefaultTransitions { - BackgroundPaymentsIntroScreen( - onBack = { navController.popBackStack() }, - onContinue = { - navController.navigate(Routes.BackgroundPaymentsSettings) - } - ) - } -} - -private fun NavGraphBuilder.advancedSettings(navController: NavHostController) { - composableWithDefaultTransitions { - AdvancedSettingsScreen(navController) - } - composableWithDefaultTransitions { - CoinSelectPreferenceScreen(navController) - } - composableWithDefaultTransitions { - ElectrumConfigScreen(it.savedStateHandle, navController) - } - composableWithDefaultTransitions { - RgsServerScreen(it.savedStateHandle, navController) - } - composableWithDefaultTransitions { - AddressViewerScreen(navController) - } - composableWithDefaultTransitions { - NodeInfoScreen(navController) - } -} - -private fun NavGraphBuilder.aboutSettings(navController: NavHostController) { - composableWithDefaultTransitions { - AboutScreen( - onBack = { - navController.popBackStack() - } - ) - } -} - -private fun NavGraphBuilder.transactionSpeedSettings(navController: NavHostController) { - composableWithDefaultTransitions { - TransactionSpeedSettingsScreen(navController) - } - composableWithDefaultTransitions { - CustomFeeSettingsScreen(navController) - } -} - -private fun NavGraphBuilder.securitySettings(navController: NavHostController) { - composableWithDefaultTransitions { - SecuritySettingsScreen(navController = navController) - } -} - -private fun NavGraphBuilder.disablePin(navController: NavHostController) { - composableWithDefaultTransitions { - DisablePinScreen(navController) - } -} - -private fun NavGraphBuilder.changePin(navController: NavHostController) { - composableWithDefaultTransitions { - ChangePinScreen(navController) - } -} - -private fun NavGraphBuilder.changePinNew(navController: NavHostController) { - composableWithDefaultTransitions { - ChangePinNewScreen(navController) - } -} - -private fun NavGraphBuilder.changePinConfirm(navController: NavHostController) { - composableWithDefaultTransitions { - val route = it.toRoute() - ChangePinConfirmScreen( - newPin = route.newPin, - navController = navController, - ) - } -} - -private fun NavGraphBuilder.changePinResult(navController: NavHostController) { - composableWithDefaultTransitions { - ChangePinResultScreen(navController) - } -} - -private fun NavGraphBuilder.defaultUnitSettings( - currencyViewModel: CurrencyViewModel, - navController: NavHostController, -) { - composableWithDefaultTransitions { - DefaultUnitSettingsScreen(currencyViewModel, navController) - } -} - -private fun NavGraphBuilder.localCurrencySettings( - currencyViewModel: CurrencyViewModel, - navController: NavHostController, -) { - composableWithDefaultTransitions { - LocalCurrencySettingsScreen(currencyViewModel, navController) - } -} - -private fun NavGraphBuilder.backupSettings( - navController: NavHostController, -) { - composableWithDefaultTransitions { - BackupSettingsScreen(navController) - } -} - -private fun NavGraphBuilder.resetAndRestoreSettings( - navController: NavHostController, -) { - composableWithDefaultTransitions { - ResetAndRestoreScreen(navController) - } -} - -private fun NavGraphBuilder.channelOrdersSettings( - navController: NavHostController, -) { - composableWithDefaultTransitions { - ChannelOrdersScreen( - onBackClick = { navController.popBackStack() }, - onOrderItemClick = { navController.navigateToOrderDetail(it) }, - onCjitItemClick = { navController.navigateToCjitDetail(it) }, - ) - } -} - -private fun NavGraphBuilder.orderDetailSettings( - navController: NavHostController, -) { - composableWithDefaultTransitions { - OrderDetailScreen( - orderItem = it.toRoute(), - onBackClick = { navController.popBackStack() }, - ) - } -} - -private fun NavGraphBuilder.cjitDetailSettings( - navController: NavHostController, -) { - composableWithDefaultTransitions { - CJitDetailScreen( - cjitItem = it.toRoute(), - onBackClick = { navController.popBackStack() }, - ) - } -} - -private fun NavGraphBuilder.lightningConnections( - navController: NavHostController, -) { - navigationWithDefaultTransitions( - startDestination = Routes.LightningConnections, - ) { - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ConnectionsNav) } - val viewModel = hiltViewModel(parentEntry) - LightningConnectionsScreen(navController, viewModel) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ConnectionsNav) } - val viewModel = hiltViewModel(parentEntry) - ChannelDetailScreen( - navController = navController, - viewModel = viewModel, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ConnectionsNav) } - val viewModel = hiltViewModel(parentEntry) - CloseConnectionScreen( - navController = navController, - viewModel = viewModel, - ) - } - } -} - -private fun NavGraphBuilder.activityItem( - activityListViewModel: ActivityListViewModel, - navController: NavHostController, -) { - composableWithDefaultTransitions { - ActivityDetailScreen( - listViewModel = activityListViewModel, - route = it.toRoute(), - onExploreClick = { id -> navController.navigateToActivityExplore(id) }, - onChannelClick = { channelId -> - navController.currentBackStackEntry?.savedStateHandle?.set("selectedChannelId", channelId) - navController.navigate(Routes.ConnectionsNav) { - launchSingleTop = true - } - }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - ) - } - composableWithDefaultTransitions { - ActivityExploreScreen( - route = it.toRoute(), - onBackClick = { navController.popBackStack() }, - ) - } -} - -private fun NavGraphBuilder.qrScanner( - appViewModel: AppViewModel, - navController: NavHostController, -) { - composableWithDefaultTransitions( - enterTransition = { Transitions.slideInVertically }, - popExitTransition = { Transitions.slideOutVertically }, - ) { - QrScanningScreen(navController = navController) { qrCode -> - appViewModel.onScanResult( - data = qrCode, - delayMs = TRANSITION_SHEET_MS, - ) - } - } -} - -private fun NavGraphBuilder.authCheck( - navController: NavHostController, -) { - composableWithDefaultTransitions { - val route = it.toRoute() - AuthCheckScreen( - route = route, - navController = navController, - ) - } -} - -private fun NavGraphBuilder.logs( - navController: NavHostController, -) { - composableWithDefaultTransitions { - LogsScreen(navController) - } - composableWithDefaultTransitions { - val route = it.toRoute() - LogDetailScreen( - navController = navController, - fileName = route.fileName, - ) - } -} - -private fun NavGraphBuilder.suggestions( - navController: NavHostController, -) { - composableWithDefaultTransitions { - BuyIntroScreen( - onBackClick = { navController.popBackStack() } - ) - } -} - -private fun NavGraphBuilder.update() { - composableWithDefaultTransitions { - CriticalUpdateScreen() - } -} - -private fun NavGraphBuilder.recoveryMode( - navController: NavHostController, - appViewModel: AppViewModel, -) { - composableWithDefaultTransitions { - RecoveryModeScreen( - onNavigateToSeed = { - navController.navigate(Routes.RecoveryMnemonic) - }, - appViewModel = appViewModel - ) - } - composableWithDefaultTransitions { - RecoveryMnemonicScreen( - onNavigateBack = { - navController.popBackStack() - } - ) - } -} - -private fun NavGraphBuilder.support( - navController: NavHostController, -) { - composableWithDefaultTransitions { - SupportScreen(navController) - } - - composableWithDefaultTransitions { - AppStatusScreen(navController) - } - - composableWithDefaultTransitions { - ReportIssueScreen( - onBack = { navController.popBackStack() }, - navigateResultScreen = { isSuccess -> - if (isSuccess) { - navController.navigate(Routes.ReportIssueSuccess) - } else { - navController.navigate(Routes.ReportIssueFailure) - } - } - ) - } - - composableWithDefaultTransitions { - ReportIssueResultScreen( - isSuccess = true, - onBack = { navController.popBackStack() }, - onClose = { navController.navigateToHome() }, - ) - } - - composableWithDefaultTransitions { - ReportIssueResultScreen( - isSuccess = false, - onBack = { navController.popBackStack() }, - onClose = { navController.navigateToHome() }, - ) - } -} - -private fun NavGraphBuilder.widgets( - navController: NavHostController, - settingsViewModel: SettingsViewModel, - currencyViewModel: CurrencyViewModel, -) { - composableWithDefaultTransitions { - WidgetsIntroScreen( - onContinue = { - settingsViewModel.setHasSeenWidgetsIntro(true) - navController.navigate(Routes.AddWidget) - }, - onBackClick = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - 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) - } - }, - fiatSymbol = LocalCurrencies.current.currencySymbol, - onBackCLick = { navController.popBackStack() } - ) - } - composableWithDefaultTransitions { - CalculatorPreviewScreen( - onClose = { navController.navigateToHome() }, - onBack = { navController.popBackStack() }, - currencyViewModel = currencyViewModel - ) - } - navigationWithDefaultTransitions( - startDestination = Routes.HeadlinesPreview - ) { - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Headlines) } - val viewModel = hiltViewModel(parentEntry) - - HeadlinesPreviewScreen( - headlinesViewModel = viewModel, - onClose = { navController.navigateToHome() }, - onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.HeadlinesEdit) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Headlines) } - val viewModel = hiltViewModel(parentEntry) - - HeadlinesEditScreen( - headlinesViewModel = viewModel, - onBack = { navController.popBackStack() }, - navigatePreview = { - navController.navigate(Routes.HeadlinesPreview) - } - ) - } - } - navigationWithDefaultTransitions( - startDestination = Routes.FactsPreview - ) { - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Facts) } - val viewModel = hiltViewModel(parentEntry) - - FactsPreviewScreen( - factsViewModel = viewModel, - onClose = { navController.navigateToHome() }, - onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.FactsEdit) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Facts) } - val viewModel = hiltViewModel(parentEntry) - - FactsEditScreen( - factsViewModel = viewModel, - onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.FactsPreview) } - ) - } - } - navigationWithDefaultTransitions( - startDestination = Routes.BlocksPreview - ) { - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Blocks) } - val viewModel = hiltViewModel(parentEntry) - - BlocksPreviewScreen( - blocksViewModel = viewModel, - onClose = { navController.navigateToHome() }, - onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.BlocksEdit) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Blocks) } - val viewModel = hiltViewModel(parentEntry) - - BlocksEditScreen( - blocksViewModel = viewModel, - onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.BlocksPreview) } - ) - } - } - navigationWithDefaultTransitions( - startDestination = Routes.WeatherPreview - ) { - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Weather) } - val viewModel = hiltViewModel(parentEntry) - - WeatherPreviewScreen( - weatherViewModel = viewModel, - onClose = { navController.navigateToHome() }, - onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.WeatherEdit) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Weather) } - val viewModel = hiltViewModel(parentEntry) - - WeatherEditScreen( - weatherViewModel = viewModel, - onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.WeatherPreview) } - ) - } - } - navigationWithDefaultTransitions( - startDestination = Routes.PricePreview - ) { - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Price) } - val viewModel = hiltViewModel(parentEntry) - - PricePreviewScreen( - priceViewModel = viewModel, - onClose = { navController.navigateToHome() }, - onBack = { navController.popBackStack() }, - navigateEditWidget = { navController.navigate(Routes.PriceEdit) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Price) } - val viewModel = hiltViewModel(parentEntry) - PriceEditScreen( - viewModel = viewModel, - onBack = { navController.popBackStack() }, - navigatePreview = { navController.navigate(Routes.PricePreview) } - ) - } - } -} - -// endregion - -// region events -fun NavController.navigateToHome() { - val popped = popBackStack(inclusive = false) - if (!popped) { - navigate(Routes.Home) { - popUpTo(graph.startDestinationId) - launchSingleTop = true - } - } -} - -fun NavController.navigateToAllActivity() { - navigate(Routes.AllActivity) { - launchSingleTop = true - } -} - -/** - * Navigates to the specified route only if not already on that route. - */ -inline fun NavController.navigateIfNotCurrent(route: T) { - val isOnRoute = currentBackStackEntry?.destination?.hasRoute() ?: false - if (!isOnRoute) { - navigate(route) - } -} - -fun NavController.navigateToGeneralSettings() = navigate( - route = Routes.GeneralSettings, -) - -fun NavController.navigateToSecuritySettings() = navigate( - route = Routes.SecuritySettings, -) - -fun NavController.navigateToDisablePin() = navigate( - route = Routes.DisablePin, -) - -fun NavController.navigateToChangePin() = navigate( - route = Routes.ChangePin, -) - -fun NavController.navigateToChangePinNew() = navigate( - route = Routes.ChangePinNew, -) - -fun NavController.navigateToChangePinConfirm(newPin: String) = navigate( - route = Routes.ChangePinConfirm(newPin), -) - -fun NavController.navigateToChangePinResult() = navigate( - route = Routes.ChangePinResult, -) - -fun NavController.navigateToAuthCheck( - showLogoOnPin: Boolean = false, - requirePin: Boolean = false, - requireBiometrics: Boolean = false, - onSuccessActionId: String, - navOptions: NavOptions? = null, -) = navigate( - route = Routes.AuthCheck( - showLogoOnPin = showLogoOnPin, - requirePin = requirePin, - requireBiometrics = requireBiometrics, - onSuccessActionId = onSuccessActionId, - ), - navOptions = navOptions, -) - -fun NavController.navigateToDefaultUnitSettings() = navigate( - route = Routes.DefaultUnitSettings, -) - -fun NavController.navigateToLocalCurrencySettings() = navigate( - route = Routes.LocalCurrencySettings, -) - -fun NavController.navigateToBackupSettings() = navigate( - route = Routes.BackupSettings, -) - -fun NavController.navigateToOrderDetail(id: String) = navigate( - route = Routes.OrderDetail(id), -) - -fun NavController.navigateToCjitDetail(id: String) = navigate( - route = Routes.CjitDetail(id), -) - -fun NavController.navigateToDevSettings() = navigate( - route = Routes.DevSettings, -) - -fun NavController.navigateToTransferSavingsIntro() = navigate( - route = Routes.SavingsIntro, -) - -fun NavController.navigateToTransferSavingsAvailability() = navigate( - route = Routes.SavingsAvailability, -) - -fun NavController.navigateToTransferSpendingIntro() = navigate( - route = Routes.SpendingIntro, -) - -fun NavController.navigateToTransferSpendingAmount() = navigate( - route = Routes.SpendingAmount, -) - -fun NavController.navigateToTransferIntro() = navigate( - route = Routes.TransferIntro, -) - -fun NavController.navigateToTransferFunding() = navigate( - route = Routes.Funding, -) - -fun NavController.navigateToActivityItem(id: String) = navigate( - route = Routes.ActivityDetail(id), -) - -fun NavController.navigateToActivityExplore(id: String) = navigate( - route = Routes.ActivityExplore(id), -) - -fun NavController.navigateToScanner(isCalledForResult: Boolean = false) { - if (isCalledForResult) { - currentBackStackEntry?.savedStateHandle?.set(SCAN_REQUEST_KEY, true) - } - navigate(Routes.QrScanner) -} - -fun NavController.navigateToLogDetail(fileName: String) = navigate( - route = Routes.LogDetail(fileName), -) - -fun NavController.navigateToTransactionSpeedSettings() = navigate( - route = Routes.TransactionSpeedSettings, -) - -fun NavController.navigateToCustomFeeSettings() = navigate( - route = Routes.CustomFeeSettings, -) - -fun NavController.navigateToWidgetsSettings() = navigate( - route = Routes.WidgetsSettings, -) - -fun NavController.navigateToQuickPaySettings(hasSeenIntro: Boolean = true) = navigate( - route = if (hasSeenIntro) Routes.QuickPaySettings else Routes.QuickPayIntro, -) - -fun NavController.navigateToTagsSettings() = navigate( - route = Routes.TagsSettings, -) - -fun NavController.navigateToLanguageSettings() = navigate( - route = Routes.LanguageSettings, -) - -fun NavController.navigateToAdvancedSettings() = navigate( - route = Routes.AdvancedSettings, -) - -fun NavController.navigateToAboutSettings() = navigate( - route = Routes.AboutSettings, -) -// endregion - -@Stable -sealed interface Routes { - @Serializable - data object Home : Routes - - @Serializable - data object Savings : Routes - - @Serializable - data object Spending : Routes - - @Serializable - data object Settings : Routes - - @Serializable - data object NodeInfo : Routes - - @Serializable - data object GeneralSettings : Routes - - @Serializable - data object TransactionSpeedSettings : Routes - - @Serializable - data object WidgetsSettings : Routes - - @Serializable - data object TagsSettings : Routes - - @Serializable - data object AdvancedSettings : Routes - - @Serializable - data object CoinSelectPreference : Routes - - @Serializable - data object ElectrumConfig : Routes - - @Serializable - data object RgsServer : Routes - - @Serializable - data object AddressViewer : Routes - - @Serializable - data object AboutSettings : Routes - - @Serializable - data object CustomFeeSettings : Routes - - @Serializable - data object SecuritySettings : Routes - - @Serializable - data object DisablePin : Routes - - @Serializable - data object ChangePin : Routes - - @Serializable - data object ChangePinNew : Routes - - @Serializable - data class ChangePinConfirm(val newPin: String) : Routes - - @Serializable - data object ChangePinResult : Routes - - @Serializable - data class AuthCheck( - val showLogoOnPin: Boolean = false, - val requirePin: Boolean = false, - val requireBiometrics: Boolean = false, - val onSuccessActionId: String, - ) : Routes - - @Serializable - data object DefaultUnitSettings : Routes - - @Serializable - data object LocalCurrencySettings : Routes - - @Serializable - data object BackupSettings : Routes - - @Serializable - data object ResetAndRestoreSettings : Routes - - @Serializable - data object ChannelOrdersSettings : Routes - - @Serializable - data object Logs : Routes - - @Serializable - data class LogDetail(val fileName: String) : Routes - - @Serializable - data class OrderDetail(val id: String) : Routes - - @Serializable - data class CjitDetail(val id: String) : Routes - - @Serializable - data object ConnectionsNav : Routes - - @Serializable - data object LightningConnections : Routes - - @Serializable - data object ChannelDetail : Routes - - @Serializable - data object CloseConnection : Routes - - @Serializable - data object DevSettings : Routes - - @Serializable - data object LdkDebug : Routes - - @Serializable - data object FeeSettings : Routes - - @Serializable - data object RegtestSettings : Routes - - @Serializable - data object TransferRoot : Routes - - @Serializable - data object TransferIntro : Routes - - @Serializable - data object SpendingIntro : Routes - - @Serializable - data object SpendingAmount : Routes - - @Serializable - data object SpendingConfirm : Routes - - @Serializable - data object SpendingAdvanced : Routes - - @Serializable - data object TransferLiquidity : Routes - - @Serializable - data object SettingUp : Routes - - @Serializable - data object SavingsIntro : Routes - - @Serializable - data object SavingsAvailability : Routes - - @Serializable - data object SavingsConfirm : Routes - - @Serializable - data object SavingsAdvanced : Routes - - @Serializable - data object SavingsProgress : Routes - - @Serializable - data object Funding : Routes - - @Serializable - data object FundingAdvanced : Routes - - @Serializable - data object ExternalNav : Routes - - @Serializable - data class ExternalConnection(val scannedNodeUri: String? = null) : Routes - - @Serializable - data object ExternalAmount : Routes - - @Serializable - data object ExternalConfirm : Routes - - @Serializable - data object ExternalSuccess : Routes - - @Serializable - data object ExternalFeeCustom : Routes - - @Serializable - data class LnurlChannel(val uri: String, val callback: String, val k1: String) : Routes - - @Serializable - data class ActivityDetail(val id: String) : Routes - - @Serializable - data class ActivityExplore(val id: String) : Routes - - @Serializable - data object QrScanner : Routes - - @Serializable - data object BuyIntro : Routes - - @Serializable - data object Support : Routes - - @Serializable - data object ReportIssue : Routes - - @Serializable - data object ReportIssueSuccess : Routes - - @Serializable - data object ReportIssueFailure : Routes - - @Serializable - data object QuickPayIntro : Routes - - @Serializable - data object QuickPaySettings : Routes - - @Serializable - data object LanguageSettings : Routes - - @Serializable - data object ProfileIntro : Routes - - @Serializable - data object CreateProfile : Routes - - @Serializable - data object ShopIntro : Routes - - @Serializable - data object ShopDiscover : Routes - - @Serializable - data class ShopWebView(val page: String, val title: String) : Routes - - @Serializable - data object WidgetsIntro : Routes - - @Serializable - data object AddWidget : Routes - - @Serializable - data object Headlines : Routes - - @Serializable - data object HeadlinesPreview : Routes - - @Serializable - data object HeadlinesEdit : Routes - - @Serializable - data object Facts : Routes - - @Serializable - data object FactsPreview : Routes - - @Serializable - data object FactsEdit : Routes - - @Serializable - data object Blocks : Routes - - @Serializable - data object BlocksPreview : Routes - - @Serializable - data object BlocksEdit : Routes - - @Serializable - data object Weather : Routes - - @Serializable - data object WeatherPreview : Routes - - @Serializable - data object WeatherEdit : Routes - - @Serializable - data object Price : Routes - - @Serializable - data object PricePreview : Routes - - @Serializable - data object PriceEdit : Routes - - @Serializable - data object CalculatorPreview : Routes - - @Serializable - data object AppStatus : Routes - - @Serializable - data object CriticalUpdate : Routes - - @Serializable - data object RecoveryMode : Routes - - @Serializable - data object RecoveryMnemonic : Routes - - @Serializable - data object BackgroundPaymentsIntro : Routes - - @Serializable - data object BackgroundPaymentsSettings : Routes - - @Serializable - data object AllActivity : Routes -} diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 0ae430519..545057edd 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -11,6 +11,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.semantics @@ -18,18 +19,15 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.fragment.app.FragmentActivity import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable import to.bitkit.androidServices.LightningNodeService import to.bitkit.androidServices.LightningNodeService.Companion.CHANNEL_ID_NODE import to.bitkit.models.NewTransactionSheetDetails @@ -37,17 +35,14 @@ import to.bitkit.ui.components.AuthCheckView import to.bitkit.ui.components.InactivityTracker import to.bitkit.ui.components.IsOnlineTracker import to.bitkit.ui.components.ToastOverlay -import to.bitkit.ui.onboarding.CreateWalletWithPassphraseScreen -import to.bitkit.ui.onboarding.IntroScreen -import to.bitkit.ui.onboarding.OnboardingSlidesScreen -import to.bitkit.ui.onboarding.RestoreWalletScreen -import to.bitkit.ui.onboarding.TermsOfUseScreen -import to.bitkit.ui.onboarding.WarningMultipleDevicesScreen +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.Transitions +import to.bitkit.ui.nav.entries.onboardingEntries import to.bitkit.ui.screens.SplashScreen import to.bitkit.ui.sheets.ForgotPinSheet import to.bitkit.ui.sheets.NewTransactionSheet import to.bitkit.ui.theme.AppThemeSurface -import to.bitkit.ui.utils.composableWithDefaultTransitions import to.bitkit.ui.utils.enableAppEdgeToEdge import to.bitkit.utils.Logger import to.bitkit.viewmodels.ActivityListViewModel @@ -111,8 +106,7 @@ class MainActivity : FragmentActivity() { } if (!walletViewModel.walletExists && !isRecoveryMode) { - OnboardingNav( - startupNavController = rememberNavController(), + OnboardingContent( scope = scope, appViewModel = appViewModel, walletViewModel = walletViewModel, @@ -218,119 +212,43 @@ class MainActivity : FragmentActivity() { } @Composable -private fun OnboardingNav( - startupNavController: NavHostController, +private fun OnboardingContent( scope: CoroutineScope, appViewModel: AppViewModel, walletViewModel: WalletViewModel, ) { - NavHost( - navController = startupNavController, - startDestination = StartupRoutes.Terms, - ) { - composable { - TermsOfUseScreen( - onNavigateToIntro = { - startupNavController.navigate(StartupRoutes.Intro) - } - ) - } - composableWithDefaultTransitions { - IntroScreen( - onStartClick = { - startupNavController.navigate(StartupRoutes.Slides()) - }, - onSkipClick = { - startupNavController.navigate(StartupRoutes.Slides(StartupRoutes.LAST_SLIDE_INDEX)) - }, - ) - } - composableWithDefaultTransitions { navBackEntry -> - val route = navBackEntry.toRoute() - val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() - OnboardingSlidesScreen( - currentTab = route.tab, + val backStack = rememberNavBackStack(Routes.Terms) + val navigator = remember(backStack) { Navigator(backStack) } + val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() + + NavDisplay( + backStack = backStack, + onBack = { navigator.goBack() }, + transitionSpec = Transitions.screenDefault, + popTransitionSpec = Transitions.screenDefaultPop, + predictivePopTransitionSpec = Transitions.screenDefaultPredictivePop, + entryProvider = entryProvider { + onboardingEntries( + navigator = navigator, isGeoBlocked = isGeoBlocked, - onAdvancedSetupClick = { startupNavController.navigate(StartupRoutes.Advanced) }, - onCreateClick = { + onCreateWallet = { passphrase -> scope.launch { runCatching { appViewModel.resetIsAuthenticatedState() walletViewModel.setInitNodeLifecycleState() - walletViewModel.createWallet(bip39Passphrase = null) - }.onFailure { - appViewModel.toast(it) - } + walletViewModel.createWallet(bip39Passphrase = passphrase) + }.onFailure { appViewModel.toast(it) } } }, - onRestoreClick = { - startupNavController.navigate( - StartupRoutes.WarningMultipleDevices - ) - }, - ) - } - composableWithDefaultTransitions { - WarningMultipleDevicesScreen( - onBackClick = { - startupNavController.popBackStack() - }, - onConfirmClick = { - startupNavController.navigate(StartupRoutes.Restore) - } - ) - } - composableWithDefaultTransitions { - RestoreWalletScreen( - onBackClick = { startupNavController.popBackStack() }, - onRestoreClick = { mnemonic, passphrase -> + onRestoreWallet = { mnemonic, passphrase -> scope.launch { runCatching { appViewModel.resetIsAuthenticatedState() walletViewModel.restoreWallet(mnemonic, passphrase) - }.onFailure { - appViewModel.toast(it) - } - } - } - ) - } - composableWithDefaultTransitions { - CreateWalletWithPassphraseScreen( - onBackClick = { startupNavController.popBackStack() }, - onCreateClick = { passphrase -> - scope.launch { - runCatching { - appViewModel.resetIsAuthenticatedState() - walletViewModel.createWallet(bip39Passphrase = passphrase) - }.onFailure { - appViewModel.toast(it) - } + }.onFailure { appViewModel.toast(it) } } }, ) } - } -} - -private object StartupRoutes { - const val LAST_SLIDE_INDEX = 4 - - @Serializable - data object Terms - - @Serializable - data object Intro - - @Serializable - data class Slides(val tab: Int = 0) - - @Serializable - data object Restore - - @Serializable - data object Advanced - - @Serializable - data object WarningMultipleDevices + ) } diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 320165885..d09be1d39 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import org.lightningdevkit.ldknode.BalanceDetails import org.lightningdevkit.ldknode.BalanceSource import org.lightningdevkit.ldknode.BestBlock @@ -58,6 +57,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.rememberMoneyText import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsTextButtonRow +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -72,7 +72,7 @@ import kotlin.time.ExperimentalTime @Composable fun NodeInfoScreen( - navController: NavController, + navigator: Navigator, ) { val wallet = walletViewModel ?: return val app = appViewModel ?: return @@ -87,7 +87,7 @@ fun NodeInfoScreen( uiState = uiState, isDevModeEnabled = isDevModeEnabled, balanceDetails = lightningState.balances, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onRefresh = { wallet.onPullToRefresh() }, onDisconnectPeer = { wallet.disconnectPeer(it) }, onCopy = { text -> 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 699c15242..593f26e1c 100644 --- a/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt +++ b/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt @@ -3,67 +3,62 @@ package to.bitkit.ui.components 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.settingsViewModel +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.viewmodels.AppViewModel +import to.bitkit.viewmodels.SettingsViewModel @Composable fun AuthCheckScreen( - navController: NavController, + navigator: Navigator, route: Routes.AuthCheck, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, ) { - val app = appViewModel ?: return - val settings = settingsViewModel ?: return - - val isPinOnLaunchEnabled by settings.isPinOnLaunchEnabled.collectAsStateWithLifecycle() - val isBiometricEnabled by settings.isBiometricEnabled.collectAsStateWithLifecycle() - val isPinOnIdleEnabled by settings.isPinOnIdleEnabled.collectAsStateWithLifecycle() - val isPinForPaymentsEnabled by settings.isPinForPaymentsEnabled.collectAsStateWithLifecycle() + val isPinOnLaunchEnabled by settingsViewModel.isPinOnLaunchEnabled.collectAsStateWithLifecycle() + val isBiometricEnabled by settingsViewModel.isBiometricEnabled.collectAsStateWithLifecycle() + val isPinOnIdleEnabled by settingsViewModel.isPinOnIdleEnabled.collectAsStateWithLifecycle() + val isPinForPaymentsEnabled by settingsViewModel.isPinForPaymentsEnabled.collectAsStateWithLifecycle() AuthCheckView( showLogoOnPin = route.showLogoOnPin, - appViewModel = app, - settingsViewModel = settings, + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, requireBiometrics = route.requireBiometrics, requirePin = route.requirePin, onSuccess = { when (route.onSuccessActionId) { AuthCheckAction.TOGGLE_BIOMETRICS -> { - settings.setIsBiometricEnabled(!isBiometricEnabled) - navController.popBackStack() + settingsViewModel.setIsBiometricEnabled(!isBiometricEnabled) + navigator.goBack() } AuthCheckAction.TOGGLE_PIN_ON_LAUNCH -> { - settings.setIsPinOnLaunchEnabled(!isPinOnLaunchEnabled) - navController.popBackStack() + settingsViewModel.setIsPinOnLaunchEnabled(!isPinOnLaunchEnabled) + navigator.goBack() } AuthCheckAction.TOGGLE_PIN_ON_IDLE -> { - settings.setIsPinOnIdleEnabled(!isPinOnIdleEnabled) - navController.popBackStack() + settingsViewModel.setIsPinOnIdleEnabled(!isPinOnIdleEnabled) + navigator.goBack() } AuthCheckAction.TOGGLE_PIN_FOR_PAYMENTS -> { - settings.setIsPinForPaymentsEnabled(!isPinForPaymentsEnabled) - navController.popBackStack() + settingsViewModel.setIsPinForPaymentsEnabled(!isPinForPaymentsEnabled) + navigator.goBack() } AuthCheckAction.DISABLE_PIN -> { - app.removePin() - navController.popBackStack() + appViewModel.removePin() + navigator.goBack() } AuthCheckAction.NAV_TO_RESET -> { - navController.navigate( - route = Routes.ResetAndRestoreSettings, - navOptions = navOptions { popUpTo(Routes.BackupSettings) } - ) + navigator.navigate(Routes.ResetAndRestoreSettings) } } }, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, ) } 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 0071996ac..d35c109ca 100644 --- a/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt +++ b/app/src/main/java/to/bitkit/ui/components/DrawerMenu.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.DrawerValue import androidx.compose.material3.HorizontalDivider 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 @@ -38,14 +37,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -import androidx.navigation.NavController -import androidx.navigation.NavDestination.Companion.hasRoute -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.navigateToHome +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.blockPointerInputPassthrough import to.bitkit.ui.theme.AppThemeSurface @@ -61,7 +56,7 @@ private val drawerWidth = 200.dp @Composable fun DrawerMenu( drawerState: DrawerState, - rootNavController: NavController, + navigator: Navigator, hasSeenWidgetsIntro: Boolean, hasSeenShopIntro: Boolean, modifier: Modifier = Modifier, @@ -95,36 +90,57 @@ fun DrawerMenu( .blockPointerInputPassthrough() ) ) { - Menu( - rootNavController = rootNavController, - drawerState = drawerState, - onClickAddWidget = { + MenuContent( + onWalletClick = { + if (!navigator.isAtHome()) navigator.navigateToHome() + scope.launch { drawerState.close() } + }, + onActivityClick = { + navigator.navigate(Routes.AllActivity) + scope.launch { drawerState.close() } + }, + onContactsClick = null, // TODO IMPLEMENT CONTACTS + onProfileClick = null, // TODO IMPLEMENT PROFILE + onWidgetsClick = { if (!hasSeenWidgetsIntro) { - rootNavController.navigateIfNotCurrent(Routes.WidgetsIntro) + navigator.navigate(Routes.WidgetsIntro) } else { - rootNavController.navigateIfNotCurrent(Routes.AddWidget) + navigator.navigate(Routes.AddWidget) } + scope.launch { drawerState.close() } }, - onClickShop = { + onShopClick = { if (!hasSeenShopIntro) { - rootNavController.navigateIfNotCurrent(Routes.ShopIntro) + navigator.navigate(Routes.ShopIntro) } else { - rootNavController.navigateIfNotCurrent(Routes.ShopDiscover) + navigator.navigate(Routes.ShopDiscover) } + scope.launch { drawerState.close() } + }, + onSettingsClick = { + navigator.navigate(Routes.Settings) + scope.launch { drawerState.close() } + }, + onAppStatusClick = { + navigator.navigate(Routes.AppStatus) + scope.launch { drawerState.close() } }, ) } } +@Suppress("LongParameterList") @Composable -private fun Menu( - rootNavController: NavController, - drawerState: DrawerState, - onClickAddWidget: () -> Unit, - onClickShop: () -> Unit, +private fun MenuContent( + onWalletClick: () -> Unit, + onActivityClick: () -> Unit, + onContactsClick: (() -> Unit)?, + onProfileClick: (() -> Unit)?, + onWidgetsClick: () -> Unit, + onShopClick: () -> Unit, + onSettingsClick: () -> Unit, + onAppStatusClick: () -> Unit, ) { - val scope = rememberCoroutineScope() - Column( modifier = Modifier .width(drawerWidth) @@ -137,65 +153,49 @@ private fun Menu( DrawerItem( label = stringResource(R.string.wallet__drawer__wallet), iconRes = R.drawable.ic_coins, - onClick = { - val isInHome = rootNavController.currentBackStackEntry?.destination?.hasRoute() ?: false - if (!isInHome) rootNavController.navigateToHome() - scope.launch { drawerState.close() } - }, + onClick = onWalletClick, modifier = Modifier.testTag("DrawerWallet") ) DrawerItem( label = stringResource(R.string.wallet__drawer__activity), iconRes = R.drawable.ic_heartbeat, - onClick = { - rootNavController.navigateIfNotCurrent(Routes.AllActivity) - scope.launch { drawerState.close() } - }, + onClick = onActivityClick, modifier = Modifier.testTag("DrawerActivity") ) DrawerItem( label = stringResource(R.string.wallet__drawer__contacts), iconRes = R.drawable.ic_users, - onClick = null, // TODO IMPLEMENT CONTACTS + onClick = onContactsClick, modifier = Modifier.testTag("DrawerContacts") ) DrawerItem( label = stringResource(R.string.wallet__drawer__profile), iconRes = R.drawable.ic_user_square, - onClick = null, // TODO IMPLEMENT PROFILE + onClick = onProfileClick, modifier = Modifier.testTag("DrawerProfile") ) DrawerItem( label = stringResource(R.string.wallet__drawer__widgets), iconRes = R.drawable.ic_stack, - onClick = { - onClickAddWidget() - scope.launch { drawerState.close() } - }, + onClick = onWidgetsClick, modifier = Modifier.testTag("DrawerWidgets") ) DrawerItem( label = stringResource(R.string.wallet__drawer__shop), iconRes = R.drawable.ic_store_front, - onClick = { - onClickShop() - scope.launch { drawerState.close() } - }, + onClick = onShopClick, modifier = Modifier.testTag("DrawerShop") ) DrawerItem( label = stringResource(R.string.wallet__drawer__settings), iconRes = R.drawable.ic_settings, - onClick = { - rootNavController.navigateIfNotCurrent(Routes.Settings) - scope.launch { drawerState.close() } - }, + onClick = onSettingsClick, modifier = Modifier.testTag("DrawerSettings") ) @@ -205,10 +205,7 @@ private fun Menu( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() - .clickableAlpha { - rootNavController.navigateIfNotCurrent(Routes.AppStatus) - scope.launch { drawerState.close() } - } + .clickableAlpha(onClick = onAppStatusClick) ) { AppStatus( showText = true, @@ -297,14 +294,16 @@ private fun DrawerItem( @Composable private fun Preview() { AppThemeSurface { - val navController = rememberNavController() Box { - DrawerMenu( - rootNavController = navController, - drawerState = rememberDrawerState(initialValue = DrawerValue.Open), - hasSeenWidgetsIntro = false, - hasSeenShopIntro = false, - modifier = Modifier.align(Alignment.TopEnd), + MenuContent( + onWalletClick = {}, + onActivityClick = {}, + onContactsClick = null, + onProfileClick = null, + onWidgetsClick = {}, + onShopClick = {}, + onSettingsClick = {}, + onAppStatusClick = {}, ) } } 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 f7700fc5e..d75b106cd 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -1,54 +1,7 @@ package to.bitkit.ui.components -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 -import androidx.compose.material3.BottomSheetScaffold -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberBottomSheetScaffoldState -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope -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.sheets.BackupRoute -import to.bitkit.ui.sheets.PinRoute -import to.bitkit.ui.sheets.SendRoute -import to.bitkit.ui.theme.AppShapes -import to.bitkit.ui.theme.Colors - enum class SheetSize { LARGE, MEDIUM, SMALL, CALENDAR; } -private val sheetContainerColor = Color(0xFF141414) // Equivalent to White08 on a Black background - -@Stable -sealed interface Sheet { - data class Send(val route: SendRoute = SendRoute.Recipient) : Sheet - data object Receive : Sheet - data class Pin(val route: PinRoute = PinRoute.Prompt()) : Sheet - data class Backup(val route: BackupRoute = BackupRoute.ShowMnemonic) : Sheet - data object ActivityDateRangeSelector : Sheet - data object ActivityTagSelector : Sheet - data class LnurlAuth(val domain: String, val lnurl: String, val k1: String) : Sheet - data object ForceTransfer : Sheet - data class Gift(val code: String, val amount: ULong) : Sheet - - data class TimedSheet(val type: TimedSheetType) : Sheet -} - /**@param priority Priority levels for timed sheets (higher number = higher priority)*/ enum class TimedSheetType(val priority: Int) { APP_UPDATE(priority = 5), @@ -57,89 +10,3 @@ enum class TimedSheetType(val priority: Int) { QUICK_PAY(priority = 2), HIGH_BALANCE(priority = 1) } - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SheetHost( - shouldExpand: Boolean, - onDismiss: () -> Unit = {}, - sheets: @Composable ColumnScope.() -> Unit, - content: @Composable () -> Unit, -) { - val scope = rememberCoroutineScope() - val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - ) - - // Automatically expand or hide the bottom sheet based on bool flag - LaunchedEffect(shouldExpand) { - if (shouldExpand) { - scaffoldState.bottomSheetState.expand() - } else { - scaffoldState.bottomSheetState.hide() - } - } - - // Observe the state of the bottom sheet to invoke onDismiss callback - // TODO prevent onDismiss call during first render - LaunchedEffect(scaffoldState.bottomSheetState.isVisible) { - if (!scaffoldState.bottomSheetState.isVisible) { - onDismiss() - } - } - - Box(modifier = Modifier.fillMaxSize()) { - BottomSheetScaffold( - scaffoldState = scaffoldState, - sheetPeekHeight = 0.dp, - sheetShape = AppShapes.sheet, - sheetContent = sheets, - sheetDragHandle = { SheetDragHandle() }, - sheetContainerColor = sheetContainerColor, - sheetContentColor = MaterialTheme.colorScheme.onSurface, - ) { - content() - - // Dismiss on back - BackHandler(enabled = scaffoldState.bottomSheetState.isVisible) { - scope.launch { - scaffoldState.bottomSheetState.hide() - onDismiss() - } - } - - Scrim(scaffoldState.bottomSheetState) { - scope.launch { - scaffoldState.bottomSheetState.hide() - onDismiss() - } - } - } - } -} - -@Composable -@OptIn(ExperimentalMaterial3Api::class) -private fun Scrim( - bottomSheetState: SheetState, - onClick: () -> Unit, -) { - val isBottomSheetVisible = bottomSheetState.targetValue != SheetValue.Hidden - val scrimAlpha by animateFloatAsState( - targetValue = if (isBottomSheetVisible) 0.5f else 0f, - animationSpec = tween(durationMillis = 300), - label = "sheetScrimAlpha" - ) - if (scrimAlpha > 0f || isBottomSheetVisible) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Colors.Black.copy(alpha = scrimAlpha)) - .clickable( - interactionSource = null, - indication = null, - onClick = onClick, - ) - ) - } -} diff --git a/app/src/main/java/to/bitkit/ui/nav/DeepLinks.kt b/app/src/main/java/to/bitkit/ui/nav/DeepLinks.kt new file mode 100644 index 000000000..2bfa3f1a3 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/DeepLinks.kt @@ -0,0 +1,181 @@ +package to.bitkit.ui.nav + +import android.net.Uri +import androidx.core.net.toUri +import kotlin.reflect.KClass + +/** + * Supported URI schemes for deep linking. + */ +enum class UriScheme(val value: String) { + BITCOIN("bitcoin"), + LIGHTNING("lightning"), + LNURL("lnurl"), + LNURL_PAY("lnurlp"), + LNURL_WITHDRAW("lnurlw"), + LNURL_CHANNEL("lnurlc"), + BITKIT("bitkit"), + HTTPS("https"); + + val withColon: String get() = "$value:" + val withSlashes: String get() = "$value://" +} + +/** + * Defines a pattern for matching deep link URIs to Routes. + */ +data class DeepLinkPattern( + val routeClass: KClass, + val uriPattern: Uri, +) + +/** + * Request object for parsing incoming deep link URIs. + */ +data class DeepLinkRequest( + val uri: Uri, +) { + val scheme: String? = uri.scheme?.lowercase() + val host: String? = uri.host?.lowercase() + val pathSegments: List = uri.pathSegments + val queryParams: Map = uri.queryParameterNames + .associateWith { uri.getQueryParameter(it) ?: "" } +} + +/** + * Result of a successful pattern match. + */ +data class DeepLinkMatchResult( + val routeClass: KClass, + val args: Map, +) + +/** + * Matches a DeepLinkRequest against a DeepLinkPattern. + */ +class DeepLinkMatcher( + private val request: DeepLinkRequest, + private val pattern: DeepLinkPattern, +) { + fun match(): DeepLinkMatchResult? { + val patternUri = pattern.uriPattern + + // Check scheme matches (case-insensitive) + val patternScheme = patternUri.scheme?.lowercase() + if (request.scheme != patternScheme) return null + + // Check host matches (if specified in pattern) + val patternHost = patternUri.host?.lowercase() + if (patternHost != null && request.host != patternHost) return null + + // Extract path arguments + val args = mutableMapOf() + val patternSegments = patternUri.pathSegments + val requestSegments = request.pathSegments + + // For schemes without host (like bitcoin:address), the "host" becomes path + val effectiveRequestSegments = if (request.host != null && patternHost == null) { + listOf(request.host) + requestSegments + } else { + requestSegments + } + + if (patternSegments.size != effectiveRequestSegments.size) return null + + patternSegments.forEachIndexed { index, segment -> + if (segment.startsWith("{") && segment.endsWith("}")) { + val argName = segment.removeSurrounding("{", "}") + args[argName] = effectiveRequestSegments[index] + } else if (segment.lowercase() != effectiveRequestSegments[index].lowercase()) { + return null + } + } + + // Add query params to args + args.putAll(request.queryParams) + + return DeepLinkMatchResult(pattern.routeClass, args) + } +} + +/** + * Registry of all supported deep link patterns. + * + * Note: The Rust-based bitkitcore.decode() remains the primary parser for complex + * Bitcoin/Lightning URIs. These patterns provide type-safe documentation and + * early pattern matching for known routes. + */ +object DeepLinkPatterns { + // bitkit:// scheme patterns + val RECOVERY_MODE = DeepLinkPattern( + routeClass = Routes.RecoveryMode::class, + uriPattern = "${UriScheme.BITKIT.withSlashes}recovery-mode".toUri() + ) + + // bitcoin:// scheme patterns (BIP21) + // Note: bitcoin: URIs use address as "host" not path + val SEND_BITCOIN = DeepLinkPattern( + routeClass = Routes.SendAddress::class, + uriPattern = "${UriScheme.BITCOIN.withSlashes}{address}".toUri() + ) + + // lightning:// scheme patterns + val SEND_LIGHTNING = DeepLinkPattern( + routeClass = Routes.SendAddress::class, + uriPattern = "${UriScheme.LIGHTNING.withSlashes}{invoice}".toUri() + ) + + // lnurl:// scheme patterns + val LNURL_PAY = DeepLinkPattern( + routeClass = Routes.SendAddress::class, + uriPattern = "${UriScheme.LNURL_PAY.withSlashes}{data}".toUri() + ) + + val LNURL_WITHDRAW = DeepLinkPattern( + routeClass = Routes.ReceiveQr::class, + uriPattern = "${UriScheme.LNURL_WITHDRAW.withSlashes}{data}".toUri() + ) + + val LNURL_CHANNEL = DeepLinkPattern( + routeClass = Routes.LnurlChannel::class, + uriPattern = "${UriScheme.LNURL_CHANNEL.withSlashes}{data}".toUri() + ) + + // https:// scheme patterns (App Links) + val TREASURE_HUNT = DeepLinkPattern( + routeClass = Routes.GiftLoading::class, + uriPattern = "${UriScheme.HTTPS.withSlashes}www.bitkit.to/treasure-hunt".toUri() + ) + + /** + * All registered patterns for matching. + */ + val all: List> = listOf( + RECOVERY_MODE, + SEND_BITCOIN, + SEND_LIGHTNING, + LNURL_PAY, + LNURL_WITHDRAW, + LNURL_CHANNEL, + TREASURE_HUNT, + ) + + /** + * Find the first matching pattern for the given URI. + */ + fun findMatch(uri: Uri): DeepLinkMatchResult? { + val request = DeepLinkRequest(uri) + return all.firstNotNullOfOrNull { pattern -> + @Suppress("UNCHECKED_CAST") + DeepLinkMatcher(request, pattern as DeepLinkPattern).match() + } + } +} + +// TODO Temporary fix while these schemes can't be decoded +fun String.removeLightningSchemes(): String = listOf( + UriScheme.LNURL, + UriScheme.LNURL_PAY, + UriScheme.LNURL_WITHDRAW, + UriScheme.LNURL_CHANNEL, +).fold(this) { acc, scheme -> acc.replace(scheme.withColon, "") } diff --git a/app/src/main/java/to/bitkit/ui/nav/Navigator.kt b/app/src/main/java/to/bitkit/ui/nav/Navigator.kt new file mode 100644 index 000000000..ea8534483 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/Navigator.kt @@ -0,0 +1,59 @@ +package to.bitkit.ui.nav + +import androidx.compose.animation.core.AnimationConstants +import androidx.compose.runtime.Stable +import androidx.navigation3.runtime.NavBackStack +import androidx.navigation3.runtime.NavKey + +@Stable +class Navigator(@PublishedApi internal val backStack: NavBackStack) { + fun navigate(route: Routes) = run { backStack.add(route) } + + fun goBack(): Boolean = backStack.removeLastOrNull() != null + + fun popBackTo(route: Routes, inclusive: Boolean = false): Boolean { + val index = backStack.indexOfFirst { it == route } + if (index == -1) return false + + val removeCount = if (inclusive) { + backStack.size - index + } else { + backStack.size - index - 1 + } + + repeat(removeCount) { + backStack.removeLastOrNull() + } + return true + } + + fun navigateToHome() { + val homeIndex = backStack.indexOfFirst { it is Routes.Home } + if (homeIndex != -1) { + while (backStack.size > homeIndex + 1) { + backStack.removeLastOrNull() + } + } else { + while (backStack.size > 1) { + backStack.removeLastOrNull() + } + if (backStack.lastOrNull() !is Routes.Home) { + backStack.add(Routes.Home) + } + } + } + + fun isAtHome(): Boolean = backStack.lastOrNull() is Routes.Home + + fun shouldShowTabBar(): Boolean = when (backStack.lastOrNull()) { + is Routes.Home, is Routes.Savings, is Routes.Spending, is Routes.AllActivity -> true + else -> false + } + + fun navigateToQuickPaySettings(hasSeenIntro: Boolean = true) = navigate( + if (hasSeenIntro) Routes.QuickPaySettings else Routes.QuickPayIntro + ) +} + +const val MS_NAV_DELAY = 100L +const val MS_TRANSITION_SCREEN = AnimationConstants.DefaultDurationMillis.toLong() // 300ms diff --git a/app/src/main/java/to/bitkit/ui/nav/Routes.kt b/app/src/main/java/to/bitkit/ui/nav/Routes.kt new file mode 100644 index 000000000..4cf192421 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/Routes.kt @@ -0,0 +1,527 @@ +package to.bitkit.ui.nav + +import androidx.compose.runtime.Stable +import androidx.navigation3.runtime.NavKey +import com.synonym.bitkitcore.Activity +import kotlinx.serialization.Serializable + +@Stable +sealed interface Routes : NavKey { + // Core screens + @Serializable + data object Home : Routes + + @Serializable + data object Savings : Routes + + @Serializable + data object Spending : Routes + + @Serializable + data object AllActivity : Routes + + // Onboarding + @Serializable + data object Terms : Routes + + @Serializable + data object Intro : Routes + + @Serializable + data class Slides(val tab: Int = 0) : Routes + + @Serializable + data object Restore : Routes + + @Serializable + data object Advanced : Routes + + @Serializable + data object WarningMultipleDevices : Routes + + // Settings + @Serializable + data object Settings : Routes + + @Serializable + data object NodeInfo : Routes + + @Serializable + data object GeneralSettings : Routes + + @Serializable + data object TransactionSpeedSettings : Routes + + @Serializable + data object WidgetsSettings : Routes + + @Serializable + data object TagsSettings : Routes + + @Serializable + data object AdvancedSettings : Routes + + @Serializable + data object CoinSelectPreference : Routes + + @Serializable + data object ElectrumConfig : Routes + + @Serializable + data object RgsServer : Routes + + @Serializable + data object AddressViewer : Routes + + @Serializable + data object AboutSettings : Routes + + @Serializable + data object CustomFeeSettings : Routes + + @Serializable + data object SecuritySettings : Routes + + @Serializable + data object DisablePin : Routes + + @Serializable + data object ChangePin : Routes + + @Serializable + data object ChangePinNew : Routes + + @Serializable + data class ChangePinConfirm(val newPin: String) : Routes + + @Serializable + data object ChangePinResult : Routes + + @Serializable + data class AuthCheck( + val showLogoOnPin: Boolean = false, + val requirePin: Boolean = false, + val requireBiometrics: Boolean = false, + val onSuccessActionId: String, + ) : Routes + + @Serializable + data object DefaultUnitSettings : Routes + + @Serializable + data object LocalCurrencySettings : Routes + + @Serializable + data object BackupSettings : Routes + + @Serializable + data object ResetAndRestoreSettings : Routes + + @Serializable + data object ChannelOrdersSettings : Routes + + @Serializable + data object Logs : Routes + + @Serializable + data class LogDetail(val fileName: String) : Routes + + // Activity detail - passes full serializable object + @Serializable + data class ActivityDetail(val activity: Activity) : Routes + + @Serializable + data class ActivityExplore(val id: String) : Routes + + // Orders - passes ID to look up from ViewModel + @Serializable + data class OrderDetail(val orderId: String) : Routes + + @Serializable + data class CjitDetail(val entryId: String) : Routes + + // Lightning connections + @Serializable + data object LightningConnections : Routes + + @Serializable + data object ChannelDetail : Routes + + @Serializable + data object CloseConnection : Routes + + // Dev settings + @Serializable + data object DevSettings : Routes + + @Serializable + data object LdkDebug : Routes + + @Serializable + data object FeeSettings : Routes + + @Serializable + data object RegtestSettings : Routes + + // Transfer flow + @Serializable + data object TransferIntro : Routes + + @Serializable + data object SpendingIntro : Routes + + @Serializable + data object SpendingAmount : Routes + + @Serializable + data object SpendingConfirm : Routes + + @Serializable + data object SpendingAdvanced : Routes + + @Serializable + data object TransferLiquidity : Routes + + @Serializable + data object SettingUp : Routes + + @Serializable + data object SavingsIntro : Routes + + @Serializable + data object SavingsAvailability : Routes + + @Serializable + data object SavingsConfirm : Routes + + @Serializable + data object SavingsAdvanced : Routes + + @Serializable + data object SavingsProgress : Routes + + @Serializable + data object Funding : Routes + + @Serializable + data object FundingAdvanced : Routes + + // External node + @Serializable + data class ExternalConnection(val scannedNodeUri: String? = null) : Routes + + @Serializable + data object ExternalAmount : Routes + + @Serializable + data object ExternalConfirm : Routes + + @Serializable + data object ExternalSuccess : Routes + + @Serializable + data object ExternalFeeCustom : Routes + + @Serializable + data object ExternalNodeScanner : Routes + + @Serializable + data class LnurlChannel(val uri: String, val callback: String, val k1: String) : Routes + + // Scanner + @Serializable + data object QrScanner : Routes + + // Buy + @Serializable + data object BuyIntro : Routes + + // Support + @Serializable + data object Support : Routes + + @Serializable + data object ReportIssue : Routes + + @Serializable + data object ReportIssueSuccess : Routes + + @Serializable + data object ReportIssueFailure : Routes + + // Quick Pay + @Serializable + data object QuickPayIntro : Routes + + @Serializable + data object QuickPaySettings : Routes + + // Language + @Serializable + data object LanguageSettings : Routes + + // Profile + @Serializable + data object ProfileIntro : Routes + + @Serializable + data object CreateProfile : Routes + + // Shop + @Serializable + data object ShopIntro : Routes + + @Serializable + data object ShopDiscover : Routes + + @Serializable + data class ShopWebView(val page: String, val title: String) : Routes + + // Widgets + @Serializable + data object WidgetsIntro : Routes + + @Serializable + data object AddWidget : Routes + + @Serializable + data object Headlines : Routes + + @Serializable + data object HeadlinesPreview : Routes + + @Serializable + data object HeadlinesEdit : Routes + + @Serializable + data object Facts : Routes + + @Serializable + data object FactsPreview : Routes + + @Serializable + data object FactsEdit : Routes + + @Serializable + data object Blocks : Routes + + @Serializable + data object BlocksPreview : Routes + + @Serializable + data object BlocksEdit : Routes + + @Serializable + data object Weather : Routes + + @Serializable + data object WeatherPreview : Routes + + @Serializable + data object WeatherEdit : Routes + + @Serializable + data object Price : Routes + + @Serializable + data object PricePreview : Routes + + @Serializable + data object PriceEdit : Routes + + @Serializable + data object CalculatorPreview : Routes + + // App status + @Serializable + data object AppStatus : Routes + + @Serializable + data object CriticalUpdate : Routes + + // Recovery + @Serializable + data object RecoveryMode : Routes + + @Serializable + data object RecoveryMnemonic : Routes + + // Background payments + @Serializable + data object BackgroundPaymentsIntro : Routes + + @Serializable + data object BackgroundPaymentsSettings : Routes + + // region Send Flow (17 routes) + @Serializable + data object SendRecipient : Routes + + @Serializable + data object SendAddress : Routes + + @Serializable + data class SendAmount(val prefill: String? = null) : Routes + + @Serializable + data object SendQrScanner : Routes + + @Serializable + data object SendCoinSelection : Routes + + @Serializable + data object SendFeeRate : Routes + + @Serializable + data object SendFeeCustom : Routes + + @Serializable + data object SendConfirm : Routes + + @Serializable + data object SendSuccess : Routes + + @Serializable + data class SendError(val message: String) : Routes + + @Serializable + data object SendWithdrawConfirm : Routes + + @Serializable + data object SendWithdrawError : Routes + + @Serializable + data object SendSupport : Routes + + @Serializable + data object SendAddTag : Routes + + @Serializable + data object SendPinCheck : Routes + + @Serializable + data object SendQuickPay : Routes + // endregion + + // region Receive Flow (9 routes) + @Serializable + data object ReceiveQr : Routes + + @Serializable + data object ReceiveAmount : Routes + + @Serializable + data object ReceiveConfirm : Routes + + @Serializable + data object ReceiveConfirmInbound : Routes + + @Serializable + data object ReceiveLiquidity : Routes + + @Serializable + data object ReceiveLiquidityAdditional : Routes + + @Serializable + data object ReceiveEditInvoice : Routes + + @Serializable + data object ReceiveAddTag : Routes + + @Serializable + data object ReceiveGeoBlock : Routes + // endregion + + // region Pin Flow (5 routes) + @Serializable + data class PinPrompt(val showLaterButton: Boolean = false) : Routes + + @Serializable + data object PinChoose : Routes + + @Serializable + data class PinConfirm(val pin: String) : Routes + + @Serializable + data object PinBiometrics : Routes + + @Serializable + data class PinResult(val isBioOn: Boolean) : Routes + // endregion + + // region Backup Flow (9 routes) + @Serializable + data object BackupIntro : Routes + + @Serializable + data object BackupShowMnemonic : Routes + + @Serializable + data object BackupShowPassphrase : Routes + + @Serializable + data object BackupConfirmMnemonic : Routes + + @Serializable + data object BackupConfirmPassphrase : Routes + + @Serializable + data object BackupWarning : Routes + + @Serializable + data object BackupSuccess : Routes + + @Serializable + data object BackupMultipleDevices : Routes + + @Serializable + data object BackupMetadata : Routes + // endregion + + // region Gift Flow (5 routes) + @Serializable + data class GiftLoading(val code: String, val amount: ULong) : Routes + + @Serializable + data object GiftUsed : Routes + + @Serializable + data object GiftUsedUp : Routes + + @Serializable + data object GiftError : Routes + + @Serializable + data object GiftSuccess : Routes + // endregion + + // region Simple Sheets (4 routes) + @Serializable + data object ActivityDateRangeSelectorSheet : Routes + + @Serializable + data object ActivityTagSelectorSheet : Routes + + @Serializable + data class LnurlAuthSheet(val domain: String, val lnurl: String, val k1: String) : Routes + + @Serializable + data object ForceTransferSheet : Routes + + // Timed Sheets + @Serializable + data object TimedUpdateSheet : Routes + + @Serializable + data object TimedBackupSheet : Routes + + @Serializable + data object TimedNotificationsSheet : Routes + + @Serializable + data object TimedQuickPaySheet : Routes + + @Serializable + data object TimedHighBalanceSheet : Routes + // endregion +} diff --git a/app/src/main/java/to/bitkit/ui/nav/SheetSceneStrategy.kt b/app/src/main/java/to/bitkit/ui/nav/SheetSceneStrategy.kt new file mode 100644 index 000000000..e3fffff68 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/SheetSceneStrategy.kt @@ -0,0 +1,149 @@ +package to.bitkit.ui.nav + +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 +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.scene.OverlayScene +import androidx.navigation3.scene.Scene +import androidx.navigation3.scene.SceneStrategy +import androidx.navigation3.scene.SceneStrategyScope +import kotlinx.coroutines.launch +import to.bitkit.ui.components.SheetDragHandle +import to.bitkit.ui.components.SheetSize +import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.theme.AppShapes +import to.bitkit.ui.theme.Colors + +private val sheetContainerColor = Color(0xFF141414) + +@OptIn(ExperimentalMaterial3Api::class) +class SheetSceneStrategy : SceneStrategy { + + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { + val lastEntry = entries.lastOrNull() + val sheetProperties = lastEntry?.metadata?.get(SHEET_KEY) as? SheetProperties + return sheetProperties?.let { props -> + @Suppress("UNCHECKED_CAST") + BitKitSheetScene( + key = lastEntry.contentKey as T, + previousEntries = entries.dropLast(1), + overlaidEntries = entries.dropLast(1), + entry = lastEntry, + sheetSize = props.size, + onBack = onBack, + ) + } + } + + companion object Companion { + fun sheet(size: SheetSize = SheetSize.LARGE): Map = + mapOf(SHEET_KEY to SheetProperties(size)) + + internal const val SHEET_KEY = "bitkit_sheet" + } +} + +data class SheetProperties( + val size: SheetSize = SheetSize.LARGE, +) + +@Composable +fun ColumnScope.SheetEntryContent(content: @Composable ColumnScope.() -> Unit) = run { content() } + +@OptIn(ExperimentalMaterial3Api::class) +internal class BitKitSheetScene( + override val key: T, + override val previousEntries: List>, + override val overlaidEntries: List>, + private val entry: NavEntry, + private val sheetSize: SheetSize, + private val onBack: () -> Unit, +) : OverlayScene { + + override val entries: List> = listOf(entry) + + override val content: @Composable (() -> Unit) = { + val scope = rememberCoroutineScope() + val scaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ) + + LaunchedEffect(Unit) { + scaffoldState.bottomSheetState.expand() + } + + Box(modifier = Modifier.fillMaxSize()) { + BottomSheetScaffold( + scaffoldState = scaffoldState, + sheetPeekHeight = 0.dp, + sheetShape = AppShapes.sheet, + sheetContent = { + Box(modifier = Modifier.sheetHeight(sheetSize)) { + entry.Content() + } + }, + sheetDragHandle = { SheetDragHandle() }, + sheetContainerColor = sheetContainerColor, + sheetContentColor = MaterialTheme.colorScheme.onSurface, + containerColor = Color.Transparent, + ) { + BackHandler(enabled = scaffoldState.bottomSheetState.isVisible) { + scope.launch { + scaffoldState.bottomSheetState.hide() + onBack() + } + } + + Scrim(isVisible = scaffoldState.bottomSheetState.targetValue != SheetValue.Hidden) { + scope.launch { + scaffoldState.bottomSheetState.hide() + onBack() + } + } + } + } + } +} + +@Composable +private fun Scrim( + isVisible: Boolean, + onClick: () -> Unit, +) { + val scrimAlpha by animateFloatAsState( + targetValue = if (isVisible) 0.5f else 0f, + animationSpec = tween(durationMillis = 300), + label = "sheetScrimAlpha" + ) + if (scrimAlpha > 0f || isVisible) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Colors.Black.copy(alpha = scrimAlpha)) + .clickable( + interactionSource = null, + indication = null, + onClick = onClick, + ) + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/nav/Transitions.kt b/app/src/main/java/to/bitkit/ui/nav/Transitions.kt new file mode 100644 index 000000000..aaf293c0f --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/Transitions.kt @@ -0,0 +1,83 @@ +package to.bitkit.ui.nav + +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith +import androidx.navigation3.ui.NavDisplay + +private const val MS_DURATION = 300 + +object Transitions { + + val screenDefault: AnimatedContentTransitionScope<*>.() -> ContentTransform = { + slideInHorizontally( + initialOffsetX = { it }, + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing) + ) togetherWith slideOutHorizontally( + targetOffsetX = { -it / 3 }, + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing) + ) + fadeOut( + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing), + targetAlpha = 0.8f + ) + } + + val screenDefaultPop: AnimatedContentTransitionScope<*>.() -> ContentTransform = { + slideInHorizontally( + initialOffsetX = { -it / 3 }, + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing) + ) + fadeIn( + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing), + initialAlpha = 0.8f + ) togetherWith slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing) + ) + } + + val screenDefaultPredictivePop: AnimatedContentTransitionScope<*>.(Int) -> ContentTransform = { _ -> + slideInHorizontally( + initialOffsetX = { -it / 3 }, + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing) + ) + fadeIn( + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing), + initialAlpha = 0.8f + ) togetherWith slideOutHorizontally( + targetOffsetX = { it }, + animationSpec = tween(MS_DURATION, easing = FastOutSlowInEasing) + ) + } + + private val verticalSlideSpec = NavDisplay.transitionSpec { + slideInVertically( + initialOffsetY = { it }, + animationSpec = tween(MS_DURATION) + ) togetherWith ExitTransition.KeepUntilTransitionsFinished + } + + private val verticalSlidePopSpec = NavDisplay.popTransitionSpec { + EnterTransition.None togetherWith slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(MS_DURATION) + ) + } + + private val verticalSlidePredictivePopSpec = NavDisplay.predictivePopTransitionSpec { + EnterTransition.None togetherWith slideOutVertically( + targetOffsetY = { it }, + animationSpec = tween(MS_DURATION) + ) + } + + val verticalSlideMetadata = verticalSlideSpec + verticalSlidePopSpec + verticalSlidePredictivePopSpec +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/HomeEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/HomeEntries.kt new file mode 100644 index 000000000..268e91cad --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/HomeEntries.kt @@ -0,0 +1,309 @@ +package to.bitkit.ui.nav.entries + +import androidx.compose.material3.DrawerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.ext.rawId +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.Transitions +import to.bitkit.ui.screens.CriticalUpdateScreen +import to.bitkit.ui.screens.profile.CreateProfileScreen +import to.bitkit.ui.screens.profile.ProfileIntroScreen +import to.bitkit.ui.screens.recovery.RecoveryMnemonicScreen +import to.bitkit.ui.screens.recovery.RecoveryModeScreen +import to.bitkit.ui.screens.scanner.QrScanningScreen +import to.bitkit.ui.screens.shop.ShopIntroScreen +import to.bitkit.ui.screens.shop.shopDiscover.ShopDiscoverScreen +import to.bitkit.ui.screens.shop.shopWebView.ShopWebViewScreen +import to.bitkit.ui.screens.wallets.HomeScreen +import to.bitkit.ui.screens.wallets.SavingsWalletScreen +import to.bitkit.ui.screens.wallets.SpendingWalletScreen +import to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen +import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen +import to.bitkit.ui.screens.wallets.activity.AllActivityScreen +import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen +import to.bitkit.ui.utils.RequestNotificationPermissions +import to.bitkit.viewmodels.ActivityListViewModel +import to.bitkit.viewmodels.AppViewModel +import to.bitkit.viewmodels.SettingsViewModel +import to.bitkit.viewmodels.WalletViewModel + +/** + * Home section entry providers for Navigation 3. + */ +@Suppress("LongParameterList", "LongMethod") +fun EntryProviderScope.homeEntries( + navigator: Navigator, + drawerState: DrawerState, + walletViewModel: WalletViewModel, + appViewModel: AppViewModel, + activityListViewModel: ActivityListViewModel, + settingsViewModel: SettingsViewModel, +) { + entry { + HomeEntry( + navigator = navigator, + drawerState = drawerState, + walletViewModel = walletViewModel, + appViewModel = appViewModel, + activityListViewModel = activityListViewModel, + settingsViewModel = settingsViewModel, + ) + } + + entry { + SavingsEntry( + navigator = navigator, + appViewModel = appViewModel, + activityListViewModel = activityListViewModel, + settingsViewModel = settingsViewModel, + ) + } + + entry { + SpendingEntry( + navigator = navigator, + walletViewModel = walletViewModel, + activityListViewModel = activityListViewModel, + settingsViewModel = settingsViewModel, + ) + } + + entry { + AllActivityScreen( + viewModel = activityListViewModel, + onBack = { + activityListViewModel.clearFilters() + navigator.navigateToHome() + }, + onActivityItemClick = { navigator.navigate(Routes.ActivityDetail(it)) }, + onTagClick = { navigator.navigate(Routes.ActivityTagSelectorSheet) }, + onDateRangeClick = { navigator.navigate(Routes.ActivityDateRangeSelectorSheet) }, + onEmptyActivityRowClick = { navigator.navigate(Routes.ReceiveQr) }, + ) + } + + entry { route -> + ActivityDetailScreen( + navigator = navigator, + activityId = route.activity.rawId(), + listViewModel = activityListViewModel, + ) + } + + entry { route -> + ActivityExploreScreen( + navigator = navigator, + activityId = route.id, + ) + } + + entry( + metadata = Transitions.verticalSlideMetadata + ) { + QrScanningScreen( + navigator = navigator, + onScanSuccess = { qrCode -> + appViewModel.onScanResult(qrCode) + }, + ) + } + + // Profile Flow + profileEntries(navigator, settingsViewModel) + + // Shop Flow + shopEntries(navigator, appViewModel, settingsViewModel) + + // Buy Flow + entry { + BuyIntroScreen( + onBackClick = { navigator.goBack() }, + ) + } + + // App Status + entry { + CriticalUpdateScreen() + } + + // Recovery Flow + recoveryEntries(navigator, appViewModel, settingsViewModel) +} + +@Composable +private fun SavingsEntry( + navigator: Navigator, + appViewModel: AppViewModel, + activityListViewModel: ActivityListViewModel, + settingsViewModel: SettingsViewModel, +) { + val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() + val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() + val onchainActivities by activityListViewModel.onchainActivities.collectAsStateWithLifecycle() + + SavingsWalletScreen( + isGeoBlocked = isGeoBlocked, + onchainActivities = onchainActivities.orEmpty(), + onAllActivityButtonClick = { navigator.navigate(Routes.AllActivity) }, + onActivityItemClick = { navigator.navigate(Routes.ActivityDetail(it)) }, + onEmptyActivityRowClick = { navigator.navigate(Routes.ReceiveQr) }, + onTransferToSpendingClick = { + if (!hasSeenSpendingIntro) { + navigator.navigate(Routes.SpendingIntro) + } else { + navigator.navigate(Routes.SpendingAmount) + } + }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun SpendingEntry( + navigator: Navigator, + walletViewModel: WalletViewModel, + activityListViewModel: ActivityListViewModel, + settingsViewModel: SettingsViewModel, +) { + val hasSeenSavingsIntro by settingsViewModel.hasSeenSavingsIntro.collectAsStateWithLifecycle() + val uiState by walletViewModel.uiState.collectAsStateWithLifecycle() + val lightningActivities by activityListViewModel.lightningActivities.collectAsStateWithLifecycle() + + SpendingWalletScreen( + uiState = uiState, + lightningActivities = lightningActivities.orEmpty(), + onAllActivityButtonClick = { navigator.navigate(Routes.AllActivity) }, + onActivityItemClick = { navigator.navigate(Routes.ActivityDetail(it)) }, + onEmptyActivityRowClick = { navigator.navigate(Routes.ReceiveQr) }, + onTransferToSavingsClick = { + if (!hasSeenSavingsIntro) { + navigator.navigate(Routes.SavingsIntro) + } else { + navigator.navigate(Routes.SavingsAvailability) + } + }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun HomeEntry( + navigator: Navigator, + drawerState: DrawerState, + walletViewModel: WalletViewModel, + appViewModel: AppViewModel, + activityListViewModel: ActivityListViewModel, + settingsViewModel: SettingsViewModel, +) { + val mainUiState by walletViewModel.uiState.collectAsStateWithLifecycle() + val isRecoveryMode by walletViewModel.isRecoveryMode.collectAsStateWithLifecycle() + + RequestNotificationPermissions( + showPermissionDialog = !isRecoveryMode, + onPermissionChange = { granted -> + settingsViewModel.setNotificationPreference(granted) + } + ) + + HomeScreen( + mainUiState = mainUiState, + drawerState = drawerState, + navigator = navigator, + settingsViewModel = settingsViewModel, + walletViewModel = walletViewModel, + appViewModel = appViewModel, + activityListViewModel = activityListViewModel, + ) +} + +/** + * Profile flow entries. + */ +private fun EntryProviderScope.profileEntries( + navigator: Navigator, + settingsViewModel: SettingsViewModel, +) { + entry { + ProfileIntroScreen( + onContinue = { + settingsViewModel.setHasSeenProfileIntro(true) + navigator.navigate(Routes.CreateProfile) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + CreateProfileScreen( + onBack = { navigator.goBack() }, + ) + } +} + +/** + * Shop flow entries. + */ +private fun EntryProviderScope.shopEntries( + navigator: Navigator, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, +) { + entry { + ShopIntroScreen( + onContinue = { + settingsViewModel.setHasSeenShopIntro(true) + navigator.navigate(Routes.ShopDiscover) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + ShopDiscoverScreen( + onBack = { navigator.goBack() }, + navigateWebView = { page, title -> + navigator.navigate(Routes.ShopWebView(page, title)) + }, + ) + } + + entry { route -> + ShopWebViewScreen( + page = route.page, + title = route.title, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + onPaymentIntent = { data -> + appViewModel.onScanResult(data) + }, + ) + } +} + +/** + * Recovery flow entries. + */ +private fun EntryProviderScope.recoveryEntries( + navigator: Navigator, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, +) { + entry { + RecoveryModeScreen( + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, + onNavigateToSeed = { navigator.navigate(Routes.RecoveryMnemonic) }, + ) + } + + entry { + RecoveryMnemonicScreen( + onNavigateBack = { navigator.goBack() }, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/OnboardingEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/OnboardingEntries.kt new file mode 100644 index 000000000..267cc9b87 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/OnboardingEntries.kt @@ -0,0 +1,65 @@ +package to.bitkit.ui.nav.entries + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.onboarding.CreateWalletWithPassphraseScreen +import to.bitkit.ui.onboarding.IntroScreen +import to.bitkit.ui.onboarding.OnboardingSlidesScreen +import to.bitkit.ui.onboarding.RestoreWalletScreen +import to.bitkit.ui.onboarding.TermsOfUseScreen +import to.bitkit.ui.onboarding.WarningMultipleDevicesScreen + +private const val LAST_SLIDE_INDEX = 4 + +fun EntryProviderScope.onboardingEntries( + navigator: Navigator, + isGeoBlocked: Boolean, + onCreateWallet: (passphrase: String?) -> Unit, + onRestoreWallet: (mnemonic: String, passphrase: String?) -> Unit, +) { + entry { + TermsOfUseScreen( + onNavigateToIntro = { navigator.navigate(Routes.Intro) } + ) + } + + entry { + IntroScreen( + onStartClick = { navigator.navigate(Routes.Slides()) }, + onSkipClick = { navigator.navigate(Routes.Slides(LAST_SLIDE_INDEX)) }, + ) + } + + entry { route -> + OnboardingSlidesScreen( + currentTab = route.tab, + isGeoBlocked = isGeoBlocked, + onAdvancedSetupClick = { navigator.navigate(Routes.Advanced) }, + onCreateClick = { onCreateWallet(null) }, + onRestoreClick = { navigator.navigate(Routes.WarningMultipleDevices) }, + ) + } + + entry { + WarningMultipleDevicesScreen( + onBackClick = { navigator.goBack() }, + onConfirmClick = { navigator.navigate(Routes.Restore) } + ) + } + + entry { + RestoreWalletScreen( + onBackClick = { navigator.goBack() }, + onRestoreClick = onRestoreWallet + ) + } + + entry { + CreateWalletWithPassphraseScreen( + onBackClick = { navigator.goBack() }, + onCreateClick = onCreateWallet, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/SettingsEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/SettingsEntries.kt new file mode 100644 index 000000000..3095757be --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/SettingsEntries.kt @@ -0,0 +1,289 @@ +package to.bitkit.ui.nav.entries + +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.ui.NodeInfoScreen +import to.bitkit.ui.components.AuthCheckScreen +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.screens.settings.DevSettingsScreen +import to.bitkit.ui.screens.settings.FeeSettingsScreen +import to.bitkit.ui.screens.settings.LdkDebugScreen +import to.bitkit.ui.settings.AboutScreen +import to.bitkit.ui.settings.AdvancedSettingsScreen +import to.bitkit.ui.settings.BackupSettingsScreen +import to.bitkit.ui.settings.BlocktankRegtestScreen +import to.bitkit.ui.settings.CJitDetailScreen +import to.bitkit.ui.settings.ChannelOrdersScreen +import to.bitkit.ui.settings.LanguageSettingsScreen +import to.bitkit.ui.settings.LogDetailScreen +import to.bitkit.ui.settings.LogsScreen +import to.bitkit.ui.settings.OrderDetailScreen +import to.bitkit.ui.settings.SecuritySettingsScreen +import to.bitkit.ui.settings.SettingsScreen +import to.bitkit.ui.settings.advanced.AddressViewerScreen +import to.bitkit.ui.settings.advanced.CoinSelectPreferenceScreen +import to.bitkit.ui.settings.advanced.ElectrumConfigScreen +import to.bitkit.ui.settings.advanced.RgsServerScreen +import to.bitkit.ui.settings.appStatus.AppStatusScreen +import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsIntroScreen +import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsSettings +import to.bitkit.ui.settings.backups.ResetAndRestoreScreen +import to.bitkit.ui.settings.general.DefaultUnitSettingsScreen +import to.bitkit.ui.settings.general.GeneralSettingsScreen +import to.bitkit.ui.settings.general.LocalCurrencySettingsScreen +import to.bitkit.ui.settings.general.TagsSettingsScreen +import to.bitkit.ui.settings.general.WidgetsSettingsScreen +import to.bitkit.ui.settings.lightning.ChannelDetailScreen +import to.bitkit.ui.settings.lightning.CloseConnectionScreen +import to.bitkit.ui.settings.lightning.LightningConnectionsScreen +import to.bitkit.ui.settings.lightning.LightningConnectionsViewModel +import to.bitkit.ui.settings.pin.ChangePinConfirmScreen +import to.bitkit.ui.settings.pin.ChangePinNewScreen +import to.bitkit.ui.settings.pin.ChangePinResultScreen +import to.bitkit.ui.settings.pin.ChangePinScreen +import to.bitkit.ui.settings.pin.DisablePinScreen +import to.bitkit.ui.settings.quickPay.QuickPayIntroScreen +import to.bitkit.ui.settings.quickPay.QuickPaySettingsScreen +import to.bitkit.ui.settings.support.ReportIssueResultScreen +import to.bitkit.ui.settings.support.ReportIssueScreen +import to.bitkit.ui.settings.support.SupportScreen +import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen +import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen +import to.bitkit.viewmodels.AppViewModel +import to.bitkit.viewmodels.CurrencyViewModel +import to.bitkit.viewmodels.SettingsViewModel + +/** + * Settings section entry providers for Navigation 3. + */ +@Suppress("LongMethod", "LongParameterList") +fun EntryProviderScope.settingsEntries( + navigator: Navigator, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, + currencyViewModel: CurrencyViewModel, + lightningConnectionsViewModel: LightningConnectionsViewModel, +) { + entry { + SettingsScreen(navigator) + } + + entry { + GeneralSettingsScreen(navigator) + } + + entry { + SecuritySettingsScreen(navigator) + } + + entry { + AdvancedSettingsScreen(navigator) + } + + entry { + AboutScreen(onBack = { navigator.goBack() }) + } + + entry { + TransactionSpeedSettingsScreen(navigator) + } + + entry { + CustomFeeSettingsScreen(navigator) + } + + entry { + WidgetsSettingsScreen(navigator) + } + + entry { + TagsSettingsScreen(navigator) + } + + entry { + NodeInfoScreen(navigator) + } + + entry { + CoinSelectPreferenceScreen(navigator) + } + + entry { + ElectrumConfigScreen(navigator) + } + + entry { + RgsServerScreen(navigator) + } + + entry { + AddressViewerScreen(navigator) + } + + entry { + DisablePinScreen(navigator) + } + + entry { + ChangePinScreen(navigator) + } + + entry { + ChangePinNewScreen(navigator) + } + + entry { route -> + ChangePinConfirmScreen(newPin = route.newPin, navigator = navigator) + } + + entry { + ChangePinResultScreen(navigator) + } + + entry { route -> + AuthCheckScreen( + navigator = navigator, + route = route, + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, + ) + } + + entry { + DefaultUnitSettingsScreen(currencyViewModel = currencyViewModel, navigator = navigator) + } + + entry { + LocalCurrencySettingsScreen(currencyViewModel = currencyViewModel, navigator = navigator) + } + + entry { + BackupSettingsScreen(navigator) + } + + entry { + ResetAndRestoreScreen(navigator) + } + + entry { + ChannelOrdersScreen( + onBackClick = { navigator.goBack() }, + onOrderItemClick = { orderId -> navigator.navigate(Routes.OrderDetail(orderId)) }, + onCjitItemClick = { entryId -> navigator.navigate(Routes.CjitDetail(entryId)) }, + ) + } + + entry { route -> + OrderDetailScreen( + orderId = route.orderId, + onBackClick = { navigator.goBack() }, + ) + } + + entry { route -> + CJitDetailScreen( + entryId = route.entryId, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + LightningConnectionsScreen(navigator = navigator, viewModel = lightningConnectionsViewModel) + } + + entry { + ChannelDetailScreen(navigator = navigator, viewModel = lightningConnectionsViewModel) + } + + entry { + CloseConnectionScreen(navigator = navigator, viewModel = lightningConnectionsViewModel) + } + + entry { + LogsScreen(navigator) + } + + entry { route -> + LogDetailScreen(navigator = navigator, fileName = route.fileName) + } + + entry { + QuickPayIntroScreen( + onBack = { navigator.goBack() }, + onContinue = { navigator.navigateToQuickPaySettings() }, + ) + } + + entry { + QuickPaySettingsScreen(onBack = { navigator.goBack() }) + } + + entry { + LanguageSettingsScreen(onBackClick = { navigator.goBack() }) + } + + entry { + BackgroundPaymentsIntroScreen( + onBack = { navigator.goBack() }, + onContinue = { navigator.navigate(Routes.BackgroundPaymentsSettings) }, + ) + } + + entry { + BackgroundPaymentsSettings(onBack = { navigator.goBack() }) + } + + entry { + DevSettingsScreen(navigator) + } + + entry { + LdkDebugScreen(navigator) + } + + entry { + FeeSettingsScreen(navigator) + } + + entry { + BlocktankRegtestScreen(navigator) + } + + entry { + AppStatusScreen(navigator) + } + + entry { + SupportScreen(navigator) + } + + entry { + ReportIssueScreen( + onBack = { navigator.goBack() }, + navigateResultScreen = { success -> + if (success) { + navigator.navigate(Routes.ReportIssueSuccess) + } else { + navigator.navigate(Routes.ReportIssueFailure) + } + } + ) + } + + entry { + ReportIssueResultScreen( + isSuccess = true, + onBack = { navigator.goBack() }, + onClose = { navigator.navigate(Routes.Support) }, + ) + } + + entry { + ReportIssueResultScreen( + isSuccess = false, + onBack = { navigator.goBack() }, + onClose = { navigator.navigate(Routes.Support) }, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt new file mode 100644 index 000000000..896f41209 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt @@ -0,0 +1,896 @@ +package to.bitkit.ui.nav.entries + +import android.content.Intent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.core.net.toUri +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.R +import to.bitkit.env.Env +import to.bitkit.models.NewTransactionSheetDetails +import to.bitkit.models.NewTransactionSheetDirection +import to.bitkit.models.NewTransactionSheetType +import to.bitkit.ui.LocalBalances +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.SheetSceneStrategy +import to.bitkit.ui.screens.scanner.QrScanningScreen +import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorContent +import to.bitkit.ui.screens.wallets.activity.TagSelectorContent +import to.bitkit.ui.screens.wallets.receive.EditInvoiceScreen +import to.bitkit.ui.screens.wallets.receive.LocationBlockScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveAmountScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveConfirmScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveLiquidityScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveQrScreen +import to.bitkit.ui.screens.wallets.send.AddTagScreen +import to.bitkit.ui.screens.wallets.send.SendAddressScreen +import to.bitkit.ui.screens.wallets.send.SendAmountScreen +import to.bitkit.ui.screens.wallets.send.SendCoinSelectionScreen +import to.bitkit.ui.screens.wallets.send.SendConfirmScreen +import to.bitkit.ui.screens.wallets.send.SendErrorScreen +import to.bitkit.ui.screens.wallets.send.SendFeeCustomScreen +import to.bitkit.ui.screens.wallets.send.SendFeeRateScreen +import to.bitkit.ui.screens.wallets.send.SendFeeViewModel +import to.bitkit.ui.screens.wallets.send.SendPinCheckScreen +import to.bitkit.ui.screens.wallets.send.SendQuickPayScreen +import to.bitkit.ui.screens.wallets.send.SendRecipientScreen +import to.bitkit.ui.screens.wallets.withdraw.WithdrawConfirmScreen +import to.bitkit.ui.screens.wallets.withdraw.WithdrawErrorScreen +import to.bitkit.ui.settings.backups.BackupIntroScreen +import to.bitkit.ui.settings.backups.BackupNavSheetViewModel +import to.bitkit.ui.settings.backups.ConfirmMnemonicScreen +import to.bitkit.ui.settings.backups.ConfirmPassphraseScreen +import to.bitkit.ui.settings.backups.MetadataScreen +import to.bitkit.ui.settings.backups.MultipleDevicesScreen +import to.bitkit.ui.settings.backups.ShowMnemonicScreen +import to.bitkit.ui.settings.backups.ShowPassphraseScreen +import to.bitkit.ui.settings.backups.SuccessScreen +import to.bitkit.ui.settings.backups.WarningScreen +import to.bitkit.ui.settings.pin.PinBiometricsScreen +import to.bitkit.ui.settings.pin.PinChooseScreen +import to.bitkit.ui.settings.pin.PinConfirmScreen +import to.bitkit.ui.settings.pin.PinPromptScreen +import to.bitkit.ui.settings.pin.PinResultScreen +import to.bitkit.ui.settings.support.SupportScreen +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.sheets.BackgroundPaymentsIntroSheet +import to.bitkit.ui.sheets.ForceTransferContent +import to.bitkit.ui.sheets.GiftErrorSheet +import to.bitkit.ui.sheets.GiftLoading +import to.bitkit.ui.sheets.GiftViewModel +import to.bitkit.ui.sheets.HighBalanceWarningSheet +import to.bitkit.ui.sheets.LnurlAuthContent +import to.bitkit.ui.sheets.NewTransactionSheetView +import to.bitkit.ui.sheets.QuickPayIntroSheet +import to.bitkit.ui.sheets.UpdateSheet +import to.bitkit.ui.utils.NotificationUtils +import to.bitkit.viewmodels.ActivityListViewModel +import to.bitkit.viewmodels.AmountInputViewModel +import to.bitkit.viewmodels.AppViewModel +import to.bitkit.viewmodels.SendEvent +import to.bitkit.viewmodels.SettingsViewModel +import to.bitkit.viewmodels.TransferViewModel +import to.bitkit.viewmodels.WalletViewModel + +/** + * Sheet flow entry providers for Navigation 3. + * These handle flows that were previously rendered as bottom sheets with internal navigation. + */ +@Suppress("LongMethod", "LongParameterList") +fun EntryProviderScope.sheetEntries( + navigator: Navigator, + appViewModel: AppViewModel, + walletViewModel: WalletViewModel, + activityListViewModel: ActivityListViewModel, + transferViewModel: TransferViewModel, +) { + // Simple sheet entries + simpleSheetEntries(navigator, appViewModel, activityListViewModel, transferViewModel) + + // Pin flow entries + pinFlowEntries(navigator) + + // Backup flow entries + backupFlowEntries(navigator) + + // Send flow entries + sendFlowEntries(navigator, appViewModel, walletViewModel) + + // Receive flow entries + receiveFlowEntries(navigator, walletViewModel) + + // Gift flow entries + giftFlowEntries(navigator, appViewModel) + + // Timed sheet entries + timedSheetEntries(navigator, appViewModel) +} + +/** + * Simple sheets that don't have internal navigation. + */ +@Suppress("LongParameterList", "LongMethod") +private fun EntryProviderScope.simpleSheetEntries( + navigator: Navigator, + appViewModel: AppViewModel, + activityListViewModel: ActivityListViewModel, + transferViewModel: TransferViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val startDate by activityListViewModel.startDate.collectAsStateWithLifecycle() + val endDate by activityListViewModel.endDate.collectAsStateWithLifecycle() + + DateRangeSelectorContent( + initialStartDate = startDate, + initialEndDate = endDate, + onClearClick = { activityListViewModel.clearDateRange() }, + onApplyClick = { start, end -> + activityListViewModel.setDateRange(startDate = start, endDate = end) + navigator.goBack() + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val availableTags by activityListViewModel.availableTags.collectAsStateWithLifecycle() + val selectedTags by activityListViewModel.selectedTags.collectAsStateWithLifecycle() + + TagSelectorContent( + availableTags = availableTags, + selectedTags = selectedTags, + onTagClick = { + activityListViewModel.toggleTag(it) + navigator.goBack() + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + LnurlAuthContent( + domain = route.domain, + onContinue = { + appViewModel.requestLnurlAuth( + callback = route.lnurl, + k1 = route.k1, + domain = route.domain, + ) + }, + onCancel = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val isLoading by transferViewModel.isForceTransferLoading.collectAsStateWithLifecycle() + + ForceTransferContent( + isLoading = isLoading, + onForceTransfer = { + transferViewModel.forceTransfer { + navigator.goBack() + } + }, + onCancel = { navigator.goBack() }, + ) + } +} + +/** + * Pin setup flow entries. + */ +private fun EntryProviderScope.pinFlowEntries(navigator: Navigator) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + PinPromptScreen( + showLaterButton = route.showLaterButton, + onContinue = { navigator.navigate(Routes.PinChoose) }, + onLater = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + PinChooseScreen( + onPinChosen = { pin -> navigator.navigate(Routes.PinConfirm(pin)) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + PinConfirmScreen( + originalPin = route.pin, + onPinConfirmed = { navigator.navigate(Routes.PinBiometrics) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + PinBiometricsScreen( + onContinue = { isBioOn -> navigator.navigate(Routes.PinResult(isBioOn)) }, + onSkip = { navigator.navigate(Routes.PinResult(isBioOn = false)) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + PinResultScreen( + isBioOn = route.isBioOn, + onDismiss = { navigator.navigateToHome() }, + onBack = { navigator.navigateToHome() }, + ) + } +} + +/** + * Backup flow entries. + */ +@Suppress("LongMethod") +private fun EntryProviderScope.backupFlowEntries(navigator: Navigator) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + BackupIntroScreen( + hasFunds = LocalBalances.current.totalSats > 0u, + onClose = { navigator.goBack() }, + onConfirm = { navigator.navigate(Routes.BackupShowMnemonic) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + ShowMnemonicScreen( + uiState = viewModel.uiState.value, + onRevealClick = viewModel::onRevealMnemonic, + onContinueClick = { navigator.navigate(Routes.BackupShowPassphrase) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + ShowPassphraseScreen( + uiState = viewModel.uiState.value, + onContinue = { navigator.navigate(Routes.BackupConfirmMnemonic) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + ConfirmMnemonicScreen( + uiState = viewModel.uiState.value, + onContinue = { navigator.navigate(Routes.BackupConfirmPassphrase) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + ConfirmPassphraseScreen( + uiState = viewModel.uiState.value, + onPassphraseChange = viewModel::onPassphraseInput, + onContinue = { navigator.navigate(Routes.BackupWarning) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + WarningScreen( + onContinue = { navigator.navigate(Routes.BackupSuccess) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + SuccessScreen( + onContinue = { navigator.navigate(Routes.BackupMultipleDevices) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + MultipleDevicesScreen( + onContinue = { navigator.navigate(Routes.BackupMetadata) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + MetadataScreen( + uiState = viewModel.uiState.value, + onDismiss = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + ) + } +} + +/** + * Send flow entries (17 routes). + */ +@Suppress("LongMethod") +private fun EntryProviderScope.sendFlowEntries( + navigator: Navigator, + appViewModel: AppViewModel, + walletViewModel: WalletViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + LaunchedEffect(Unit) { + appViewModel.resetSendState() + appViewModel.resetQuickPayData() + } + SendRecipientScreen( + onEvent = { appViewModel.setSendEvent(it) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendAddressScreen( + uiState = uiState, + onBack = { navigator.goBack() }, + onEvent = { appViewModel.setSendEvent(it) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(route.prefill) { + route.prefill?.let { prefill -> + appViewModel.setSendEvent(SendEvent.AddressContinue(prefill)) + } + } + + SendAmountScreen( + uiState = uiState, + walletUiState = walletUiState, + canGoBack = true, + onBack = { navigator.goBack() }, + onEvent = { appViewModel.setSendEvent(it) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + QrScanningScreen( + navigator = navigator, + onScanSuccess = { qrCode -> + navigator.goBack() + appViewModel.onScanResult(data = qrCode) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendCoinSelectionScreen( + requiredAmount = sendUiState.amount, + address = sendUiState.address, + onBack = { navigator.goBack() }, + onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val viewModel = hiltViewModel() + SendFeeRateScreen( + sendUiState = sendUiState, + viewModel = viewModel, + onBack = { navigator.goBack() }, + onContinue = { navigator.goBack() }, + onSelect = { speed -> appViewModel.onSelectSpeed(speed) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + SendFeeCustomScreen( + viewModel = viewModel, + onBack = { navigator.goBack() }, + onContinue = { speed -> appViewModel.setTransactionSpeed(speed) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() + + SendConfirmScreen( + savedStateHandle = remember { androidx.lifecycle.SavedStateHandle() }, + uiState = uiState, + isNodeRunning = walletUiState.nodeLifecycleState.isRunning(), + canGoBack = true, + onBack = { navigator.goBack() }, + onEvent = { e -> appViewModel.setSendEvent(e) }, + onClickAddTag = { navigator.navigate(Routes.SendAddTag) }, + onClickTag = { tag -> appViewModel.removeTag(tag) }, + onNavigateToPin = { navigator.navigate(Routes.SendPinCheck) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val sendDetail by appViewModel.successSendUiState.collectAsStateWithLifecycle() + NewTransactionSheetView( + details = sendDetail, + onCloseClick = { navigator.navigateToHome() }, + onDetailClick = { appViewModel.onClickSendDetail() }, + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .navigationBarsPadding() + .testTag("SendSuccess"), + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + SendErrorScreen( + errorMessage = route.message, + onRetry = { navigator.navigate(Routes.SendRecipient) }, + onClose = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + WithdrawConfirmScreen( + uiState = uiState, + onBack = { navigator.goBack() }, + onConfirm = { appViewModel.onConfirmWithdraw() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + WithdrawErrorScreen( + uiState = uiState, + onBack = { navigator.goBack() }, + onClickScan = { navigator.navigate(Routes.SendQrScanner) }, + onClickSupport = { navigator.navigate(Routes.SendSupport) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + SupportScreen(navigator) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + AddTagScreen( + onBack = { navigator.goBack() }, + onTagSelected = { tag -> + appViewModel.addTagToSelected(tag) + navigator.goBack() + }, + tqgInputTestTag = "TagInputSend", + addButtonTestTag = "SendTagsSubmit", + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + SendPinCheckScreen( + onBack = { navigator.goBack() }, + onSuccess = { + navigator.goBack() + appViewModel.setSendEvent(SendEvent.PayConfirmed) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val quickPayData by appViewModel.quickPayData.collectAsStateWithLifecycle() + quickPayData?.let { data -> + SendQuickPayScreen( + quickPayData = data, + onPaymentComplete = { paymentHash, amountWithFee -> + appViewModel.handlePaymentSuccess( + NewTransactionSheetDetails( + type = NewTransactionSheetType.LIGHTNING, + direction = NewTransactionSheetDirection.SENT, + paymentHashOrTxId = paymentHash, + sats = amountWithFee, + ), + ) + }, + onShowError = { errorMessage -> + navigator.navigate(Routes.SendError(errorMessage)) + }, + ) + } + } +} + +/** + * Receive flow entries (9 routes). + */ +@Suppress("LongMethod") +private fun EntryProviderScope.receiveFlowEntries( + navigator: Navigator, + walletViewModel: WalletViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() + val cjitInvoice by walletViewModel.pendingCjitInvoice.collectAsStateWithLifecycle() + val lightningState by walletViewModel.lightningState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + walletViewModel.resetPreActivityMetadataTagsForCurrentInvoice() + walletViewModel.refreshReceiveState() + } + + ReceiveQrScreen( + cjitInvoice = cjitInvoice, + walletState = walletUiState, + onClickEditInvoice = { navigator.navigate(Routes.ReceiveEditInvoice) }, + onClickReceiveCjit = { + if (lightningState.isGeoBlocked) { + navigator.navigate(Routes.ReceiveGeoBlock) + } else { + navigator.navigate(Routes.ReceiveAmount) + } + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + ReceiveAmountScreen( + onCjitCreated = { entry -> + walletViewModel.setPendingCjitEntry(entry) + navigator.navigate(Routes.ReceiveConfirm) + }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + LocationBlockScreen( + onBackPressed = { navigator.goBack() }, + navigateAdvancedSetup = { navigator.navigate(Routes.ExternalConnection()) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + entry?.let { entryDetails -> + ReceiveConfirmScreen( + entry = entryDetails, + onLearnMore = { navigator.navigate(Routes.ReceiveLiquidity) }, + onContinue = { invoice -> + walletViewModel.setPendingCjitInvoice(invoice) + navigator.popBackTo(Routes.ReceiveQr) + }, + onBack = { navigator.goBack() }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + entry?.let { entryDetails -> + ReceiveConfirmScreen( + entry = entryDetails, + isAdditional = true, + onLearnMore = { navigator.navigate(Routes.ReceiveLiquidityAdditional) }, + onContinue = { invoice -> + walletViewModel.setPendingCjitInvoice(invoice) + navigator.popBackTo(Routes.ReceiveQr) + }, + onBack = { navigator.goBack() }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + val settingsViewModel = hiltViewModel() + val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + val context = LocalContext.current + + entry?.let { entryDetails -> + ReceiveLiquidityScreen( + entry = entryDetails, + onContinue = { navigator.goBack() }, + onBack = { navigator.goBack() }, + hasNotificationPermission = notificationsGranted, + onSwitchClick = { NotificationUtils.openNotificationSettings(context) }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + val settingsViewModel = hiltViewModel() + val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + val context = LocalContext.current + + entry?.let { entryDetails -> + ReceiveLiquidityScreen( + entry = entryDetails, + isAdditional = true, + onContinue = { navigator.goBack() }, + onBack = { navigator.goBack() }, + hasNotificationPermission = notificationsGranted, + onSwitchClick = { NotificationUtils.openNotificationSettings(context) }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val walletState by walletViewModel.walletState.collectAsStateWithLifecycle() + val editInvoiceAmountViewModel = hiltViewModel() + + LaunchedEffect(Unit) { editInvoiceAmountViewModel.clearInput() } + + EditInvoiceScreen( + amountInputViewModel = editInvoiceAmountViewModel, + walletUiState = walletState, + onBack = { navigator.goBack() }, + updateInvoice = walletViewModel::updateBip21Invoice, + onClickAddTag = { navigator.navigate(Routes.ReceiveAddTag) }, + onClickTag = walletViewModel::removeTag, + onDescriptionUpdate = walletViewModel::updateBip21Description, + navigateReceiveConfirm = { entry -> + walletViewModel.setPendingCjitEntry(entry) + navigator.navigate(Routes.ReceiveConfirmInbound) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + AddTagScreen( + onBack = { navigator.goBack() }, + onTagSelected = { tag -> + walletViewModel.addTagToSelected(tag) + navigator.goBack() + }, + tqgInputTestTag = "TagInputReceive", + addButtonTestTag = "ReceiveTagsSubmit", + ) + } +} + +/** + * Gift flow entries (5 routes). + */ +@Suppress("LongMethod") +private fun EntryProviderScope.giftFlowEntries( + navigator: Navigator, + appViewModel: AppViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + val viewModel = hiltViewModel() + + LaunchedEffect(route.code, route.amount) { + viewModel.initialize(route.code, route.amount) + } + + LaunchedEffect(viewModel) { + viewModel.successEvent.collect { details -> + navigator.navigateToHome() + appViewModel.showTransactionSheet(details) + } + } + + LaunchedEffect(viewModel) { + viewModel.navigationEvent.collect { route -> + when (route) { + is Routes.GiftUsed -> navigator.navigate(Routes.GiftUsed) + is Routes.GiftUsedUp -> navigator.navigate(Routes.GiftUsedUp) + is Routes.GiftError -> navigator.navigate(Routes.GiftError) + is Routes.GiftSuccess -> navigator.navigateToHome() + else -> { /* Ignore other routes */ } + } + } + } + + GiftLoading(viewModel = viewModel) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + GiftErrorSheet( + titleRes = R.string.other__gift__used__title, + textRes = R.string.other__gift__used__text, + testTag = "GiftUsed", + onDismiss = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + GiftErrorSheet( + titleRes = R.string.other__gift__used_up__title, + textRes = R.string.other__gift__used_up__text, + testTag = "GiftUsedUp", + onDismiss = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + GiftErrorSheet( + titleRes = R.string.other__gift__error__title, + textRes = R.string.other__gift__error__text, + testTag = "GiftError", + onDismiss = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + // This route is typically not navigated to directly, + // as success triggers navigation to home and shows transaction sheet + LaunchedEffect(Unit) { + navigator.navigateToHome() + } + } +} + +/** + * Timed sheet entries - sheets that appear automatically based on conditions. + */ +@Suppress("LongMethod") +private fun EntryProviderScope.timedSheetEntries( + navigator: Navigator, + appViewModel: AppViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + UpdateSheet( + onCancel = { + appViewModel.dismissTimedSheet() + navigator.goBack() + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + BackupIntroScreen( + hasFunds = LocalBalances.current.totalSats > 0u, + onClose = { + appViewModel.dismissTimedSheet() + navigator.goBack() + }, + onConfirm = { navigator.navigate(Routes.BackupShowMnemonic) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + BackgroundPaymentsIntroSheet( + onContinue = { + appViewModel.dismissTimedSheet(skipQueue = true) + navigator.navigate(Routes.BackgroundPaymentsSettings) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + QuickPayIntroSheet( + onContinue = { + appViewModel.dismissTimedSheet(skipQueue = true) + navigator.navigateToQuickPaySettings() + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val context = LocalContext.current + HighBalanceWarningSheet( + understoodClick = { + appViewModel.dismissTimedSheet() + navigator.goBack() + }, + learnMoreClick = { + val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri()) + context.startActivity(intent) + appViewModel.dismissTimedSheet(skipQueue = true) + navigator.goBack() + }, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/TransferEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/TransferEntries.kt new file mode 100644 index 000000000..988cf89e9 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/TransferEntries.kt @@ -0,0 +1,324 @@ +package to.bitkit.ui.nav.entries + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.models.Toast +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.screens.transfer.FundingAdvancedScreen +import to.bitkit.ui.screens.transfer.FundingScreen +import to.bitkit.ui.screens.transfer.LiquidityScreen +import to.bitkit.ui.screens.transfer.SavingsAdvancedScreen +import to.bitkit.ui.screens.transfer.SavingsAvailabilityScreen +import to.bitkit.ui.screens.transfer.SavingsConfirmScreen +import to.bitkit.ui.screens.transfer.SavingsIntroScreen +import to.bitkit.ui.screens.transfer.SavingsProgressScreen +import to.bitkit.ui.screens.transfer.SettingUpScreen +import to.bitkit.ui.screens.transfer.SpendingAdvancedScreen +import to.bitkit.ui.screens.transfer.SpendingAmountScreen +import to.bitkit.ui.screens.transfer.SpendingConfirmScreen +import to.bitkit.ui.screens.transfer.SpendingIntroScreen +import to.bitkit.ui.screens.transfer.TransferIntroScreen +import to.bitkit.ui.screens.transfer.external.ExternalAmountScreen +import to.bitkit.ui.screens.transfer.external.ExternalConfirmScreen +import to.bitkit.ui.screens.transfer.external.ExternalConnectionScreen +import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen +import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel +import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen +import to.bitkit.ui.screens.transfer.external.LnurlChannelScreen +import to.bitkit.ui.screens.scanner.QrScanningScreen +import to.bitkit.viewmodels.AppViewModel +import to.bitkit.viewmodels.SettingsViewModel +import to.bitkit.viewmodels.TransferViewModel +import to.bitkit.viewmodels.WalletViewModel + +/** + * Transfer flow entry providers for Navigation 3. + */ +@Suppress("LongMethod", "LongParameterList") +fun EntryProviderScope.transferEntries( + navigator: Navigator, + appViewModel: AppViewModel, + walletViewModel: WalletViewModel, + transferViewModel: TransferViewModel, + settingsViewModel: SettingsViewModel, +) { + // Transfer Intro + entry { + TransferIntroScreen( + onContinueClick = { + navigator.navigate(Routes.Funding) + settingsViewModel.setHasSeenTransferIntro(true) + }, + onBackClick = { navigator.goBack() }, + ) + } + + // Savings Flow + entry { + SavingsIntroScreen( + onContinueClick = { + navigator.navigate(Routes.SavingsAvailability) + settingsViewModel.setHasSeenSavingsIntro(true) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SavingsAvailabilityScreen( + onBackClick = { navigator.goBack() }, + onCancelClick = { navigator.navigateToHome() }, + onContinueClick = { navigator.navigate(Routes.SavingsConfirm) }, + ) + } + + entry { + SavingsConfirmScreen( + onConfirm = { navigator.navigate(Routes.SavingsProgress) }, + onAdvancedClick = { navigator.navigate(Routes.SavingsAdvanced) }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SavingsAdvancedScreen( + onContinueClick = { navigator.goBack() }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SavingsProgressScreen( + wallet = walletViewModel, + transfer = transferViewModel, + onContinueClick = { navigator.navigateToHome() }, + onForceTransfer = { navigator.navigate(Routes.ForceTransferSheet) }, + ) + } + + // Spending Flow + entry { + SpendingIntroScreen( + onContinueClick = { + navigator.navigate(Routes.SpendingAmount) + settingsViewModel.setHasSeenSpendingIntro(true) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SpendingAmountScreen( + viewModel = transferViewModel, + onBackClick = { navigator.goBack() }, + onOrderCreated = { navigator.navigate(Routes.SpendingConfirm) }, + toastException = { appViewModel.toast(it) }, + toast = { title, description -> + appViewModel.toast( + type = Toast.ToastType.ERROR, + title = title, + description = description, + ) + }, + ) + } + + entry { + SpendingConfirmScreen( + viewModel = transferViewModel, + onBackClick = { navigator.goBack() }, + onCloseClick = { navigator.navigateToHome() }, + onLearnMoreClick = { navigator.navigate(Routes.TransferLiquidity) }, + onAdvancedClick = { navigator.navigate(Routes.SpendingAdvanced) }, + onConfirm = { navigator.navigate(Routes.SettingUp) }, + ) + } + + entry { + SpendingAdvancedScreen( + viewModel = transferViewModel, + onBackClick = { navigator.goBack() }, + onOrderCreated = { navigator.goBack() }, + ) + } + + entry { + LiquidityScreen( + onBackClick = { navigator.goBack() }, + onContinueClick = { navigator.goBack() }, + ) + } + + entry { + SettingUpScreen( + viewModel = transferViewModel, + onContinueClick = { navigator.navigateToHome() }, + ) + } + + // Funding Flow + entry { + FundingEntry( + navigator = navigator, + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, + ) + } + + entry { + FundingAdvancedScreen( + onLnurl = { navigator.navigate(Routes.QrScanner) }, + onManual = { navigator.navigate(Routes.ExternalConnection()) }, + onBackClick = { navigator.goBack() }, + ) + } + + // External Node Flow + externalNodeEntries( + navigator = navigator, + walletViewModel = walletViewModel, + ) +} + +@Composable +private fun FundingEntry( + navigator: Navigator, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, +) { + val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() + val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() + + FundingScreen( + onTransfer = { + if (!hasSeenSpendingIntro) { + navigator.navigate(Routes.SpendingIntro) + } else { + navigator.navigate(Routes.SpendingAmount) + } + }, + onFund = { + navigator.navigate(Routes.ReceiveQr) + }, + onAdvanced = { navigator.navigate(Routes.FundingAdvanced) }, + onBackClick = { navigator.goBack() }, + isGeoBlocked = isGeoBlocked, + ) +} + +/** + * External node connection flow entries. + * Note: Uses a shared ViewModel across screens for the external node connection flow. + */ +private fun EntryProviderScope.externalNodeEntries( + navigator: Navigator, + walletViewModel: WalletViewModel, +) { + entry { route -> + ExternalConnectionEntry( + navigator = navigator, + scannedNodeUri = route.scannedNodeUri, + ) + } + + entry { + ExternalAmountEntry(navigator = navigator) + } + + entry { + ExternalConfirmEntry( + navigator = navigator, + walletViewModel = walletViewModel, + ) + } + + entry { + ExternalFeeCustomEntry(navigator = navigator) + } + + entry { + ExternalSuccessScreen( + onContinue = { navigator.navigateToHome() }, + ) + } + + entry { route -> + LnurlChannelScreen( + uri = route.uri, + callback = route.callback, + k1 = route.k1, + onConnected = { navigator.navigate(Routes.ExternalSuccess) }, + onBack = { navigator.goBack() }, + onClose = { navigator.navigateToHome() }, + ) + } + + entry { + QrScanningScreen( + navigator = navigator, + onScanSuccess = { qrCode -> + navigator.navigate(Routes.ExternalConnection(scannedNodeUri = qrCode)) + }, + ) + } +} + +@Composable +private fun ExternalConnectionEntry( + navigator: Navigator, + scannedNodeUri: String?, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalConnectionScreen( + scannedNodeUri = scannedNodeUri, + viewModel = viewModel, + onNodeConnected = { navigator.navigate(Routes.ExternalAmount) }, + onScanClick = { navigator.navigate(Routes.ExternalNodeScanner) }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ExternalAmountEntry( + navigator: Navigator, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalAmountScreen( + viewModel = viewModel, + onContinue = { navigator.navigate(Routes.ExternalConfirm) }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ExternalConfirmEntry( + navigator: Navigator, + walletViewModel: WalletViewModel, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalConfirmScreen( + viewModel = viewModel, + onConfirm = { + walletViewModel.refreshState() + navigator.navigate(Routes.ExternalSuccess) + }, + onNetworkFeeClick = { navigator.navigate(Routes.ExternalFeeCustom) }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ExternalFeeCustomEntry( + navigator: Navigator, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalFeeCustomScreen( + viewModel = viewModel, + onBack = { navigator.goBack() }, + ) +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/WidgetEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/WidgetEntries.kt new file mode 100644 index 000000000..5746ef56b --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/WidgetEntries.kt @@ -0,0 +1,302 @@ +package to.bitkit.ui.nav.entries + +import androidx.compose.runtime.Composable +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.models.WidgetType +import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.screens.widgets.AddWidgetsScreen +import to.bitkit.ui.screens.widgets.WidgetsIntroScreen +import to.bitkit.ui.screens.widgets.blocks.BlocksEditScreen +import to.bitkit.ui.screens.widgets.blocks.BlocksPreviewScreen +import to.bitkit.ui.screens.widgets.blocks.BlocksViewModel +import to.bitkit.ui.screens.widgets.calculator.CalculatorPreviewScreen +import to.bitkit.ui.screens.widgets.calculator.CalculatorViewModel +import to.bitkit.ui.screens.widgets.facts.FactsEditScreen +import to.bitkit.ui.screens.widgets.facts.FactsPreviewScreen +import to.bitkit.ui.screens.widgets.facts.FactsViewModel +import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditScreen +import to.bitkit.ui.screens.widgets.headlines.HeadlinesPreviewScreen +import to.bitkit.ui.screens.widgets.headlines.HeadlinesViewModel +import to.bitkit.ui.screens.widgets.price.PriceEditScreen +import to.bitkit.ui.screens.widgets.price.PricePreviewScreen +import to.bitkit.ui.screens.widgets.price.PriceViewModel +import to.bitkit.ui.screens.widgets.weather.WeatherEditScreen +import to.bitkit.ui.screens.widgets.weather.WeatherPreviewScreen +import to.bitkit.ui.screens.widgets.weather.WeatherViewModel +import to.bitkit.viewmodels.CurrencyViewModel +import to.bitkit.viewmodels.SettingsViewModel + +/** + * Widget flow entry providers for Navigation 3. + */ +@Suppress("LongMethod") +fun EntryProviderScope.widgetEntries( + navigator: Navigator, + currencyViewModel: CurrencyViewModel, + settingsViewModel: SettingsViewModel, +) { + // Widgets Intro + entry { + WidgetsIntroScreen( + onContinue = { + settingsViewModel.setHasSeenWidgetsIntro(true) + navigator.navigate(Routes.AddWidget) + }, + onBackClick = { navigator.goBack() }, + ) + } + + // Add Widget + entry { + AddWidgetsScreen( + fiatSymbol = LocalCurrencies.current.currencySymbol, + onWidgetSelected = { widgetType -> + when (widgetType) { + WidgetType.NEWS -> navigator.navigate(Routes.Headlines) + WidgetType.FACTS -> navigator.navigate(Routes.Facts) + WidgetType.BLOCK -> navigator.navigate(Routes.Blocks) + WidgetType.WEATHER -> navigator.navigate(Routes.Weather) + WidgetType.PRICE -> navigator.navigate(Routes.Price) + WidgetType.CALCULATOR -> navigator.navigate(Routes.CalculatorPreview) + } + }, + onBackCLick = { navigator.goBack() }, + ) + } + + // Headlines Flow + headlinesEntries(navigator) + + // Facts Flow + factsEntries(navigator) + + // Blocks Flow + blocksEntries(navigator) + + // Weather Flow + weatherEntries(navigator) + + // Price Flow + priceEntries(navigator) + + // Calculator Preview + entry { + CalculatorEntry( + navigator = navigator, + currencyViewModel = currencyViewModel, + ) + } +} + +private fun EntryProviderScope.headlinesEntries(navigator: Navigator) { + entry { + HeadlinesPreviewEntry(navigator) + } + + entry { + HeadlinesPreviewEntry(navigator) + } + + entry { + HeadlinesEditEntry(navigator) + } +} + +@Composable +private fun HeadlinesPreviewEntry( + navigator: Navigator, + viewModel: HeadlinesViewModel = hiltViewModel(), +) { + HeadlinesPreviewScreen( + headlinesViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.HeadlinesEdit) }, + ) +} + +@Composable +private fun HeadlinesEditEntry( + navigator: Navigator, + viewModel: HeadlinesViewModel = hiltViewModel(), +) { + HeadlinesEditScreen( + headlinesViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.HeadlinesPreview) }, + ) +} + +private fun EntryProviderScope.factsEntries(navigator: Navigator) { + entry { + FactsPreviewEntry(navigator) + } + + entry { + FactsPreviewEntry(navigator) + } + + entry { + FactsEditEntry(navigator) + } +} + +@Composable +private fun FactsPreviewEntry( + navigator: Navigator, + viewModel: FactsViewModel = hiltViewModel(), +) { + FactsPreviewScreen( + factsViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.FactsEdit) }, + ) +} + +@Composable +private fun FactsEditEntry( + navigator: Navigator, + viewModel: FactsViewModel = hiltViewModel(), +) { + FactsEditScreen( + factsViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.FactsPreview) }, + ) +} + +private fun EntryProviderScope.blocksEntries(navigator: Navigator) { + entry { + BlocksPreviewEntry(navigator) + } + + entry { + BlocksPreviewEntry(navigator) + } + + entry { + BlocksEditEntry(navigator) + } +} + +@Composable +private fun BlocksPreviewEntry( + navigator: Navigator, + viewModel: BlocksViewModel = hiltViewModel(), +) { + BlocksPreviewScreen( + blocksViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.BlocksEdit) }, + ) +} + +@Composable +private fun BlocksEditEntry( + navigator: Navigator, + viewModel: BlocksViewModel = hiltViewModel(), +) { + BlocksEditScreen( + blocksViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.BlocksPreview) }, + ) +} + +private fun EntryProviderScope.weatherEntries(navigator: Navigator) { + entry { + WeatherPreviewEntry(navigator) + } + + entry { + WeatherPreviewEntry(navigator) + } + + entry { + WeatherEditEntry(navigator) + } +} + +@Composable +private fun WeatherPreviewEntry( + navigator: Navigator, + viewModel: WeatherViewModel = hiltViewModel(), +) { + WeatherPreviewScreen( + weatherViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.WeatherEdit) }, + ) +} + +@Composable +private fun WeatherEditEntry( + navigator: Navigator, + viewModel: WeatherViewModel = hiltViewModel(), +) { + WeatherEditScreen( + weatherViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.WeatherPreview) }, + ) +} + +private fun EntryProviderScope.priceEntries(navigator: Navigator) { + entry { + PricePreviewEntry(navigator) + } + + entry { + PricePreviewEntry(navigator) + } + + entry { + PriceEditEntry(navigator) + } +} + +@Composable +private fun PricePreviewEntry( + navigator: Navigator, + viewModel: PriceViewModel = hiltViewModel(), +) { + PricePreviewScreen( + priceViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.PriceEdit) }, + ) +} + +@Composable +private fun PriceEditEntry( + navigator: Navigator, + priceViewModel: PriceViewModel = hiltViewModel(), +) { + PriceEditScreen( + viewModel = priceViewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.PricePreview) }, + ) +} + +@Composable +private fun CalculatorEntry( + navigator: Navigator, + currencyViewModel: CurrencyViewModel, + viewModel: CalculatorViewModel = hiltViewModel(), +) { + CalculatorPreviewScreen( + viewModel = viewModel, + currencyViewModel = currencyViewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + ) +} diff --git a/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt b/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt index 30b60c475..920a82d54 100644 --- a/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt @@ -135,7 +135,6 @@ private fun TermsText( } } - @Preview(showSystemUi = true) @Composable private fun TermsPreview() { diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index ee78f6339..8340a001e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale @@ -71,6 +70,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.SheetTopBar @@ -80,16 +80,25 @@ import to.bitkit.utils.Logger import to.bitkit.viewmodels.AppViewModel import java.util.concurrent.Executors -const val SCAN_REQUEST_KEY = "SCAN_REQUEST" -const val SCAN_RESULT_KEY = "SCAN_RESULT" - @OptIn(ExperimentalPermissionsApi::class) @Composable fun QrScanningScreen( - navController: NavController, - inSheet: Boolean = false, - onBack: () -> Unit = { navController.popBackStack() }, + navigator: Navigator, + onScanSuccess: (String) -> Unit, +) { + QrScanningScreenContent( + onBack = { navigator.goBack() }, + onScanSuccess = onScanSuccess, + inSheet = false, + ) +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun QrScanningScreenContent( + onBack: () -> Unit, onScanSuccess: (String) -> Unit, + inSheet: Boolean, ) { val app = appViewModel ?: return @@ -99,18 +108,8 @@ fun QrScanningScreen( LaunchedEffect(scanResult) { scanResult?.let { qrCode -> delay(100) // wait to prevent navigation result race conditions - - val prev = navController.previousBackStackEntry - val wasCalledForResult = prev?.savedStateHandle?.contains(SCAN_REQUEST_KEY) == true - if (wasCalledForResult) { - prev.savedStateHandle[SCAN_RESULT_KEY] = qrCode - onBack() - prev.savedStateHandle.remove(SCAN_REQUEST_KEY) - } else { - onBack() - onScanSuccess(qrCode) - } - + onBack() + onScanSuccess(qrCode) // Reset scan result to allow new scans setScanResult(null) } 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 8e0fd9659..98dac2553 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 @@ -10,17 +10,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel 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.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -30,7 +30,7 @@ import to.bitkit.viewmodels.DevSettingsViewModel @Composable fun DevSettingsScreen( - navController: NavController, + navigator: Navigator, viewModel: DevSettingsViewModel = hiltViewModel(), ) { val app = appViewModel ?: return @@ -41,7 +41,7 @@ fun DevSettingsScreen( ScreenColumn { AppTopBar( titleText = stringResource(R.string.settings__dev_title), - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, actions = { DrawerNavIcon() }, ) Column( @@ -49,12 +49,12 @@ fun DevSettingsScreen( .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) ) { - SettingsButtonRow("Fee Settings") { navController.navigate(Routes.FeeSettings) } - SettingsButtonRow("Channel Orders") { navController.navigate(Routes.ChannelOrdersSettings) } - SettingsButtonRow("LDK Debug") { navController.navigate(Routes.LdkDebug) } + SettingsButtonRow("Fee Settings") { navigator.navigate(Routes.FeeSettings) } + SettingsButtonRow("Channel Orders") { navigator.navigate(Routes.ChannelOrdersSettings) } + SettingsButtonRow("LDK Debug") { navigator.navigate(Routes.LdkDebug) } SectionHeader("LOGS") - SettingsButtonRow("Logs") { navController.navigate(Routes.Logs) } + SettingsButtonRow("Logs") { navigator.navigate(Routes.Logs) } SettingsTextButtonRow( title = "Export Logs", onClick = { @@ -65,7 +65,7 @@ fun DevSettingsScreen( if (Env.network == Network.REGTEST) { SectionHeader("REGTEST") - SettingsButtonRow("Blocktank Regtest") { navController.navigate(Routes.RegtestSettings) } + SettingsButtonRow("Blocktank Regtest") { navigator.navigate(Routes.RegtestSettings) } } SectionHeader("APP CACHE") diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt index 34cb37e89..f26cf5a58 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt @@ -13,11 +13,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.synonym.bitkitcore.FeeRates import to.bitkit.R import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsTextButtonRow +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -27,7 +27,7 @@ import to.bitkit.viewmodels.FeeSettingsViewModel @Composable fun FeeSettingsScreen( - navController: NavController, + navigator: Navigator, viewModel: FeeSettingsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -38,7 +38,7 @@ fun FeeSettingsScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt index ba3808df1..124cc6473 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.synonym.vssclient.KeyVersion import to.bitkit.R import to.bitkit.env.Env @@ -41,6 +40,7 @@ import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.settings.SectionFooter import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsTextButtonRow +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -54,14 +54,14 @@ import java.io.File @Composable fun LdkDebugScreen( - navController: NavController, + navigator: Navigator, viewModel: LdkDebugViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LdkDebugContent( uiState = uiState, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, onNodeUriChange = viewModel::updateNodeUri, onAddPeer = viewModel::addPeer, onPasteAndAddPeer = viewModel::pasteAndAddPeer, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index c67e05c49..adb3c20b6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -30,7 +30,6 @@ import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton -import to.bitkit.ui.components.Sheet import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +39,6 @@ import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.removeAccentTags import to.bitkit.ui.utils.withAccent import to.bitkit.ui.utils.withAccentBoldBright -import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel @@ -48,10 +46,10 @@ enum class SavingsProgressState { PROGRESS, SUCCESS, INTERRUPTED } @Composable fun SavingsProgressScreen( - app: AppViewModel, transfer: TransferViewModel, wallet: WalletViewModel, onContinueClick: () -> Unit = {}, + onForceTransfer: () -> Unit = {}, ) { val window = LocalActivity.current?.window var progressState by remember { mutableStateOf(SavingsProgressState.PROGRESS) } @@ -69,7 +67,7 @@ fun SavingsProgressScreen( progressState = SavingsProgressState.SUCCESS } else { transfer.startCoopCloseRetries(channelsFailedToCoopClose) { - app.showSheet(Sheet.ForceTransfer) + onForceTransfer() } delay(2500) progressState = SavingsProgressState.INTERRUPTED diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt index 5d45f2430..3c8e0bace 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt @@ -31,15 +31,12 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.SavedStateHandle -import kotlinx.coroutines.flow.filterNotNull import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.ext.from import to.bitkit.ext.getClipboardText import to.bitkit.ext.host import to.bitkit.ext.port -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up @@ -50,7 +47,6 @@ import to.bitkit.ui.components.TextInput import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -58,8 +54,24 @@ import to.bitkit.ui.utils.withAccent @Composable fun ExternalConnectionScreen( - route: Routes.ExternalConnection, - savedStateHandle: SavedStateHandle, + scannedNodeUri: String?, + viewModel: ExternalNodeViewModel, + onNodeConnected: () -> Unit, + onScanClick: () -> Unit, + onBackClick: () -> Unit, +) { + ExternalConnectionScreenContent( + scannedNodeUri = scannedNodeUri, + viewModel = viewModel, + onNodeConnected = onNodeConnected, + onScanClick = onScanClick, + onBackClick = onBackClick, + ) +} + +@Composable +private fun ExternalConnectionScreenContent( + scannedNodeUri: String?, viewModel: ExternalNodeViewModel, onNodeConnected: () -> Unit, onScanClick: () -> Unit, @@ -68,23 +80,13 @@ fun ExternalConnectionScreen( val context = LocalContext.current val uiState by viewModel.uiState.collectAsState() - // Handle result from scanner opened from home - LaunchedEffect(route.scannedNodeUri) { - if (route.scannedNodeUri != null) { - viewModel.parseNodeUri(route.scannedNodeUri) + // Handle scanned node URI + LaunchedEffect(scannedNodeUri) { + if (scannedNodeUri != null) { + viewModel.parseNodeUri(scannedNodeUri) } } - // Handle result from scanner opened from this screen - LaunchedEffect(savedStateHandle) { - savedStateHandle.getStateFlow(SCAN_RESULT_KEY, null) - .filterNotNull() - .collect { scannedData -> - viewModel.parseNodeUri(scannedData) - savedStateHandle.remove(SCAN_RESULT_KEY) - } - } - LaunchedEffect(viewModel, onNodeConnected) { viewModel.effects.collect { when (it) { diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt index 78105bfa3..39998c3a7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt @@ -26,7 +26,6 @@ import to.bitkit.R import to.bitkit.env.Peers import to.bitkit.ext.host import to.bitkit.ext.port -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB @@ -45,16 +44,39 @@ import to.bitkit.ui.utils.withAccent @Composable fun LnurlChannelScreen( - route: Routes.LnurlChannel, + uri: String, + callback: String, + k1: String, onConnected: () -> Unit, onBack: () -> Unit, onClose: () -> Unit, viewModel: LnurlChannelViewModel = hiltViewModel(), +) { + LnurlChannelScreenContent( + uri = uri, + callback = callback, + k1 = k1, + onConnected = onConnected, + onBack = onBack, + onClose = onClose, + viewModel = viewModel, + ) +} + +@Composable +private fun LnurlChannelScreenContent( + uri: String, + callback: String, + k1: String, + onConnected: () -> Unit, + onBack: () -> Unit, + onClose: () -> Unit, + viewModel: LnurlChannelViewModel, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.init(route) + viewModel.init(uri, callback, k1) } LaunchedEffect(uiState.isConnected) { diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 21891070d..91176127f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -14,7 +14,6 @@ import to.bitkit.R import to.bitkit.ext.parse import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo -import to.bitkit.ui.Routes import to.bitkit.ui.shared.toast.ToastEventBus import javax.inject.Inject @@ -27,16 +26,20 @@ class LnurlChannelViewModel @Inject constructor( private val _uiState = MutableStateFlow(LnurlChannelUiState()) val uiState = _uiState.asStateFlow() - private lateinit var params: Routes.LnurlChannel + private lateinit var uri: String + private lateinit var callback: String + private lateinit var k1: String - fun init(route: Routes.LnurlChannel) { - this.params = route + fun init(uri: String, callback: String, k1: String) { + this.uri = uri + this.callback = callback + this.k1 = k1 fetchChannelInfo() } private fun fetchChannelInfo() { viewModelScope.launch { - lightningRepo.fetchLnurlChannelInfo(params.uri) + lightningRepo.fetchLnurlChannelInfo(uri) .onSuccess { channelInfo -> val peer = runCatching { PeerDetails.parse(channelInfo.uri) }.getOrElse { errorToast(it) @@ -66,7 +69,7 @@ class LnurlChannelViewModel @Inject constructor( // Connect to peer if not connected lightningRepo.connectPeer(peer) - lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) + lightningRepo.requestLnurlChannel(callback = callback, k1 = k1, nodeId = nodeId) .onSuccess { ToastEventBus.send( type = Toast.ToastType.SUCCESS, 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 5e8a6c64d..3500abb3c 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 @@ -26,7 +26,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material3.DrawerState -import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -36,7 +35,6 @@ import androidx.compose.material3.VerticalDivider import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -56,16 +54,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import com.synonym.bitkitcore.Activity import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.rememberHazeState -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.env.Env @@ -74,13 +68,11 @@ import to.bitkit.models.BalanceState import to.bitkit.models.Suggestion import to.bitkit.models.WidgetType import to.bitkit.ui.LocalBalances -import to.bitkit.ui.Routes import to.bitkit.ui.components.ActivityBanner import to.bitkit.ui.components.AppStatus import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.EmptyStateView import to.bitkit.ui.components.HorizontalSpacer -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.StatusBarSpacer import to.bitkit.ui.components.SuggestionCard import to.bitkit.ui.components.TabBar @@ -91,10 +83,8 @@ 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.navigateToActivityItem -import to.bitkit.ui.navigateToAllActivity -import to.bitkit.ui.navigateToTransferFunding -import to.bitkit.ui.navigateToTransferIntro +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.screens.wallets.activity.components.ActivityListSimple import to.bitkit.ui.screens.wallets.activity.utils.previewActivityItems @@ -108,8 +98,6 @@ import to.bitkit.ui.screens.widgets.price.PriceCard import to.bitkit.ui.screens.widgets.weather.WeatherCard import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.shareText -import to.bitkit.ui.sheets.BackupRoute -import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -123,8 +111,7 @@ import to.bitkit.viewmodels.WalletViewModel fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, - rootNavController: NavController, - walletNavController: NavHostController, + navigator: Navigator, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, @@ -156,12 +143,11 @@ fun HomeScreen( DeleteWidgetAlert(type, homeViewModel) } + val scope = rememberCoroutineScope() + Content( mainUiState = mainUiState, homeUiState = homeUiState, - rootNavController = rootNavController, - walletNavController = walletNavController, - drawerState = drawerState, latestActivities = latestActivities, onRefresh = { activityListViewModel.resync() @@ -170,9 +156,9 @@ fun HomeScreen( }, onClickProfile = { if (!hasSeenProfileIntro) { - rootNavController.navigate(Routes.ProfileIntro) + navigator.navigate(Routes.ProfileIntro) } else { - rootNavController.navigate(Routes.CreateProfile) + navigator.navigate(Routes.CreateProfile) } }, onRemoveSuggestion = { suggestion -> @@ -181,27 +167,27 @@ fun HomeScreen( onClickSuggestion = { suggestion -> when (suggestion) { Suggestion.BUY -> { - rootNavController.navigate(Routes.BuyIntro) + navigator.navigate(Routes.BuyIntro) } Suggestion.LIGHTNING -> { if (!hasSeenTransferIntro) { - rootNavController.navigateToTransferIntro() + navigator.navigate(Routes.TransferIntro) } else { - rootNavController.navigateToTransferFunding() + navigator.navigate(Routes.Funding) } } Suggestion.BACK_UP -> { - appViewModel.showSheet(Sheet.Backup(BackupRoute.Intro)) + navigator.navigate(Routes.BackupIntro) } Suggestion.SECURE -> { - appViewModel.showSheet(Sheet.Pin(PinRoute.Prompt(showLaterButton = true))) + navigator.navigate(Routes.PinPrompt(showLaterButton = true)) } Suggestion.SUPPORT -> { - rootNavController.navigate(Routes.Support) + navigator.navigate(Routes.Support) } Suggestion.INVITE -> { @@ -215,54 +201,54 @@ fun HomeScreen( Suggestion.PROFILE -> { if (!hasSeenProfileIntro) { - rootNavController.navigate(Routes.ProfileIntro) + navigator.navigate(Routes.ProfileIntro) } else { - rootNavController.navigate(Routes.CreateProfile) + navigator.navigate(Routes.CreateProfile) } } Suggestion.SHOP -> { if (!hasSeenShopIntro) { - rootNavController.navigate(Routes.ShopIntro) + navigator.navigate(Routes.ShopIntro) } else { - rootNavController.navigate(Routes.ShopDiscover) + navigator.navigate(Routes.ShopDiscover) } } Suggestion.QUICK_PAY -> { if (!quickPayIntroSeen) { - rootNavController.navigate(Routes.QuickPayIntro) + navigator.navigate(Routes.QuickPayIntro) } else { - rootNavController.navigate(Routes.QuickPaySettings) + navigator.navigateToQuickPaySettings() } } Suggestion.NOTIFICATIONS -> { if (bgPaymentsIntroSeen) { - rootNavController.navigate(Routes.BackgroundPaymentsSettings) + navigator.navigate(Routes.BackgroundPaymentsSettings) } else { - rootNavController.navigate(Routes.BackgroundPaymentsIntro) + navigator.navigate(Routes.BackgroundPaymentsIntro) } } } }, onClickAddWidget = { if (!hasSeenWidgetsIntro) { - rootNavController.navigate(Routes.WidgetsIntro) + navigator.navigate(Routes.WidgetsIntro) } else { - rootNavController.navigate(Routes.AddWidget) + navigator.navigate(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 -> navigator.navigate(Routes.BlocksPreview) + WidgetType.CALCULATOR -> navigator.navigate(Routes.CalculatorPreview) + WidgetType.FACTS -> navigator.navigate(Routes.FactsPreview) + WidgetType.NEWS -> navigator.navigate(Routes.HeadlinesPreview) + WidgetType.PRICE -> navigator.navigate(Routes.PricePreview) + WidgetType.WEATHER -> navigator.navigate(Routes.WeatherPreview) } }, onClickDeleteWidget = { widgetType -> @@ -272,7 +258,14 @@ fun HomeScreen( homeViewModel.moveWidget(fromIndex, toIndex) }, onDismissEmptyState = homeViewModel::dismissEmptyState, - onClickEmptyActivityRow = { appViewModel.showSheet(Sheet.Receive) }, + onClickEmptyActivityRow = { navigator.navigate(Routes.ReceiveQr) }, + onClickSavings = { navigator.navigate(Routes.Savings) }, + onClickSpending = { navigator.navigate(Routes.Spending) }, + onAllActivityClick = { navigator.navigate(Routes.AllActivity) }, + onActivityItemClick = { navigator.navigate(Routes.ActivityDetail(it)) }, + onClickSettingUp = { navigator.navigate(Routes.SettingUp) }, + onClickAppStatus = { navigator.navigate(Routes.AppStatus) }, + onOpenDrawer = { scope.launch { drawerState.open() } }, ) } @@ -281,9 +274,6 @@ fun HomeScreen( private fun Content( mainUiState: MainUiState, homeUiState: HomeUiState, - rootNavController: NavController, - walletNavController: NavController, - drawerState: DrawerState, hazeState: HazeState = rememberHazeState(), latestActivities: List?, onClickProfile: () -> Unit = {}, @@ -297,6 +287,13 @@ private fun Content( onMoveWidget: (Int, Int) -> Unit = { _, _ -> }, onDismissEmptyState: () -> Unit = {}, onClickEmptyActivityRow: () -> Unit = {}, + onClickSavings: () -> Unit = {}, + onClickSpending: () -> Unit = {}, + onAllActivityClick: () -> Unit = {}, + onActivityItemClick: (Activity) -> Unit = {}, + onClickSettingUp: () -> Unit = {}, + onClickAppStatus: () -> Unit = {}, + onOpenDrawer: () -> Unit = {}, balances: BalanceState = LocalBalances.current, ) { val scope = rememberCoroutineScope() @@ -306,9 +303,8 @@ private fun Content( TopBar( hazeState = hazeState, onClickProfile = onClickProfile, - rootNavController = rootNavController, - scope = scope, - drawerState = drawerState, + onClickAppStatus = onClickAppStatus, + onOpenDrawer = onOpenDrawer, ) val pullToRefreshState = rememberPullToRefreshState() PullToRefreshBox( @@ -359,7 +355,7 @@ private fun Content( sats = balances.totalOnchainSats.toLong(), icon = painterResource(id = R.drawable.ic_btc_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Savings) } + .clickableAlpha { onClickSavings() } .padding(vertical = 4.dp) .testTag("ActivitySavings") ) @@ -370,7 +366,7 @@ private fun Content( sats = balances.totalLightningSats.toLong(), icon = painterResource(id = R.drawable.ic_ln_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Spending) } + .clickableAlpha { onClickSpending() } .padding(vertical = 4.dp) .testTag("ActivitySpending") ) @@ -490,7 +486,7 @@ private fun Content( icon = banner.icon, onClick = { when (banner) { - ActivityBannerType.SPENDING -> rootNavController.navigate(Routes.SettingUp) + ActivityBannerType.SPENDING -> onClickSettingUp() ActivityBannerType.SAVINGS -> Unit } }, @@ -502,8 +498,8 @@ private fun Content( ActivityListSimple( items = latestActivities, - onAllActivityClick = { rootNavController.navigateToAllActivity() }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, + onAllActivityClick = onAllActivityClick, + onActivityItemClick = onActivityItemClick, onEmptyActivityRowClick = onClickEmptyActivityRow, ) @@ -625,9 +621,8 @@ private fun Widgets(homeUiState: HomeUiState) { private fun TopBar( hazeState: HazeState, onClickProfile: () -> Unit, - rootNavController: NavController, - scope: CoroutineScope, - drawerState: DrawerState, + onClickAppStatus: () -> Unit, + onOpenDrawer: () -> Unit, ) { val topbarGradient = Brush.verticalGradient( colorStops = arrayOf( @@ -667,10 +662,10 @@ private fun TopBar( } }, actions = { - AppStatus(onClick = { rootNavController.navigate(Routes.AppStatus) }) + AppStatus(onClick = onClickAppStatus) HorizontalSpacer(4.dp) IconButton( - onClick = { scope.launch { drawerState.open() } }, + onClick = onOpenDrawer, modifier = Modifier.testTag("HeaderMenu") ) { Icon( @@ -713,9 +708,6 @@ private fun Preview() { homeUiState = HomeUiState( showWidgets = true, ), - rootNavController = rememberNavController(), - walletNavController = rememberNavController(), - drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), latestActivities = previewActivityItems.take(3), balances = BalanceState( totalOnchainSats = 165_000u, @@ -737,9 +729,6 @@ private fun PreviewEmpty() { homeUiState = HomeUiState( showEmptyState = true, ), - rootNavController = rememberNavController(), - walletNavController = rememberNavController(), - drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), latestActivities = previewActivityItems.take(3), balances = BalanceState() ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt index d071dfe6d..00f5b098c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt @@ -49,7 +49,7 @@ fun SavingsWalletScreen( onchainActivities: List, onAllActivityButtonClick: () -> Unit, onEmptyActivityRowClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onTransferToSpendingClick: () -> Unit, onBackClick: () -> Unit, balances: BalanceState = LocalBalances.current, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt index 82460dca1..a9b9997b6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt @@ -50,7 +50,7 @@ fun SpendingWalletScreen( uiState: MainUiState, lightningActivities: List, onAllActivityButtonClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, onTransferToSavingsClick: () -> Unit, onBackClick: () -> Unit, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 06afb3e21..6f8d733f2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -59,7 +59,6 @@ import to.bitkit.ext.toActivityItemTime import to.bitkit.ext.totalValue import to.bitkit.models.FeeRate import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BalanceHeaderView @@ -71,6 +70,8 @@ import to.bitkit.ui.components.MoneySSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.Title +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.screens.wallets.activity.components.ActivityAddTagSheet @@ -85,12 +86,30 @@ import to.bitkit.ui.utils.getScreenTitleRes import to.bitkit.viewmodels.ActivityDetailViewModel import to.bitkit.viewmodels.ActivityListViewModel -@Suppress("CyclomaticComplexMethod") @Composable fun ActivityDetailScreen( + navigator: Navigator, + activityId: String, listViewModel: ActivityListViewModel, detailViewModel: ActivityDetailViewModel = hiltViewModel(), - route: Routes.ActivityDetail, +) { + ActivityDetailScreenContent( + activityId = activityId, + listViewModel = listViewModel, + detailViewModel = detailViewModel, + onExploreClick = { id -> navigator.navigate(Routes.ActivityExplore(id)) }, + onBackClick = { navigator.goBack() }, + onCloseClick = { navigator.navigateToHome() }, + onChannelClick = { navigator.navigate(Routes.ChannelDetail) }, + ) +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun ActivityDetailScreenContent( + activityId: String, + listViewModel: ActivityListViewModel, + detailViewModel: ActivityDetailViewModel, onExploreClick: (String) -> Unit, onBackClick: () -> Unit, onCloseClick: () -> Unit, @@ -99,8 +118,8 @@ fun ActivityDetailScreen( val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() // Load activity on composition - LaunchedEffect(route.id) { - detailViewModel.loadActivity(route.id) + LaunchedEffect(activityId) { + detailViewModel.loadActivity(activityId) } // Clear state on disposal diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 7ac978f5f..31f337968 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -46,12 +46,12 @@ import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -67,15 +67,28 @@ import to.bitkit.viewmodels.ActivityDetailViewModel @Composable fun ActivityExploreScreen( + navigator: Navigator, + activityId: String, detailViewModel: ActivityDetailViewModel = hiltViewModel(), - route: Routes.ActivityExplore, +) { + ActivityExploreScreenContent( + activityId = activityId, + detailViewModel = detailViewModel, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ActivityExploreScreenContent( + activityId: String, + detailViewModel: ActivityDetailViewModel, onBackClick: () -> Unit, ) { val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() // Load activity on composition - LaunchedEffect(route.id) { - detailViewModel.loadActivity(route.id) + LaunchedEffect(activityId) { + detailViewModel.loadActivity(activityId) } // Clear state on disposal diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt index a6558b341..e90e6e1cc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt @@ -20,8 +20,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.Activity import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import to.bitkit.R -import to.bitkit.ui.appViewModel -import to.bitkit.ui.components.Sheet import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.screens.wallets.activity.components.ActivityListFilter @@ -38,9 +36,11 @@ import to.bitkit.viewmodels.ActivityListViewModel fun AllActivityScreen( viewModel: ActivityListViewModel, onBack: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, + onTagClick: () -> Unit, + onDateRangeClick: () -> Unit, + onEmptyActivityRowClick: () -> Unit, ) { - val app = appViewModel ?: return val filteredActivities by viewModel.filteredActivities.collectAsStateWithLifecycle() val searchText by viewModel.searchText.collectAsStateWithLifecycle() @@ -65,10 +65,10 @@ fun AllActivityScreen( onRemoveTag = { viewModel.toggleTag(it) }, onTabChange = { viewModel.setTab(tabs[it]) }, onBackClick = onBack, - onTagClick = { app.showSheet(Sheet.ActivityTagSelector) }, - onDateRangeClick = { app.showSheet(Sheet.ActivityDateRangeSelector) }, + onTagClick = onTagClick, + onDateRangeClick = onDateRangeClick, onActivityItemClick = onActivityItemClick, - onEmptyActivityRowClick = { app.showSheet(Sheet.Receive) }, + onEmptyActivityRowClick = onEmptyActivityRowClick, ) } @@ -88,7 +88,7 @@ private fun AllActivityScreenContent( onBackClick: () -> Unit, onTagClick: () -> Unit, onDateRangeClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, ) { Column( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt index 6c47947fa..daf73414b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt @@ -119,7 +119,7 @@ fun DateRangeSelectorSheet() { app.hideSheet() } - Content( + DateRangeSelectorContent( initialStartDate = startDate, initialEndDate = endDate, onClearClick = { @@ -137,7 +137,7 @@ fun DateRangeSelectorSheet() { @Suppress("MaxLineLength", "CyclomaticComplexMethod") @Composable -private fun Content( +internal fun DateRangeSelectorContent( initialStartDate: Long? = null, initialEndDate: Long? = null, onClearClick: () -> Unit = {}, @@ -638,7 +638,7 @@ private fun LocalDate.toFormattedString(): String { private fun PreviewEmpty() { AppThemeSurface { BottomSheetPreview { - Content() + DateRangeSelectorContent() } } } @@ -648,7 +648,7 @@ private fun PreviewEmpty() { private fun PreviewWithSelection() { AppThemeSurface { BottomSheetPreview { - Content( + DateRangeSelectorContent( initialStartDate = now() .minus(CalendarConstants.DAYS_IN_WEEK.days) .toEpochMilliseconds(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt index 3d178f85a..f99371b35 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt @@ -42,7 +42,7 @@ fun TagSelectorSheet() { activity.updateAvailableTags() } - Content( + TagSelectorContent( availableTags = availableTags, selectedTags = selectedTags, onTagClick = { @@ -53,7 +53,7 @@ fun TagSelectorSheet() { } @Composable -private fun Content( +internal fun TagSelectorContent( availableTags: List, selectedTags: Set, onTagClick: (String) -> Unit = {}, @@ -102,7 +102,7 @@ private fun Content( private fun Preview() { AppThemeSurface { BottomSheetPreview { - Content( + TagSelectorContent( availableTags = listOf("Bitcoin", "Lightning", "Sent", "Received"), selectedTags = setOf("Bitcoin", "Received"), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt index 3fabceb3a..ec982142c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt @@ -37,7 +37,7 @@ import java.util.Locale @Composable fun ActivityListGrouped( items: List?, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, modifier: Modifier = Modifier, showFooter: Boolean = false, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt index cc476b42a..cf6768700 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt @@ -22,7 +22,7 @@ import to.bitkit.ui.theme.AppThemeSurface fun ActivityListSimple( items: List?, onAllActivityClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, ) { Column( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt index b7c9d7638..69bc6fef0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt @@ -33,7 +33,6 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.formatted import to.bitkit.ext.isSent import to.bitkit.ext.isTransfer -import to.bitkit.ext.rawId import to.bitkit.ext.timestamp import to.bitkit.ext.totalValue import to.bitkit.ext.txType @@ -61,7 +60,7 @@ import java.time.ZoneId @Composable fun ActivityRow( item: Activity, - onClick: (String) -> Unit, + onClick: (Activity) -> Unit, testTag: String, ) { val blocktankInfo by blocktankViewModel?.info?.collectAsStateWithLifecycle() ?: remember { @@ -99,7 +98,7 @@ fun ActivityRow( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickableAlpha { onClick(item.rawId()) } + .clickableAlpha { onClick(item) } .background(color = Colors.Gray6, shape = Shapes.medium) .padding(16.dp) .testTag(testTag) 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 deleted file mode 100644 index 3cee808dc..000000000 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ /dev/null @@ -1,217 +0,0 @@ -package to.bitkit.ui.screens.wallets.receive - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable -import to.bitkit.repositories.LightningState -import to.bitkit.ui.screens.wallets.send.AddTagScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.NotificationUtils -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.ui.walletViewModel -import to.bitkit.viewmodels.AmountInputViewModel -import to.bitkit.viewmodels.MainUiState -import to.bitkit.viewmodels.SettingsViewModel - -@Composable -fun ReceiveSheet( - navigateToExternalConnection: () -> Unit, - walletState: MainUiState, - editInvoiceAmountViewModel: AmountInputViewModel = hiltViewModel(), - settingsViewModel: SettingsViewModel = hiltViewModel(), -) { - val wallet = requireNotNull(walletViewModel) - val navController = rememberNavController() - - LaunchedEffect(Unit) { editInvoiceAmountViewModel.clearInput() } - - val cjitInvoice = remember { mutableStateOf(null) } - val showCreateCjit = remember { mutableStateOf(false) } - val cjitEntryDetails = remember { mutableStateOf(null) } - val lightningState: LightningState by wallet.lightningState.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { - wallet.resetPreActivityMetadataTagsForCurrentInvoice() - wallet.refreshReceiveState() - } - - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("ReceiveScreen") - ) { - NavHost( - navController = navController, - startDestination = ReceiveRoute.QR, - ) { - composableWithDefaultTransitions { - LaunchedEffect(cjitInvoice.value) { - showCreateCjit.value = !cjitInvoice.value.isNullOrBlank() - } - - ReceiveQrScreen( - cjitInvoice = cjitInvoice.value, - walletState = walletState, - onClickReceiveCjit = { - if (lightningState.isGeoBlocked) { - navController.navigate(ReceiveRoute.GeoBlock) - } else { - showCreateCjit.value = true - navController.navigate(ReceiveRoute.Amount) - } - }, - onClickEditInvoice = { navController.navigate(ReceiveRoute.EditInvoice) }, - ) - } - composableWithDefaultTransitions { - ReceiveAmountScreen( - onCjitCreated = { entry -> - cjitEntryDetails.value = entry - navController.navigate(ReceiveRoute.Confirm) - }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - LocationBlockScreen( - onBackPressed = { navController.popBackStack() }, - navigateAdvancedSetup = navigateToExternalConnection, - ) - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - ReceiveConfirmScreen( - entry = entryDetails, - onLearnMore = { navController.navigate(ReceiveRoute.Liquidity) }, - onContinue = { invoice -> - cjitInvoice.value = invoice - navController.navigate(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } - }, - onBack = { navController.popBackStack() }, - ) - } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - ReceiveConfirmScreen( - entry = entryDetails, - onLearnMore = { navController.navigate(ReceiveRoute.LiquidityAdditional) }, - onContinue = { invoice -> - cjitInvoice.value = invoice - navController.navigate(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } - }, - isAdditional = true, - onBack = { navController.popBackStack() }, - ) - } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - val context = LocalContext.current - val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - - ReceiveLiquidityScreen( - entry = entryDetails, - onContinue = { navController.popBackStack() }, - onBack = { navController.popBackStack() }, - hasNotificationPermission = notificationsGranted, - onSwitchClick = { - NotificationUtils.openNotificationSettings(context) - }, - ) - } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - val context = LocalContext.current - val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - - ReceiveLiquidityScreen( - entry = entryDetails, - onContinue = { navController.popBackStack() }, - isAdditional = true, - onBack = { navController.popBackStack() }, - hasNotificationPermission = notificationsGranted, - onSwitchClick = { - NotificationUtils.openNotificationSettings(context) - }, - ) - } - } - composableWithDefaultTransitions { - val walletUiState by wallet.walletState.collectAsStateWithLifecycle() - @Suppress("ViewModelForwarding") - EditInvoiceScreen( - amountInputViewModel = editInvoiceAmountViewModel, - walletUiState = walletUiState, - onBack = { navController.popBackStack() }, - updateInvoice = wallet::updateBip21Invoice, - onClickAddTag = { navController.navigate(ReceiveRoute.AddTag) }, - onClickTag = wallet::removeTag, - onDescriptionUpdate = wallet::updateBip21Description, - navigateReceiveConfirm = { entry -> - cjitEntryDetails.value = entry - navController.navigate(ReceiveRoute.ConfirmIncreaseInbound) - } - ) - } - composableWithDefaultTransitions { - AddTagScreen( - onBack = { - navController.popBackStack() - }, - onTagSelected = { tag -> - wallet.addTagToSelected(tag) - navController.popBackStack() - }, - tqgInputTestTag = "TagInputReceive", - addButtonTestTag = "ReceiveTagsSubmit", - ) - } - } - } -} - -sealed interface ReceiveRoute { - @Serializable - data object QR : ReceiveRoute - - @Serializable - data object Amount : ReceiveRoute - - @Serializable - data object Confirm : ReceiveRoute - - @Serializable - data object ConfirmIncreaseInbound : ReceiveRoute - - @Serializable - data object Liquidity : ReceiveRoute - - @Serializable - data object LiquidityAdditional : ReceiveRoute - - @Serializable - data object EditInvoice : ReceiveRoute - - @Serializable - data object AddTag : ReceiveRoute - - @Serializable - data object GeoBlock : ReceiveRoute -} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt index 1de166d58..b36682c18 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt @@ -37,7 +37,7 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.viewmodels.AddTagUiState import to.bitkit.viewmodels.TagsViewModel @@ -81,7 +81,7 @@ fun AddTagContent( val focusRequester = remember { FocusRequester() } LaunchedEffect(focusOnShow) { if (focusOnShow) { - delay(TRANSITION_SCREEN_MS) + delay(MS_TRANSITION_SCREEN) focusRequester.requestFocus() } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt index 7ced344e8..c40f3055c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt @@ -33,7 +33,7 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.viewmodels.SendEvent import to.bitkit.viewmodels.SendUiState @@ -46,7 +46,7 @@ fun SendAddressScreen( ) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { - delay(TRANSITION_SCREEN_MS) + delay(MS_TRANSITION_SCREEN) focusRequester.requestFocus() } Column( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 640e47aa8..cd50ed9bb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -75,7 +75,7 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.ui.utils.withAccent import to.bitkit.utils.Logger import to.bitkit.viewmodels.SendEvent @@ -150,7 +150,7 @@ fun SendRecipientScreen( LaunchedEffect(cameraPermissionState.status, isCameraInitialized) { if (cameraPermissionState.status.isGranted && !isCameraInitialized) { runCatching { - delay(TRANSITION_SCREEN_MS) + delay(MS_TRANSITION_SCREEN) imageAnalysis.setAnalyzer(executor, analyzer) val cameraProvider = withContext(Dispatchers.IO) { 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 1bd2beb90..d08353774 100644 --- a/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt @@ -16,13 +16,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow -import to.bitkit.ui.navigateToHome +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -31,37 +30,37 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun AdvancedSettingsScreen( - navController: NavController, + navigator: Navigator, viewModel: AdvancedSettingsViewModel = hiltViewModel(), ) { var showResetSuggestionsDialog by remember { mutableStateOf(false) } Content( showResetSuggestionsDialog = showResetSuggestionsDialog, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onCoinSelectionClick = { - navController.navigate(Routes.CoinSelectPreference) + navigator.navigate(Routes.CoinSelectPreference) }, onLightningConnectionsClick = { - navController.navigate(Routes.LightningConnections) + navigator.navigate(Routes.LightningConnections) }, onLightningNodeClick = { - navController.navigate(Routes.NodeInfo) + navigator.navigate(Routes.NodeInfo) }, onElectrumServerClick = { - navController.navigate(Routes.ElectrumConfig) + navigator.navigate(Routes.ElectrumConfig) }, onRgsServerClick = { - navController.navigate(Routes.RgsServer) + navigator.navigate(Routes.RgsServer) }, onAddressViewerClick = { - navController.navigate(Routes.AddressViewer) + navigator.navigate(Routes.AddressViewer) }, onSuggestionsResetClick = { showResetSuggestionsDialog = true }, onResetSuggestionsDialogConfirm = { viewModel.resetSuggestions() showResetSuggestionsDialog = false - navController.navigateToHome() + navigator.navigateToHome() }, onResetSuggestionsDialogCancel = { showResetSuggestionsDialog = false }, ) 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 558d127f2..0718a38b1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt @@ -25,24 +25,21 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.toRelativeTimeString import to.bitkit.models.BackupCategory import to.bitkit.models.BackupItemStatus -import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.backupsViewModel import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB 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.navigateToAuthCheck +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -57,9 +54,8 @@ import kotlin.time.ExperimentalTime @Composable fun BackupSettingsScreen( - navController: NavController, + navigator: Navigator, ) { - val app = appViewModel ?: return val settings = settingsViewModel ?: return val viewModel = backupsViewModel ?: return @@ -68,16 +64,16 @@ fun BackupSettingsScreen( BackupSettingsScreenContent( uiState = uiState, - onBackupClick = { app.showSheet(Sheet.Backup()) }, + onBackupClick = { navigator.navigate(Routes.BackupIntro) }, onResetAndRestoreClick = { if (isPinEnabled) { - navController.navigateToAuthCheck(onSuccessActionId = AuthCheckAction.NAV_TO_RESET) + navigator.navigate(Routes.AuthCheck(onSuccessActionId = AuthCheckAction.NAV_TO_RESET)) } else { - navController.navigate(Routes.ResetAndRestoreSettings) + navigator.navigate(Routes.ResetAndRestoreSettings) } }, onRetryBackup = { category -> viewModel.retryBackup(category) }, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index 47778d650..60ca7cba0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast @@ -35,6 +34,7 @@ import to.bitkit.ui.components.Caption import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -44,8 +44,19 @@ import to.bitkit.utils.Logger @Composable fun BlocktankRegtestScreen( - navController: NavController, + navigator: Navigator, viewModel: BlocktankRegtestViewModel = hiltViewModel(), +) { + BlocktankRegtestContent( + onBack = { navigator.goBack() }, + viewModel = viewModel, + ) +} + +@Composable +private fun BlocktankRegtestContent( + onBack: () -> Unit, + viewModel: BlocktankRegtestViewModel, ) { val coroutineScope = rememberCoroutineScope() val wallet = walletViewModel ?: return @@ -55,7 +66,7 @@ fun BlocktankRegtestScreen( ScreenColumn { AppTopBar( titleText = "Blocktank Regtest", - onBackClick = { navController.popBackStack() }, + onBackClick = onBack, actions = { DrawerNavIcon() }, ) Column( 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 ed5acd4ed..cb3f345e6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt @@ -42,7 +42,6 @@ import com.synonym.bitkitcore.ILspNode import com.synonym.bitkitcore.IcJitEntry import kotlinx.coroutines.launch import to.bitkit.models.formatToModernDisplay -import to.bitkit.ui.Routes import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BodySSB @@ -135,12 +134,12 @@ private fun Content( @Composable fun OrderDetailScreen( - orderItem: Routes.OrderDetail, + orderId: String, onBackClick: () -> Unit = {}, ) { val blocktank = blocktankViewModel ?: return val orders by blocktank.orders.collectAsStateWithLifecycle() - val order = orders.find { it.id == orderItem.id } ?: return + val order = orders.find { it.id == orderId } ?: return val coroutineScope = rememberCoroutineScope() OrderDetailContent( @@ -239,12 +238,12 @@ private fun OrderDetailContent( @Composable fun CJitDetailScreen( - cjitItem: Routes.CjitDetail, + entryId: String, onBackClick: () -> Unit = {}, ) { val blocktank = blocktankViewModel ?: return val cJitEntries by blocktank.cJitEntries.collectAsStateWithLifecycle() - val entry = cJitEntries.find { it.id == cjitItem.id } ?: return + val entry = cJitEntries.find { it.id == entryId } ?: return CJitDetailContent( entry = entry, onBack = onBackClick, diff --git a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt index 0d7607cb3..b0ac94689 100644 --- a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt @@ -33,10 +33,10 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption -import to.bitkit.ui.navigateToLogDetail +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha @@ -45,7 +45,7 @@ import to.bitkit.viewmodels.LogsViewModel @Composable fun LogsScreen( - navController: NavController, + navigator: Navigator, viewModel: LogsViewModel = hiltViewModel(), ) { val logs by viewModel.logs.collectAsState() @@ -58,7 +58,7 @@ fun LogsScreen( ScreenColumn { AppTopBar( titleText = "Log Files", - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, actions = { IconButton( onClick = { showDeleteConfirmation = true }, @@ -85,7 +85,7 @@ fun LogsScreen( ) }, modifier = Modifier.clickableAlpha { - navController.navigateToLogDetail(logFile.fileName) + navigator.navigate(Routes.LogDetail(logFile.fileName)) } ) HorizontalDivider() @@ -120,7 +120,7 @@ fun LogsScreen( @Composable fun LogDetailScreen( - navController: NavController, + navigator: Navigator, fileName: String, viewModel: LogsViewModel = hiltViewModel(), ) { @@ -152,7 +152,7 @@ fun LogDetailScreen( ScreenColumn { AppTopBar( titleText = logs.find { it.fileName == fileName }?.displayName ?: "Log Content", - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, actions = { IconButton( onClick = { diff --git a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt index 5ca72a89f..a3f27efae 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt @@ -12,18 +12,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyS -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue import to.bitkit.ui.components.settings.SettingsSwitchRow -import to.bitkit.ui.navigateToAuthCheck -import to.bitkit.ui.navigateToChangePin -import to.bitkit.ui.navigateToDisablePin +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -34,10 +30,9 @@ import to.bitkit.ui.utils.rememberBiometricAuthSupported @Composable fun SecuritySettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return - val app = appViewModel ?: return val isPinEnabled by settings.isPinEnabled.collectAsStateWithLifecycle() val isPinOnLaunchEnabled by settings.isPinOnLaunchEnabled.collectAsStateWithLifecycle() @@ -62,33 +57,35 @@ fun SecuritySettingsScreen( isBiometrySupported = rememberBiometricAuthSupported(), onPinClick = { if (!isPinEnabled) { - app.showSheet(Sheet.Pin()) + navigator.navigate(Routes.PinPrompt()) } else { - navController.navigateToDisablePin() + navigator.navigate(Routes.DisablePin) } }, onChangePinClick = { - navController.navigateToChangePin() + navigator.navigate(Routes.ChangePin) }, onPinOnLaunchClick = { - navController.navigateToAuthCheck( - onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_LAUNCH, + navigator.navigate( + Routes.AuthCheck(onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_LAUNCH) ) }, onPinOnIdleClick = { - navController.navigateToAuthCheck( - onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_IDLE, + navigator.navigate( + Routes.AuthCheck(onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_IDLE) ) }, onPinForPaymentsClick = { - navController.navigateToAuthCheck( - onSuccessActionId = AuthCheckAction.TOGGLE_PIN_FOR_PAYMENTS, + navigator.navigate( + Routes.AuthCheck(onSuccessActionId = AuthCheckAction.TOGGLE_PIN_FOR_PAYMENTS) ) }, onUseBiometricsClick = { - navController.navigateToAuthCheck( - requireBiometrics = true, - onSuccessActionId = AuthCheckAction.TOGGLE_BIOMETRICS, + navigator.navigate( + Routes.AuthCheck( + requireBiometrics = true, + onSuccessActionId = AuthCheckAction.TOGGLE_BIOMETRICS, + ) ) }, onSwipeToHideBalanceClick = { @@ -103,7 +100,7 @@ fun SecuritySettingsScreen( onSendAmountWarningClick = { settings.setEnableSendAmountWarning(!enableSendAmountWarning) }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } 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 823a5d24d..847887799 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -24,18 +24,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R 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.navigateToAboutSettings -import to.bitkit.ui.navigateToAdvancedSettings -import to.bitkit.ui.navigateToBackupSettings -import to.bitkit.ui.navigateToDevSettings -import to.bitkit.ui.navigateToGeneralSettings -import to.bitkit.ui.navigateToSecuritySettings +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -47,7 +41,7 @@ private const val DEV_MODE_TAP_THRESHOLD = 5 @Composable fun SettingsScreen( - navController: NavController, + navigator: Navigator, ) { val app = appViewModel ?: return val settings = settingsViewModel ?: return @@ -58,14 +52,14 @@ fun SettingsScreen( SettingsScreenContent( isDevModeEnabled = isDevModeEnabled, - onGeneralClick = { navController.navigateToGeneralSettings() }, - onSecurityClick = { navController.navigateToSecuritySettings() }, - onBackupClick = { navController.navigateToBackupSettings() }, - onAdvancedClick = { navController.navigateToAdvancedSettings() }, - onSupportClick = { navController.navigate(Routes.Support) }, - onAboutClick = { navController.navigateToAboutSettings() }, - onDevClick = { navController.navigateToDevSettings() }, - onBackClick = { navController.popBackStack() }, + onGeneralClick = { navigator.navigate(Routes.GeneralSettings) }, + onSecurityClick = { navigator.navigate(Routes.SecuritySettings) }, + onBackupClick = { navigator.navigate(Routes.BackupSettings) }, + onAdvancedClick = { navigator.navigate(Routes.AdvancedSettings) }, + onSupportClick = { navigator.navigate(Routes.Support) }, + onAboutClick = { navigator.navigate(Routes.AboutSettings) }, + onDevClick = { navigator.navigate(Routes.DevSettings) }, + onBackClick = { navigator.goBack() }, onCogTap = { haptic.performHapticFeedback(HapticFeedbackType.Confirm) enableDevModeTapCount = enableDevModeTapCount + 1 diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 59a555bb5..d9b677247 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel @@ -42,6 +41,7 @@ import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.components.SearchInput import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -54,7 +54,7 @@ import to.bitkit.ui.utils.getBlockExplorerUrl @Composable fun AddressViewerScreen( - navController: NavController, + navigator: Navigator, viewModel: AddressViewerViewModel = hiltViewModel(), ) { val app = appViewModel ?: return @@ -64,7 +64,7 @@ fun AddressViewerScreen( AddressViewerContent( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onSearchTextChanged = viewModel::updateSearchText, onAddressSelected = { address -> viewModel.selectAddress(address) }, onSwitchAddressType = viewModel::switchAddressType, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt index 9555246b5..c112d3665 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt @@ -14,12 +14,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.CoinSelectionPreference 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.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -38,14 +38,14 @@ object CoinSelectPreferenceTestTags { @Composable fun CoinSelectPreferenceScreen( - navController: NavController, + navigator: Navigator, viewModel: CoinSelectPreferenceViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Content( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onClickManual = { viewModel.setAutoMode(false) }, onClickAutopilot = { viewModel.setAutoMode(true) }, onClickCoinSelectionPreference = { preference -> viewModel.setCoinSelectionPreference(preference) }, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index cd091065f..fea065cde 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -24,10 +24,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer @@ -42,32 +39,27 @@ import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue -import to.bitkit.ui.navigateToScanner +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable fun ElectrumConfigScreen( - savedStateHandle: SavedStateHandle, - navController: NavController, + navigator: Navigator, + scanResult: String? = null, viewModel: ElectrumConfigViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val app = appViewModel ?: return val context = LocalContext.current - // Handle result from Scanner - LaunchedEffect(savedStateHandle) { - savedStateHandle.getStateFlow(SCAN_RESULT_KEY, null) - .filterNotNull() - .collect { scannedData -> - viewModel.onScan(scannedData) - savedStateHandle.remove(SCAN_RESULT_KEY) - } + // Handle scan result passed via navigation + LaunchedEffect(scanResult) { + scanResult?.let { viewModel.onScan(it) } } // Monitor connection results @@ -96,8 +88,8 @@ fun ElectrumConfigScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, - onScan = { navController.navigateToScanner(isCalledForResult = true) }, + onBack = { navigator.goBack() }, + onScan = { navigator.navigate(Routes.QrScanner) }, onChangeHost = viewModel::setHost, onChangePort = viewModel::setPort, onChangeProtocol = viewModel::setProtocol, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index cf74847db..0aa72b2b6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -21,10 +21,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.ui.appViewModel @@ -35,32 +32,27 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer -import to.bitkit.ui.navigateToScanner +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable fun RgsServerScreen( - savedStateHandle: SavedStateHandle, - navController: NavController, + navigator: Navigator, + scanResult: String? = null, viewModel: RgsServerViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val app = appViewModel ?: return val context = LocalContext.current - // Handle result from Scanner - LaunchedEffect(savedStateHandle) { - savedStateHandle.getStateFlow(SCAN_RESULT_KEY, null) - .filterNotNull() - .collect { scannedData -> - viewModel.onScan(scannedData) - savedStateHandle.remove(SCAN_RESULT_KEY) - } + // Handle scan result passed via navigation + LaunchedEffect(scanResult) { + scanResult?.let { viewModel.onScan(it) } } // Monitor connection results @@ -87,8 +79,8 @@ fun RgsServerScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, - onScan = { navController.navigateToScanner(isCalledForResult = true) }, + onBack = { navigator.goBack() }, + onScan = { navigator.navigate(Routes.QrScanner) }, onChangeUrl = viewModel::setRgsUrl, onClickReset = viewModel::resetToDefault, onClickConnect = viewModel::onClickConnect, 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 9fe6a8799..e64dbc9e4 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 @@ -26,18 +26,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.startActivityAppSettings import to.bitkit.ext.toLocalizedTimestamp import to.bitkit.models.HealthState import to.bitkit.models.NodeLifecycleState import to.bitkit.repositories.AppHealthState -import to.bitkit.ui.Routes 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.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -50,7 +50,7 @@ import kotlin.time.ExperimentalTime @Composable fun AppStatusScreen( - navController: NavController, + navigator: Navigator, viewModel: AppStatusViewModel = hiltViewModel(), ) { val context = LocalContext.current @@ -58,12 +58,12 @@ fun AppStatusScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onInternetClick = { context.startActivityAppSettings() }, - onElectrumClick = { navController.navigate(Routes.ElectrumConfig) }, - onNodeClick = { navController.navigate(Routes.NodeInfo) }, - onChannelsClick = { navController.navigate(Routes.LightningConnections) }, - onBackupClick = { navController.navigate(Routes.BackupSettings) }, + onElectrumClick = { navigator.navigate(Routes.ElectrumConfig) }, + onNodeClick = { navigator.navigate(Routes.NodeInfo) }, + onChannelsClick = { navigator.navigate(Routes.LightningConnections) }, + onBackupClick = { navigator.navigate(Routes.BackupSettings) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt index 562bbfe62..38da9b71b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt @@ -43,17 +43,13 @@ fun BackgroundPaymentsSettings( val showNotificationDetails by settingsViewModel.showNotificationDetails.collectAsStateWithLifecycle() RequestNotificationPermissions( - onPermissionChange = { granted -> - settingsViewModel.setNotificationPreference(granted) - }, - showPermissionDialog = false + onPermissionChange = settingsViewModel::setNotificationPreference, + showPermissionDialog = false, ) Content( onBack = onBack, - onSystemSettingsClick = { - NotificationUtils.openNotificationSettings(context) - }, + onSystemSettingsClick = { NotificationUtils.openNotificationSettings(context) }, hasPermission = notificationsGranted, showDetails = showNotificationDetails, toggleNotificationDetails = settingsViewModel::toggleNotificationDetails, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt index e9b83d1fc..8a3e89b74 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt @@ -23,13 +23,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton -import to.bitkit.ui.components.Sheet +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -47,19 +46,18 @@ object ResetAndRestoreTestTags { @Composable fun ResetAndRestoreScreen( - navController: NavController, + navigator: Navigator, ) { - val app = appViewModel ?: return val wallet = walletViewModel ?: return var showDialog by remember { mutableStateOf(false) } Content( showConfirmDialog = showDialog, - onClickBackup = { app.showSheet(Sheet.Backup()) }, + onClickBackup = { navigator.navigate(Routes.BackupIntro) }, onClickReset = { showDialog = true }, onResetConfirm = { wallet.wipeWallet() }, onResetDismiss = { showDialog = false }, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, ) } 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 c5650ef5f..974ca1bf3 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 @@ -52,7 +52,7 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.ui.utils.withAccent @Composable @@ -97,7 +97,7 @@ private fun ShowMnemonicContent( // Scroll to bottom when mnemonic is revealed LaunchedEffect(showMnemonic) { if (showMnemonic) { - delay(TRANSITION_SCREEN_MS) // Wait for the animation to start + delay(MS_TRANSITION_SCREEN) // Wait for the animation to start scope.launch { scrollState.animateScrollTo(scrollState.maxValue) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt index 623984f03..809b0d200 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.PrimaryDisplay @@ -21,6 +20,7 @@ import to.bitkit.ui.components.settings.SectionFooter 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.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -30,7 +30,7 @@ import to.bitkit.viewmodels.CurrencyViewModel @Composable fun DefaultUnitSettingsScreen( currencyViewModel: CurrencyViewModel, - navController: NavController, + navigator: Navigator, ) { val (_, _, _, selectedCurrency, _, displayUnit, primaryDisplay) = LocalCurrencies.current @@ -40,7 +40,7 @@ fun DefaultUnitSettingsScreen( primaryDisplay = primaryDisplay, onPrimaryUnitClick = currencyViewModel::setPrimaryDisplayUnit, onBitcoinUnitClick = currencyViewModel::setBtcDisplayUnit, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } 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 cc63bfc56..8b237844b 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 @@ -14,23 +14,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.Language import to.bitkit.models.PrimaryDisplay import to.bitkit.models.TransactionSpeed import to.bitkit.models.transactionSpeedUiText 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.navigateToDefaultUnitSettings -import to.bitkit.ui.navigateToLanguageSettings -import to.bitkit.ui.navigateToLocalCurrencySettings -import to.bitkit.ui.navigateToQuickPaySettings -import to.bitkit.ui.navigateToTagsSettings -import to.bitkit.ui.navigateToTransactionSpeedSettings -import to.bitkit.ui.navigateToWidgetsSettings +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +33,7 @@ import to.bitkit.viewmodels.LanguageViewModel @Composable fun GeneralSettingsScreen( - navController: NavController, + navigator: Navigator, languageViewModel: LanguageViewModel = hiltViewModel(), ) { val settings = settingsViewModel ?: return @@ -59,19 +52,19 @@ fun GeneralSettingsScreen( primaryDisplay = currencies.primaryDisplay, defaultTransactionSpeed = defaultTransactionSpeed, showTagsButton = lastUsedTags.isNotEmpty(), - onBackClick = { navController.popBackStack() }, - onLocalCurrencyClick = { navController.navigateToLocalCurrencySettings() }, - onDefaultUnitClick = { navController.navigateToDefaultUnitSettings() }, - onTransactionSpeedClick = { navController.navigateToTransactionSpeedSettings() }, - onWidgetsClick = { navController.navigateToWidgetsSettings() }, - onQuickPayClick = { navController.navigateToQuickPaySettings(quickPayIntroSeen) }, - onTagsClick = { navController.navigateToTagsSettings() }, - onLanguageSettingsClick = { navController.navigateToLanguageSettings() }, + onBackClick = { navigator.goBack() }, + onLocalCurrencyClick = { navigator.navigate(Routes.LocalCurrencySettings) }, + onDefaultUnitClick = { navigator.navigate(Routes.DefaultUnitSettings) }, + onTransactionSpeedClick = { navigator.navigate(Routes.TransactionSpeedSettings) }, + onWidgetsClick = { navigator.navigate(Routes.WidgetsSettings) }, + onQuickPayClick = { navigator.navigateToQuickPaySettings(quickPayIntroSeen) }, + onTagsClick = { navigator.navigate(Routes.TagsSettings) }, + onLanguageSettingsClick = { navigator.navigate(Routes.LanguageSettings) }, onBgPaymentsClick = { if (bgPaymentsIntroSeen || notificationsGranted) { - navController.navigate(Routes.BackgroundPaymentsSettings) + navigator.navigate(Routes.BackgroundPaymentsSettings) } else { - navController.navigate(Routes.BackgroundPaymentsIntro) + navigator.navigate(Routes.BackgroundPaymentsIntro) } }, selectedLanguage = languageUiState.selectedLanguage.displayName, diff --git a/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt index 6f887099d..91fdc576e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.FxRate import to.bitkit.ui.LocalCurrencies @@ -24,6 +23,7 @@ import to.bitkit.ui.components.SearchInput 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.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -34,7 +34,7 @@ import to.bitkit.viewmodels.CurrencyViewModel @Composable fun LocalCurrencySettingsScreen( currencyViewModel: CurrencyViewModel, - navController: NavController, + navigator: Navigator, ) { val (rates, _, _, selectedCurrency) = LocalCurrencies.current var searchText by remember { mutableStateOf("") } @@ -74,7 +74,7 @@ fun LocalCurrencySettingsScreen( otherCurrencies = otherCurrencies, selectedCurrency = selectedCurrency, onCurrencyClick = { currencyViewModel.setSelectedCurrency(it) }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt index 2006e8f39..274761b31 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt @@ -13,10 +13,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.settings.SectionHeader +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -25,7 +25,7 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun TagsSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return @@ -36,10 +36,10 @@ fun TagsSettingsScreen( onClickTag = { tag -> settings.deleteLastUsedTag(tag) if (tags.size == 1) { - navController.popBackStack() + navigator.goBack() } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt index 9a860baa4..c086cf562 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt @@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ui.components.settings.SettingsSwitchRow +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -20,7 +20,7 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun WidgetsSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return @@ -28,7 +28,7 @@ fun WidgetsSettingsScreen( val showWidgetTitles by settings.showWidgetTitles.collectAsStateWithLifecycle() WidgetsSettingsContent( - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, showWidgets = showWidgets, showWidgetTitles = showWidgetTitles, onShowWidgetsClick = { settings.setShowWidgets(!showWidgets) }, 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 3081689de..3abe53b4a 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 @@ -31,7 +31,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.synonym.bitkitcore.BtBolt11InvoiceState import com.synonym.bitkitcore.BtOpenChannelState import com.synonym.bitkitcore.BtOrderState @@ -56,7 +55,6 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB @@ -67,6 +65,8 @@ 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.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -83,7 +83,7 @@ import java.util.Locale @Composable fun ChannelDetailScreen( - navController: NavController, + navigator: Navigator, viewModel: LightningConnectionsViewModel, ) { val context = LocalContext.current @@ -123,7 +123,7 @@ fun ChannelDetailScreen( txTime = txTime, isRefreshing = uiState.isRefreshing, isClosedChannel = isClosedChannel, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onRefresh = { viewModel.onPullToRefresh() }, @@ -141,7 +141,7 @@ fun ChannelDetailScreen( context.startActivity(intent) }, onSupport = { order -> contactSupport(order, channel, walletState.nodeId, context) }, - onCloseConnection = { navController.navigate(Routes.CloseConnection) }, + onCloseConnection = { navigator.navigate(Routes.CloseConnection) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt index f31de0ea5..8affe8327 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt @@ -22,13 +22,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -38,7 +38,7 @@ import to.bitkit.ui.utils.withAccentBoldBright @Composable fun CloseConnectionScreen( - navController: NavController, + navigator: Navigator, viewModel: LightningConnectionsViewModel, ) { val uiState by viewModel.closeConnectionUiState.collectAsState() @@ -51,13 +51,13 @@ fun CloseConnectionScreen( // Handle success navigation LaunchedEffect(uiState.closeSuccess) { if (uiState.closeSuccess) { - navController.popBackStack(inclusive = false) + navigator.navigate(Routes.LightningConnections) } } Content( isLoading = uiState.isLoading, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onClickClose = { viewModel.closeChannel() }, ) } 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 d409a81b3..1e096562a 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 @@ -39,13 +39,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.models.formatToModernDisplay -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.ChannelStatusUi @@ -57,7 +54,8 @@ 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.navigateToTransferFunding +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha @@ -66,7 +64,6 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors private const val CLOSED_CHANNEL_ALPHA = 0.64f -private const val CHANNEL_SELECTION_DELAY_MS = 200L object LightningConnectionsTestTags { const val SCREEN = "lightning_connections_screen" @@ -78,7 +75,7 @@ object LightningConnectionsTestTags { @Composable fun LightningConnectionsScreen( - navController: NavController, + navigator: Navigator, viewModel: LightningConnectionsViewModel, ) { val context = LocalContext.current @@ -90,30 +87,16 @@ fun LightningConnectionsScreen( viewModel.clearTransactionDetails() } - LaunchedEffect(navController.currentBackStackEntry) { - val selectedChannelId = navController.previousBackStackEntry?.savedStateHandle?.get("selectedChannelId") - if (selectedChannelId == null) return@LaunchedEffect - - navController.previousBackStackEntry?.savedStateHandle?.remove("selectedChannelId") - delay(CHANNEL_SELECTION_DELAY_MS) - if (viewModel.findAndSelectChannel(selectedChannelId)) { - navController.navigate(Routes.ChannelDetail) { - launchSingleTop = true - popUpTo(Routes.ConnectionsNav) { inclusive = false } - } - } - } - Content( uiState = uiState, - onBack = { navController.popBackStack() }, - onClickAddConnection = { navController.navigateToTransferFunding() }, + onBack = { navigator.goBack() }, + onClickAddConnection = { navigator.navigate(Routes.Funding) }, onClickExportLogs = { viewModel.zipLogsForSharing { uri -> context.shareZipFile(uri) } }, onClickChannel = { channelUi -> viewModel.setSelectedChannel(channelUi) - navController.navigate(Routes.ChannelDetail) + navigator.navigate(Routes.ChannelDetail) }, onRefresh = { viewModel.onPullToRefresh() diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt index 99934e17f..4dd7a4441 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.env.Env @@ -30,7 +29,8 @@ import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PinDots -import to.bitkit.ui.navigateToChangePinResult +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +40,7 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinConfirmScreen( newPin: String, - navController: NavController, + navigator: Navigator, ) { val app = appViewModel ?: return var pin by remember { mutableStateOf("") } @@ -50,7 +50,7 @@ fun ChangePinConfirmScreen( if (pin.length == Env.PIN_LENGTH) { if (pin == newPin) { app.editPin(newPin) - navController.navigateToChangePinResult() + navigator.navigate(Routes.ChangePinResult) } else { showError = true delay(500) @@ -71,7 +71,7 @@ fun ChangePinConfirmScreen( pin += key } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt index 79e9620d9..cddf5970f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.components.BodyM @@ -24,7 +23,8 @@ import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PinDots -import to.bitkit.ui.navigateToChangePinConfirm +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -33,13 +33,13 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinNewScreen( - navController: NavController, + navigator: Navigator, ) { var pin by remember { mutableStateOf("") } LaunchedEffect(pin) { if (pin.length == Env.PIN_LENGTH) { - navController.navigateToChangePinConfirm(pin) + navigator.navigate(Routes.ChangePinConfirm(pin)) } } @@ -54,7 +54,7 @@ fun ChangePinNewScreen( pin += key } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt index 8913265f2..9c10f05ba 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt @@ -16,11 +16,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface @@ -28,14 +28,14 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinResultScreen( - navController: NavController, + navigator: Navigator, ) { ChangePinResultContent( onOkClick = { - navController.popBackStack(inclusive = false) + navigator.navigate(Routes.SecuritySettings) }, onBackClick = { - navController.popBackStack() + navigator.goBack() } ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt index 85c727ed0..9511ad262 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.appViewModel @@ -29,7 +28,8 @@ import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PinDots -import to.bitkit.ui.navigateToChangePinNew +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -39,7 +39,7 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinScreen( - navController: NavController, + navigator: Navigator, ) { val app = appViewModel ?: return val attemptsRemaining by app.pinAttemptsRemaining.collectAsStateWithLifecycle() @@ -48,7 +48,7 @@ fun ChangePinScreen( LaunchedEffect(pin) { if (pin.length == Env.PIN_LENGTH) { if (app.validatePin(pin)) { - navController.navigateToChangePinNew() + navigator.navigate(Routes.ChangePinNew) } else { pin = "" } @@ -67,7 +67,7 @@ fun ChangePinScreen( pin += key } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, onClickForgotPin = { app.setShowForgotPin(true) }, ) } 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..4c22b855a 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 @@ -16,14 +16,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.navOptions import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton -import to.bitkit.ui.navigateToAuthCheck +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -32,17 +30,18 @@ import to.bitkit.ui.theme.Colors @Composable fun DisablePinScreen( - navController: NavController, + navigator: Navigator, ) { DisablePinContent( onDisableClick = { - navController.navigateToAuthCheck( - requirePin = true, - onSuccessActionId = AuthCheckAction.DISABLE_PIN, - navOptions = navOptions { popUpTo(Routes.SecuritySettings) }, + navigator.navigate( + Routes.AuthCheck( + requirePin = true, + onSuccessActionId = AuthCheckAction.DISABLE_PIN, + ) ) }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } 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..a1b18042d 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 @@ -16,33 +16,49 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env -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.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +/** + * Support screen composable. + */ @Composable fun SupportScreen( - navController: NavController, + onBack: () -> Unit, + onClickHelpCenter: () -> Unit, +) { + Content( + onBack = onBack, + onClickReportIssue = {}, + onClickHelpCenter = onClickHelpCenter, + onClickAppStatus = {}, + ) +} + +@Composable +fun SupportScreen( + navigator: Navigator, ) { val context = LocalContext.current Content( - onBack = { navController.popBackStack() }, - onClickReportIssue = { navController.navigate(Routes.ReportIssue) }, + onBack = { navigator.goBack() }, + onClickReportIssue = { navigator.navigate(Routes.ReportIssue) }, onClickHelpCenter = { val intent = Intent(Intent.ACTION_VIEW, Env.BITKIT_HELP_CENTER.toUri()) context.startActivity(intent) }, - onClickAppStatus = { navController.navigate(Routes.AppStatus) }, + onClickAppStatus = { navigator.navigate(Routes.AppStatus) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt index 01ea002d1..8c24bb357 100644 --- a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.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.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.TransactionDefaults import to.bitkit.models.BITCOIN_SYMBOL @@ -31,6 +30,7 @@ import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.currencyViewModel +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +40,7 @@ import to.bitkit.ui.theme.Colors @Composable fun CustomFeeSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return val customFeeRate = settings.defaultTransactionSpeed.collectAsStateWithLifecycle() @@ -83,9 +83,9 @@ fun CustomFeeSettingsScreen( onContinue = { val feeRate = input.toUIntOrNull() ?: 0u settings.setDefaultTransactionSpeed(TransactionSpeed.Custom(feeRate)) - navController.popBackStack() + navigator.goBack() }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt index 35db5e680..a3b1e3ff1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt @@ -11,13 +11,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.TransactionSpeed 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.navigateToCustomFeeSettings +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -27,7 +27,7 @@ import to.bitkit.ui.theme.Colors @Composable fun TransactionSpeedSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return val defaultTransactionSpeed = settings.defaultTransactionSpeed.collectAsStateWithLifecycle() @@ -36,10 +36,10 @@ fun TransactionSpeedSettingsScreen( selectedSpeed = defaultTransactionSpeed.value, onSpeedSelected = { settings.setDefaultTransactionSpeed(it) - navController.popBackStack() + navigator.goBack() }, - onCustomFeeClick = { navController.navigateToCustomFeeSettings() }, - onBackClick = { navController.popBackStack() }, + onCustomFeeClick = { navigator.navigate(Routes.CustomFeeSettings) }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt deleted file mode 100644 index 0ccaf924b..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt +++ /dev/null @@ -1,180 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -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.settings.backups.BackupContract -import to.bitkit.ui.settings.backups.BackupIntroScreen -import to.bitkit.ui.settings.backups.BackupNavSheetViewModel -import to.bitkit.ui.settings.backups.ConfirmMnemonicScreen -import to.bitkit.ui.settings.backups.ConfirmPassphraseScreen -import to.bitkit.ui.settings.backups.MetadataScreen -import to.bitkit.ui.settings.backups.MultipleDevicesScreen -import to.bitkit.ui.settings.backups.ShowMnemonicScreen -import to.bitkit.ui.settings.backups.ShowPassphraseScreen -import to.bitkit.ui.settings.backups.SuccessScreen -import to.bitkit.ui.settings.backups.WarningScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.composableWithDefaultTransitions - -@Composable -fun BackupSheet( - sheet: Sheet.Backup, - onDismiss: () -> Unit, - viewModel: BackupNavSheetViewModel = hiltViewModel(), -) { - val navController = rememberNavController() - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val currentOnDismiss by rememberUpdatedState(onDismiss) - - LaunchedEffect(Unit) { - viewModel.loadMnemonicData() - } - - DisposableEffect(Unit) { - onDispose { - viewModel.resetState() - } - } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - BackupContract.SideEffect.NavigateToShowPassphrase -> navController.navigate(BackupRoute.ShowPassphrase) - BackupContract.SideEffect.NavigateToConfirmMnemonic -> navController.navigate( - BackupRoute.ConfirmMnemonic - ) - - BackupContract.SideEffect.NavigateToConfirmPassphrase -> navController.navigate( - BackupRoute.ConfirmPassphrase - ) - - BackupContract.SideEffect.NavigateToWarning -> navController.navigate(BackupRoute.Warning) - BackupContract.SideEffect.NavigateToSuccess -> navController.navigate(BackupRoute.Success) - BackupContract.SideEffect.NavigateToMultipleDevices -> navController.navigate( - BackupRoute.MultipleDevices - ) - - BackupContract.SideEffect.NavigateToMetadata -> navController.navigate(BackupRoute.Metadata) - BackupContract.SideEffect.DismissSheet -> currentOnDismiss() - } - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight(SheetSize.MEDIUM) - .testTag("backup_navigation_sheet") - ) { - NavHost( - navController = navController, - startDestination = sheet.route, - ) { - composableWithDefaultTransitions { - BackupIntroScreen( - hasFunds = LocalBalances.current.totalSats > 0u, - onClose = currentOnDismiss, - onConfirm = { navController.navigate(BackupRoute.ShowMnemonic) }, - ) - } - composableWithDefaultTransitions { - ShowMnemonicScreen( - uiState = uiState, - onRevealClick = viewModel::onRevealMnemonic, - onContinueClick = viewModel::onShowMnemonicContinue, - ) - } - composableWithDefaultTransitions { - ShowPassphraseScreen( - uiState = uiState, - onContinue = viewModel::onShowPassphraseContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - ConfirmMnemonicScreen( - uiState = uiState, - onContinue = viewModel::onConfirmMnemonicContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - ConfirmPassphraseScreen( - uiState = uiState, - onPassphraseChange = viewModel::onPassphraseInput, - onContinue = viewModel::onConfirmPassphraseContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - WarningScreen( - onContinue = viewModel::onWarningContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - SuccessScreen( - onContinue = viewModel::onSuccessContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - MultipleDevicesScreen( - onContinue = viewModel::onMultipleDevicesContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - MetadataScreen( - uiState = uiState, - onDismiss = viewModel::onMetadataClose, - onBack = { navController.popBackStack() }, - ) - } - } - } -} - -sealed interface BackupRoute { - @Serializable - data object Intro : BackupRoute - - @Serializable - data object ShowMnemonic : BackupRoute - - @Serializable - data object ShowPassphrase : BackupRoute - - @Serializable - data object ConfirmMnemonic : BackupRoute - - @Serializable - data object ConfirmPassphrase : BackupRoute - - @Serializable - data object Warning : BackupRoute - - @Serializable - data object Success : BackupRoute - - @Serializable - data object MultipleDevices : BackupRoute - - @Serializable - data object Metadata : BackupRoute -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt index 0800b4de2..ee7568b5d 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt @@ -39,7 +39,7 @@ fun ForceTransferSheet( transferViewModel: TransferViewModel, ) { val isLoading by transferViewModel.isForceTransferLoading.collectAsStateWithLifecycle() - Content( + ForceTransferContent( isLoading = isLoading, onForceTransfer = { transferViewModel.forceTransfer { @@ -51,7 +51,7 @@ fun ForceTransferSheet( } @Composable -private fun Content( +internal fun ForceTransferContent( modifier: Modifier = Modifier, isLoading: Boolean = false, onForceTransfer: () -> Unit = {}, @@ -118,7 +118,7 @@ private fun Content( private fun Preview() { AppThemeSurface { BottomSheetPreview { - Content() + ForceTransferContent() } } } @@ -128,7 +128,7 @@ private fun Preview() { private fun PreviewLoading() { AppThemeSurface { BottomSheetPreview { - Content(isLoading = true) + ForceTransferContent(isLoading = true) } } } diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt index 23ce441e0..d8f4132ff 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt @@ -33,6 +33,8 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors +internal const val IMAGE_WIDTH_FRACTION = 0.8f + @Composable fun GiftLoading( viewModel: GiftViewModel, diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftRoute.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftRoute.kt deleted file mode 100644 index 060bb8fbb..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftRoute.kt +++ /dev/null @@ -1,23 +0,0 @@ -package to.bitkit.ui.sheets - -import kotlinx.serialization.Serializable - -internal const val IMAGE_WIDTH_FRACTION = 0.8f - -@Serializable -sealed interface GiftRoute { - @Serializable - data object Loading : GiftRoute - - @Serializable - data object Used : GiftRoute - - @Serializable - data object UsedUp : GiftRoute - - @Serializable - data object Error : GiftRoute - - @Serializable - data object Success : GiftRoute -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt deleted file mode 100644 index 8d286dfbc..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt +++ /dev/null @@ -1,98 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import to.bitkit.R -import to.bitkit.models.NewTransactionSheetDetails -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.viewmodels.AppViewModel - -@Composable -fun GiftSheet( - sheet: Sheet.Gift, - appViewModel: AppViewModel, - modifier: Modifier = Modifier, - viewModel: GiftViewModel = hiltViewModel(), -) { - val navController = rememberNavController() - - LaunchedEffect(sheet.code, sheet.amount) { - viewModel.initialize(sheet.code, sheet.amount) - } - - val onSuccessState = rememberUpdatedState { details: NewTransactionSheetDetails -> - appViewModel.hideSheet() - appViewModel.showTransactionSheet(details) - } - - LaunchedEffect(Unit) { - viewModel.successEvent.collect { details -> - onSuccessState.value.invoke(details) - } - } - - LaunchedEffect(Unit) { - viewModel.navigationEvent.collect { route -> - when (route) { - is GiftRoute.Success -> appViewModel.hideSheet() - else -> navController.navigate(route) { - popUpTo(GiftRoute.Loading) { inclusive = false } - } - } - } - } - - Column( - modifier = modifier - .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("GiftSheet") - ) { - NavHost( - navController = navController, - startDestination = GiftRoute.Loading, - ) { - composableWithDefaultTransitions { - GiftLoading( - viewModel = viewModel, - ) - } - composableWithDefaultTransitions { - GiftErrorSheet( - titleRes = R.string.other__gift__used__title, - textRes = R.string.other__gift__used__text, - testTag = "GiftUsed", - onDismiss = { appViewModel.hideSheet() }, - ) - } - composableWithDefaultTransitions { - GiftErrorSheet( - titleRes = R.string.other__gift__used_up__title, - textRes = R.string.other__gift__used_up__text, - testTag = "GiftUsedUp", - onDismiss = { appViewModel.hideSheet() }, - ) - } - composableWithDefaultTransitions { - GiftErrorSheet( - titleRes = R.string.other__gift__error__title, - textRes = R.string.other__gift__error__text, - testTag = "GiftError", - onDismiss = { appViewModel.hideSheet() }, - ) - } - } - } -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt index d7a1bc8e4..91945415f 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt @@ -21,6 +21,7 @@ import to.bitkit.models.NewTransactionSheetType import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.GiftClaimResult +import to.bitkit.ui.nav.Routes import to.bitkit.utils.Logger import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -32,7 +33,7 @@ class GiftViewModel @Inject constructor( private val activityRepo: ActivityRepo, ) : ViewModel() { - private val _navigationEvent = MutableSharedFlow(extraBufferCapacity = 1) + private val _navigationEvent = MutableSharedFlow(extraBufferCapacity = 1) val navigationEvent = _navigationEvent.asSharedFlow() private val _successEvent = MutableSharedFlow(extraBufferCapacity = 1) @@ -46,11 +47,6 @@ class GiftViewModel @Inject constructor( private var isClaiming: Boolean = false fun initialize(code: String, amount: ULong) { - if (!isClaiming) { - viewModelScope.launch { - _navigationEvent.emit(GiftRoute.Loading) - } - } this.code = code this.amount = amount viewModelScope.launch(bgDispatcher) { @@ -71,7 +67,7 @@ class GiftViewModel @Inject constructor( onSuccess = { result -> when (result) { is GiftClaimResult.SuccessWithLiquidity -> { - _navigationEvent.emit(GiftRoute.Success) + _navigationEvent.emit(Routes.GiftSuccess) } is GiftClaimResult.SuccessWithoutLiquidity -> { insertGiftActivity(result) @@ -83,7 +79,7 @@ class GiftViewModel @Inject constructor( sats = result.sats, ) ) - _navigationEvent.emit(GiftRoute.Success) + _navigationEvent.emit(Routes.GiftSuccess) } } }, @@ -116,9 +112,9 @@ class GiftViewModel @Inject constructor( Logger.error("Gift claim failed: $error", error, context = TAG) val route = when { - errorContains(error, "GIFT_CODE_ALREADY_USED") -> GiftRoute.Used - errorContains(error, "GIFT_CODE_USED_UP") -> GiftRoute.UsedUp - else -> GiftRoute.Error + errorContains(error, "GIFT_CODE_ALREADY_USED") -> Routes.GiftUsed + errorContains(error, "GIFT_CODE_USED_UP") -> Routes.GiftUsedUp + else -> Routes.GiftError } _navigationEvent.emit(route) diff --git a/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt index 36c951e96..f9d5a9be4 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt @@ -20,7 +20,6 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.SheetSize import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar @@ -28,28 +27,9 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.viewmodels.AppViewModel @Composable -fun LnurlAuthSheet( - sheet: Sheet.LnurlAuth, - app: AppViewModel, -) { - Content( - domain = sheet.domain, - onContinue = { - app.requestLnurlAuth( - callback = sheet.lnurl, - k1 = sheet.k1, - domain = sheet.domain, - ) - }, - onCancel = { app.hideSheet() }, - ) -} - -@Composable -private fun Content( +internal fun LnurlAuthContent( domain: String, modifier: Modifier = Modifier, onCancel: () -> Unit = {}, @@ -112,7 +92,7 @@ private fun Content( private fun Preview() { AppThemeSurface { BottomSheetPreview { - Content( + LnurlAuthContent( domain = "LNMarkets.com", ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt deleted file mode 100644 index dd51453ee..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt +++ /dev/null @@ -1,96 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import kotlinx.serialization.Serializable -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.components.SheetSize -import to.bitkit.ui.settings.pin.PinBiometricsScreen -import to.bitkit.ui.settings.pin.PinChooseScreen -import to.bitkit.ui.settings.pin.PinConfirmScreen -import to.bitkit.ui.settings.pin.PinPromptScreen -import to.bitkit.ui.settings.pin.PinResultScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.viewmodels.AppViewModel - -@Composable -fun PinSheet( - sheet: Sheet.Pin, - app: AppViewModel, -) { - val navController = rememberNavController() - val onDismiss = app::hideSheet - - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight(SheetSize.MEDIUM) - ) { - NavHost( - navController = navController, - startDestination = sheet.route, - ) { - composableWithDefaultTransitions { - PinPromptScreen( - showLaterButton = it.toRoute().showLaterButton, - onContinue = { navController.navigate(PinRoute.Choose) }, - onLater = onDismiss, - ) - } - composableWithDefaultTransitions { - PinChooseScreen( - onPinChosen = { pin -> - navController.navigate(PinRoute.Confirm(pin)) - }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - PinConfirmScreen( - originalPin = it.toRoute().pin, - onPinConfirmed = { navController.navigate(PinRoute.Biometrics) }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - PinBiometricsScreen( - onContinue = { isBioOn -> - navController.navigate(PinRoute.Result(isBioOn)) - }, - onSkip = { navController.navigate(PinRoute.Result(isBioOn = false)) }, - onBack = onDismiss, - ) - } - composableWithDefaultTransitions { - PinResultScreen( - isBioOn = it.toRoute().isBioOn, - onDismiss = onDismiss, - onBack = onDismiss, - ) - } - } - } -} - -sealed interface PinRoute { - @Serializable - data class Prompt(val showLaterButton: Boolean = false) : PinRoute - - @Serializable - data object Choose : PinRoute - - @Serializable - data class Confirm(val pin: String) : PinRoute - - @Serializable - data object Biometrics : PinRoute - - @Serializable - data class Result(val isBioOn: Boolean) : PinRoute -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt deleted file mode 100644 index 5c4f49e59..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ /dev/null @@ -1,342 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import kotlinx.serialization.Serializable -import to.bitkit.models.NewTransactionSheetDetails -import to.bitkit.models.NewTransactionSheetDirection -import to.bitkit.models.NewTransactionSheetType -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 -import to.bitkit.ui.screens.wallets.send.SendAddressScreen -import to.bitkit.ui.screens.wallets.send.SendAmountScreen -import to.bitkit.ui.screens.wallets.send.SendCoinSelectionScreen -import to.bitkit.ui.screens.wallets.send.SendConfirmScreen -import to.bitkit.ui.screens.wallets.send.SendErrorScreen -import to.bitkit.ui.screens.wallets.send.SendFeeCustomScreen -import to.bitkit.ui.screens.wallets.send.SendFeeRateScreen -import to.bitkit.ui.screens.wallets.send.SendFeeViewModel -import to.bitkit.ui.screens.wallets.send.SendPinCheckScreen -import to.bitkit.ui.screens.wallets.send.SendQuickPayScreen -import to.bitkit.ui.screens.wallets.send.SendRecipientScreen -import to.bitkit.ui.screens.wallets.withdraw.WithdrawConfirmScreen -import to.bitkit.ui.screens.wallets.withdraw.WithdrawErrorScreen -import to.bitkit.ui.settings.support.SupportScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.gradientBackground -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.ui.utils.navigationWithDefaultTransitions -import to.bitkit.viewmodels.AppViewModel -import to.bitkit.viewmodels.SendEffect -import to.bitkit.viewmodels.SendEvent -import to.bitkit.viewmodels.WalletViewModel - -@Composable -fun SendSheet( - appViewModel: AppViewModel, - walletViewModel: WalletViewModel, - startDestination: SendRoute = SendRoute.Recipient, -) { - LaunchedEffect(startDestination) { - // always reset state on new user-initiated send - if (startDestination == SendRoute.Recipient) { - appViewModel.resetSendState() - appViewModel.resetQuickPayData() - } - } - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("SendSheet") - ) { - val navController = rememberNavController() - 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.PopBack -> navController.popBackStack(it.route, inclusive = false) - is SendEffect.PaymentSuccess -> { - appViewModel.clearClipboardForAutoRead() - navController.navigate(SendRoute.Success) { - popUpTo(startDestination) { 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) - } - } - } - - NavHost( - navController = navController, - startDestination = startDestination, - ) { - composableWithDefaultTransitions { - SendRecipientScreen( - onEvent = { appViewModel.setSendEvent(it) } - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - SendAddressScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onEvent = { appViewModel.setSendEvent(it) }, - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() - SendAmountScreen( - uiState = uiState, - walletUiState = walletUiState, - canGoBack = startDestination != SendRoute.Amount, - onBack = { - if (!navController.popBackStack()) { - appViewModel.hideSheet() - } - }, - onEvent = { appViewModel.setSendEvent(it) } - ) - } - composableWithDefaultTransitions { - QrScanningScreen( - navController = navController, - inSheet = true, - ) { qrCode -> - navController.popBackStack() - appViewModel.onScanResult(data = qrCode) - } - } - composableWithDefaultTransitions { - val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - SendCoinSelectionScreen( - requiredAmount = sendUiState.amount, - address = sendUiState.address, - onBack = { navController.popBackStack() }, - onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, - ) - } - navigationWithDefaultTransitions( - startDestination = SendRoute.FeeRate, - ) { - composableWithDefaultTransitions { - val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } - SendFeeRateScreen( - sendUiState = sendUiState, - viewModel = hiltViewModel(parentEntry), - onBack = { navController.popBackStack() }, - onContinue = { navController.popBackStack() }, - onSelect = { speed -> appViewModel.onSelectSpeed(speed) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } - SendFeeCustomScreen( - viewModel = hiltViewModel(parentEntry), - onBack = { navController.popBackStack() }, - onContinue = { speed -> appViewModel.setTransactionSpeed(speed) }, - ) - } - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() - - SendConfirmScreen( - savedStateHandle = it.savedStateHandle, - uiState = uiState, - isNodeRunning = walletUiState.nodeLifecycleState.isRunning(), - canGoBack = startDestination != SendRoute.Confirm, - onBack = { - if (!navController.popBackStack()) { - appViewModel.hideSheet() - } - }, - onEvent = { e -> appViewModel.setSendEvent(e) }, - onClickAddTag = { navController.navigate(SendRoute.AddTag) }, - onClickTag = { tag -> appViewModel.removeTag(tag) }, - onNavigateToPin = { navController.navigate(SendRoute.PinCheck) }, - ) - } - composableWithDefaultTransitions { - val sendDetail by appViewModel.successSendUiState.collectAsStateWithLifecycle() - NewTransactionSheetView( - details = sendDetail, - onCloseClick = { appViewModel.hideSheet() }, - onDetailClick = { appViewModel.onClickSendDetail() }, - modifier = Modifier - .fillMaxSize() - .gradientBackground() - .navigationBarsPadding() - .testTag("SendSuccess") - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithdrawConfirmScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onConfirm = { appViewModel.onConfirmWithdraw() }, - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithdrawErrorScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onClickScan = { navController.navigate(SendRoute.QrScanner) }, - onClickSupport = { navController.navigate(SendRoute.Support) }, - ) - } - // TODO navigate to main support screen, not inside SEND sheet - composableWithDefaultTransitions { - SupportScreen(navController) - } - composableWithDefaultTransitions { - AddTagScreen( - onBack = { navController.popBackStack() }, - onTagSelected = { tag -> - appViewModel.addTagToSelected(tag) - navController.popBackStack() - }, - tqgInputTestTag = "TagInputSend", - addButtonTestTag = "SendTagsSubmit", - ) - } - composableWithDefaultTransitions { - SendPinCheckScreen( - onBack = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(PIN_CHECK_RESULT_KEY, false) - navController.popBackStack() - }, - onSuccess = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(PIN_CHECK_RESULT_KEY, true) - navController.popBackStack() - appViewModel.setSendEvent(SendEvent.PayConfirmed) - }, - ) - } - composableWithDefaultTransitions { - val quickPayData by appViewModel.quickPayData.collectAsStateWithLifecycle() - SendQuickPayScreen( - quickPayData = requireNotNull(quickPayData), - onPaymentComplete = { paymentHash, amountWithFee -> - appViewModel.handlePaymentSuccess( - NewTransactionSheetDetails( - type = NewTransactionSheetType.LIGHTNING, - direction = NewTransactionSheetDirection.SENT, - paymentHashOrTxId = paymentHash, - sats = amountWithFee, - ), - ) - }, - onShowError = { errorMessage -> - navController.navigate(SendRoute.Error(errorMessage)) - } - ) - } - composableWithDefaultTransitions { - val route = it.toRoute() - SendErrorScreen( - errorMessage = route.errorMessage, - onRetry = { - if (startDestination == SendRoute.Recipient) { - navController.navigate(SendRoute.Recipient) { - popUpTo { inclusive = true } - } - } else { - navController.navigate(SendRoute.Success) - } - }, - onClose = { - appViewModel.hideSheet() - } - ) - } - } - } -} - -sealed interface SendRoute { - @Serializable - data object Recipient : SendRoute - - @Serializable - data object Address : SendRoute - - @Serializable - data object Amount : SendRoute - - @Serializable - data object QrScanner : SendRoute - - @Serializable - data object WithdrawConfirm : SendRoute - - @Serializable - data object WithdrawError : SendRoute - - @Serializable - data object Support : SendRoute - - @Serializable - data object AddTag : SendRoute - - @Serializable - data object PinCheck : SendRoute - - @Serializable - data object CoinSelection : SendRoute - - @Serializable - data object QuickPay : SendRoute - - @Serializable - data object FeeNav : SendRoute - - @Serializable - data object FeeRate : SendRoute - - @Serializable - data object FeeCustom : SendRoute - - @Serializable - data object Confirm : SendRoute - - @Serializable - data object Success : SendRoute - - @Serializable - data class Error(val errorMessage: String) : SendRoute -} diff --git a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt index d7252c73e..9554979dc 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.theme -import androidx.compose.animation.core.AnimationConstants import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.navigationBars @@ -112,9 +111,6 @@ object AppSwitchDefaults { ) } -const val TRANSITION_SCREEN_MS = AnimationConstants.DefaultDurationMillis.toLong() // 300ms -const val TRANSITION_SHEET_MS = 650L - object Insets { val Top: Dp @Composable diff --git a/app/src/main/java/to/bitkit/ui/utils/Transitions.kt b/app/src/main/java/to/bitkit/ui/utils/Transitions.kt deleted file mode 100644 index d40cef1c1..000000000 --- a/app/src/main/java/to/bitkit/ui/utils/Transitions.kt +++ /dev/null @@ -1,119 +0,0 @@ -package to.bitkit.ui.utils - -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.slideOutVertically -import androidx.compose.runtime.Composable -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavDeepLink -import androidx.navigation.NavGraph -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation -import to.bitkit.ui.utils.Transitions.defaultEnterTrans -import to.bitkit.ui.utils.Transitions.defaultExitTrans -import to.bitkit.ui.utils.Transitions.defaultPopEnterTrans -import to.bitkit.ui.utils.Transitions.defaultPopExitTrans -import kotlin.reflect.KType - -@Suppress("MagicNumber") -object Transitions { - val slideInHorizontally = slideInHorizontally(animationSpec = tween(), initialOffsetX = { it }) - val slideOutHorizontally = slideOutHorizontally(animationSpec = tween(), targetOffsetX = { it }) - val slideInVertically = slideInVertically(animationSpec = tween(), initialOffsetY = { it }) - val slideOutVertically = slideOutVertically(animationSpec = tween(), targetOffsetY = { it }) - - val defaultEnterTrans: (AnimatedContentTransitionScope.() -> EnterTransition?) = { - slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) - } - - val defaultExitTrans: (AnimatedContentTransitionScope.() -> ExitTransition?) = { - slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth / 3 }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) + fadeOut( - animationSpec = tween(300, easing = FastOutSlowInEasing), - targetAlpha = 0.8f - ) - } - - val defaultPopEnterTrans: (AnimatedContentTransitionScope.() -> EnterTransition?) = { - slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth / 3 }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) + fadeIn( - animationSpec = tween(300, easing = FastOutSlowInEasing), - initialAlpha = 0.8f - ) - } - - val defaultPopExitTrans: (AnimatedContentTransitionScope.() -> ExitTransition?) = { - slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) - } -} - -/** - * Construct a nested [NavGraph] with the default screen transitions. - */ -@Suppress("LongParameterList", "MaxLineLength") -inline fun NavGraphBuilder.navigationWithDefaultTransitions( - startDestination: Any, - typeMap: Map> = emptyMap(), - deepLinks: List = emptyList(), - noinline enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultEnterTrans, - noinline exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultExitTrans, - noinline popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultPopEnterTrans, - noinline popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultPopExitTrans, - noinline builder: NavGraphBuilder.() -> Unit, -) { - navigation( - startDestination = startDestination, - typeMap = typeMap, - deepLinks = deepLinks, - enterTransition = enterTransition, - exitTransition = exitTransition, - popEnterTransition = popEnterTransition, - popExitTransition = popExitTransition, - builder = builder, - ) -} - -/** - * Adds the [Composable] to the [NavGraphBuilder] with the default screen transitions. - */ -@Suppress("LongParameterList", "MaxLineLength") -inline fun NavGraphBuilder.composableWithDefaultTransitions( - typeMap: Map> = emptyMap(), - deepLinks: List = emptyList(), - noinline enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultEnterTrans, - noinline exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultExitTrans, - noinline popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultPopEnterTrans, - noinline popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultPopExitTrans, - noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, -) { - composable( - typeMap = typeMap, - deepLinks = deepLinks, - enterTransition = enterTransition, - exitTransition = exitTransition, - popEnterTransition = popEnterTransition, - popExitTransition = popExitTransition, - content = content, - ) -} diff --git a/app/src/main/java/to/bitkit/utils/Bip21Utils.kt b/app/src/main/java/to/bitkit/utils/Bip21Utils.kt index bcb4efb69..cd7ea88f7 100644 --- a/app/src/main/java/to/bitkit/utils/Bip21Utils.kt +++ b/app/src/main/java/to/bitkit/utils/Bip21Utils.kt @@ -1,6 +1,7 @@ package to.bitkit.utils import to.bitkit.models.SATS_IN_BTC +import to.bitkit.ui.nav.UriScheme object Bip21Utils { @@ -11,7 +12,7 @@ object Bip21Utils { message: String? = "Bitkit", lightningInvoice: String? = null ): String { - val builder = StringBuilder("bitcoin:$bitcoinAddress") + val builder = StringBuilder("${UriScheme.BITCOIN.withColon}$bitcoinAddress") val queryParams = mutableListOf() diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 6e7cd229f..39a76d2dd 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -9,8 +9,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 @@ -70,7 +68,6 @@ import to.bitkit.ext.maxWithdrawableSat import to.bitkit.ext.minSendableSat import to.bitkit.ext.minWithdrawableSat import to.bitkit.ext.nowMillis -import to.bitkit.ext.rawId import to.bitkit.ext.removeSpaces import to.bitkit.ext.setClipboardText import to.bitkit.ext.toHex @@ -97,13 +94,12 @@ import to.bitkit.repositories.PreActivityMetadataRepo import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo import to.bitkit.services.AppUpdaterService -import to.bitkit.ui.Routes -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.TimedSheetType +import to.bitkit.ui.nav.DeepLinkPatterns +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.removeLightningSchemes import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.toast.ToastQueueManager -import to.bitkit.ui.sheets.SendRoute -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.utils.Logger import to.bitkit.utils.jsonLogOf import java.math.BigDecimal @@ -474,15 +470,13 @@ class AppViewModel @Inject constructor( SendEvent.DismissAmountWarning -> onDismissAmountWarning() SendEvent.PayConfirmed -> onConfirmPay() SendEvent.ClearPayConfirmation -> _sendUiState.update { s -> s.copy(shouldConfirmPay = false) } - SendEvent.BackToAmount -> setSendEffect(SendEffect.PopBack(SendRoute.Amount)) + SendEvent.BackToAmount -> setSendEffect(SendEffect.PopBack(Routes.SendAmount())) SendEvent.NavToAddress -> setSendEffect(SendEffect.NavigateToAddress) } } } } - private val isMainScanner get() = currentSheet.value !is Sheet.Send - private fun onEnterManuallyClick() { resetAddressInput() setSendEffect(SendEffect.NavigateToAddress) @@ -566,7 +560,7 @@ class AppViewModel @Inject constructor( ) } refreshOnchainSendIfNeeded() - setSendEffect(SendEffect.PopBack(SendRoute.Confirm)) + setSendEffect(SendEffect.PopBack(Routes.SendConfirm)) } } @@ -744,11 +738,7 @@ class AppViewModel @Inject constructor( if (quickPayHandled) return refreshOnchainSendIfNeeded() - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Confirm)) - } else { - setSendEffect(SendEffect.NavigateToConfirm) - } + setSendEffect(SendEffect.NavigateToConfirm) return } @@ -760,11 +750,7 @@ class AppViewModel @Inject constructor( context = TAG, ) - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } private suspend fun onScanLightning(invoice: LightningInvoice, scanResult: String) { @@ -801,21 +787,11 @@ class AppViewModel @Inject constructor( if (invoice.amountSatoshis > 0uL) { Logger.info("Found amount in invoice, proceeding with payment", context = TAG) - - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Confirm)) - } else { - setSendEffect(SendEffect.NavigateToConfirm) - } + setSendEffect(SendEffect.NavigateToConfirm) return } Logger.info("No amount found in invoice, proceeding to enter amount", context = TAG) - - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } private suspend fun onScanLnurlPay(data: LnurlPayData) { @@ -850,20 +826,12 @@ class AppViewModel @Inject constructor( val quickPayHandled = handleQuickPayIfApplicable(amountSats = minSendable, lnurlPay = data) if (quickPayHandled) return - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Confirm)) - } else { - setSendEffect(SendEffect.NavigateToConfirm) - } + setSendEffect(SendEffect.NavigateToConfirm) return } Logger.info("No amount found in lnurlPay, proceeding to enter amount manually", context = TAG) - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } private fun onScanLnurlWithdraw(data: LnurlWithdrawData) { @@ -894,20 +862,16 @@ class AppViewModel @Inject constructor( return } - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } - private suspend fun onScanLnurlAuth(data: LnurlAuthData) { + private fun onScanLnurlAuth(data: LnurlAuthData) { Logger.debug("LNURL: $data", context = TAG) - if (!isMainScanner) { - hideSheet() - delay(TRANSITION_SCREEN_MS) - } - showSheet(Sheet.LnurlAuth(domain = data.domain, lnurl = data.uri, k1 = data.k1)) + mainScreenEffect( + MainScreenEffect.Navigate( + Routes.LnurlAuthSheet(domain = data.domain, lnurl = data.uri, k1 = data.k1) + ) + ) } fun requestLnurlAuth(callback: String, k1: String, domain: String) { @@ -966,13 +930,13 @@ class AppViewModel @Inject constructor( // return // } hideSheet() // hide scan sheet if opened - val nextRoute = Routes.ExternalConnection(data.url) - mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) + mainScreenEffect(MainScreenEffect.Navigate(Routes.ExternalConnection(data.url))) } private fun onScanGift(code: String, amount: ULong) { - hideSheet() // hide scan sheet if opened - showSheet(Sheet.Gift(code = code, amount = amount)) + mainScreenEffect( + MainScreenEffect.Navigate(Routes.GiftLoading(code = code, amount = amount)) + ) } private suspend fun handleQuickPayIfApplicable( @@ -1006,11 +970,7 @@ class AppViewModel @Inject constructor( Logger.debug("QuickPayData: $quickPayData", context = TAG) - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.QuickPay)) - } else { - setSendEffect(SendEffect.NavigateToQuickPay) - } + setSendEffect(SendEffect.NavigateToQuickPay) return true } @@ -1277,8 +1237,7 @@ class AppViewModel @Inject constructor( ).onSuccess { activity -> hideNewTransactionSheet() _transactionSheet.update { it.copy(isLoadingDetails = false) } - val nextRoute = Routes.ActivityDetail(activity.rawId()) - mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) + mainScreenEffect(MainScreenEffect.Navigate(Routes.ActivityDetail(activity))) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) toast(e) @@ -1301,8 +1260,7 @@ class AppViewModel @Inject constructor( ).onSuccess { activity -> hideSheet() _successSendUiState.update { it.copy(isLoadingDetails = false) } - val nextRoute = Routes.ActivityDetail(activity.rawId()) - mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) + mainScreenEffect(MainScreenEffect.Navigate(Routes.ActivityDetail(activity))) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) toast(e) @@ -1530,27 +1488,11 @@ class AppViewModel @Inject constructor( // endregion // region Sheets - private val _currentSheet: MutableStateFlow = MutableStateFlow(null) - val currentSheet = _currentSheet.asStateFlow() - - fun showSheet(sheetType: Sheet) { - viewModelScope.launch { - _currentSheet.value?.let { - _currentSheet.update { null } - delay(SCREEN_TRANSITION_DELAY_MS) - } - _currentSheet.update { sheetType } - } - } - fun hideSheet() { - if (currentSheet.value is Sheet.TimedSheet && currentTimedSheet != null) { + if (currentTimedSheet != null) { dismissTimedSheet() - } else { - _currentSheet.update { null } } } - // endregion // region Toasts @@ -1716,37 +1658,25 @@ class AppViewModel @Inject constructor( } private fun processDeeplink(uri: Uri) = viewModelScope.launch { - if (uri.toString().contains("recovery-mode")) { + // Step 1: Try Nav3 pattern matching first + val patternMatch = DeepLinkPatterns.findMatch(uri) + + // Handle recovery-mode via pattern match + if (patternMatch?.routeClass == Routes.RecoveryMode::class) { lightningRepo.setRecoveryMode(enabled = true) delay(SCREEN_TRANSITION_DELAY_MS) - mainScreenEffect( - MainScreenEffect.Navigate( - route = Routes.RecoveryMode, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } - ) - ) + mainScreenEffect(MainScreenEffect.Navigate(Routes.RecoveryMode)) return@launch } if (!walletRepo.walletExists()) return@launch + // Step 2: Fall back to Rust-based parsing for complex URIs val data = uri.toString() delay(SCREEN_TRANSITION_DELAY_MS) handleScan(data.removeLightningSchemes()) } - // TODO Temporary fix while these schemes can't be decoded - @Suppress("SpellCheckingInspection") - private fun String.removeLightningSchemes(): String { - return this - .replace("lnurl:", "") - .replace("lnurlw:", "") - .replace("lnurlc:", "") - .replace("lnurlp:", "") - } - fun checkTimedSheets() { if (backupRepo.isRestoring.value) return @@ -1776,7 +1706,7 @@ class AppViewModel @Inject constructor( ) timedSheetQueue = eligibleSheets currentTimedSheet = eligibleSheets.first() - showSheet(Sheet.TimedSheet(eligibleSheets.first())) + mainScreenEffect(MainScreenEffect.Navigate(eligibleSheets.first().toRoute())) } else { Logger.debug("No timed sheet eligible, skipping", context = "Timed sheet") } @@ -1838,7 +1768,7 @@ class AppViewModel @Inject constructor( if (nextIndex < currentQueue.size) { Logger.debug("Moving to next timed sheet in queue: ${currentQueue[nextIndex].name}") currentTimedSheet = currentQueue[nextIndex] - showSheet(Sheet.TimedSheet(currentQueue[nextIndex])) + mainScreenEffect(MainScreenEffect.Navigate(currentQueue[nextIndex].toRoute())) } else { Logger.debug("Timed sheet queue exhausted") clearTimedSheets() @@ -1898,14 +1828,7 @@ class AppViewModel @Inject constructor( if (androidReleaseInfo.buildNumber <= currentBuildNumber) return@withContext false if (androidReleaseInfo.isCritical) { - mainScreenEffect( - MainScreenEffect.Navigate( - route = Routes.CriticalUpdate, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } - ) - ) + mainScreenEffect(MainScreenEffect.Navigate(Routes.CriticalUpdate)) return@withContext false } @@ -2019,7 +1942,7 @@ sealed class SendFee(open val value: Long) { enum class SendMethod { ONCHAIN, LIGHTNING } sealed class SendEffect { - data class PopBack(val route: SendRoute) : SendEffect() + data class PopBack(val route: Routes) : SendEffect() data object NavigateToAddress : SendEffect() data object NavigateToAmount : SendEffect() data object NavigateToScan : SendEffect() @@ -2034,11 +1957,7 @@ sealed class SendEffect { } sealed class MainScreenEffect { - data class Navigate( - val route: Routes, - val navOptions: NavOptions? = null, - ) : MainScreenEffect() - + data class Navigate(val route: Routes) : MainScreenEffect() data object WipeWallet : MainScreenEffect() data class ProcessClipboardAutoRead(val data: String) : MainScreenEffect() } @@ -2083,3 +2002,11 @@ sealed interface QuickPayData { data class LnurlPay(override val sats: ULong, val callback: String) : QuickPayData } // endregion + +private fun TimedSheetType.toRoute(): Routes = when (this) { + TimedSheetType.APP_UPDATE -> Routes.TimedUpdateSheet + TimedSheetType.BACKUP -> Routes.TimedBackupSheet + TimedSheetType.NOTIFICATIONS -> Routes.TimedNotificationsSheet + TimedSheetType.QUICK_PAY -> Routes.TimedQuickPaySheet + TimedSheetType.HIGH_BALANCE -> Routes.TimedHighBalanceSheet +} diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index ac15b99ef..698308324 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -29,6 +29,7 @@ import to.bitkit.repositories.RecoveryModeException import to.bitkit.repositories.SyncSource import to.bitkit.repositories.WalletRepo import to.bitkit.ui.onboarding.LOADING_MS +import to.bitkit.ui.screens.wallets.receive.CjitEntryDetails import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger import to.bitkit.utils.isTxSyncTimeout @@ -292,6 +293,23 @@ class WalletViewModel @Inject constructor( settingsStore.update { it.copy(hideBalance = true) } } } + + // region Receive Flow State + private val _pendingCjitEntry = MutableStateFlow(null) + val pendingCjitEntry = _pendingCjitEntry.asStateFlow() + + private val _pendingCjitInvoice = MutableStateFlow(null) + val pendingCjitInvoice = _pendingCjitInvoice.asStateFlow() + + fun setPendingCjitEntry(entry: CjitEntryDetails?) = _pendingCjitEntry.update { entry } + + fun setPendingCjitInvoice(invoice: String?) = _pendingCjitInvoice.update { invoice } + + fun resetReceiveFlowState() { + _pendingCjitEntry.update { null } + _pendingCjitInvoice.update { null } + } + // endregion } // TODO rename to walletUiState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9342bafe0..d5cf42858 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ kotlin = "2.2.21" ktor = "3.3.3" lifecycle = "2.10.0" navCompose = "2.9.6" +navigation3 = "1.0.0" room = "2.8.4" slf4j = "2.0.17" haze = "1.7.1" @@ -67,6 +68,8 @@ lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmo material = { module = "com.google.android.material:material", version = "1.13.0" } navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navCompose" } navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navCompose" } +navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } +navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } From e73fbef170278f499cde0c8f1a619942638b664c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 24 Dec 2025 13:59:47 +0100 Subject: [PATCH 2/3] chore: Update deps --- app/build.gradle.kts | 6 +++--- gradle/libs.versions.toml | 19 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index deec7020b..1dba4971e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -230,9 +230,9 @@ dependencies { implementation(libs.charts) implementation(libs.haze) implementation(libs.haze.materials) - // Navigation 3 - implementation(libs.navigation3.runtime) - implementation(libs.navigation3.ui) + // Navigation + implementation(libs.navigation.runtime) + implementation(libs.navigation.ui) implementation(libs.hilt.navigation.compose) // Hilt - DI implementation(libs.hilt.android) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5cf42858..1a8df3e7f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,10 +4,9 @@ camera = "1.5.2" detekt = "1.23.8" hilt = "2.57.2" hiltAndroidx = "1.3.0" -kotlin = "2.2.21" +kotlin = "2.3.0" ktor = "3.3.3" lifecycle = "2.10.0" -navCompose = "2.9.6" navigation3 = "1.0.0" room = "2.8.4" slf4j = "2.0.17" @@ -16,17 +15,17 @@ haze = "1.7.1" [libraries] accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version = "0.36.0" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version = "0.37.3" } -activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.1" } +activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.2" } appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } -biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha04" } +biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.33" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" } # https://developer.android.com/develop/ui/compose/bom/bom-mapping -compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.00" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.01" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-ui = { group = "androidx.compose.ui", name = "ui" } @@ -40,8 +39,8 @@ core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" } core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.2.0" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } -detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.1" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.6.0" } +detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.2" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.7.0" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } @@ -66,10 +65,8 @@ lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } material = { module = "com.google.android.material:material", version = "1.13.0" } -navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navCompose" } -navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navCompose" } -navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } -navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" } +navigation-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } +navigation-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } From f80b6f418a937ea5e3f53792dafa56fb74b0c015 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 26 Dec 2025 09:54:20 -0300 Subject: [PATCH 3/3] fix: qr code decoding error message --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 39a76d2dd..9f4de8b89 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -671,7 +671,7 @@ class AppViewModel @Inject constructor( } } - private suspend fun handleScan(result: String) = withContext(bgDispatcher) { + private suspend fun handleScan(result: String, isQrCode: Boolean) = withContext(bgDispatcher) { // always reset state on new scan resetSendState() resetQuickPayData() @@ -695,8 +695,8 @@ class AppViewModel @Inject constructor( Logger.warn("Unhandled scan data: $scan", context = TAG) toast( type = Toast.ToastType.WARNING, - title = context.getString(R.string.other__scan_err_decoding), - description = context.getString(R.string.other__scan_err_interpret_title), + title = context.getString(R.string.other__qr_error_header), + description = context.getString(R.string.other__qr_error_text), ) } }