diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 11e7ebedd..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)
- // Compose Navigation
- implementation(libs.navigation.compose)
- androidTestImplementation(libs.navigation.testing)
+ // Navigation
+ implementation(libs.navigation.runtime)
+ implementation(libs.navigation.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/di/TimedSheetModule.kt b/app/src/main/java/to/bitkit/di/TimedSheetModule.kt
new file mode 100644
index 000000000..1f218c76e
--- /dev/null
+++ b/app/src/main/java/to/bitkit/di/TimedSheetModule.kt
@@ -0,0 +1,18 @@
+package to.bitkit.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineScope
+import to.bitkit.utils.timedsheets.TimedSheetManager
+
+@Module
+@InstallIn(SingletonComponent::class)
+object TimedSheetModule {
+
+ @Provides
+ fun provideTimedSheetManagerProvider(): (CoroutineScope) -> TimedSheetManager {
+ return ::TimedSheetManager
+ }
+}
diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt
index 0220b40ed..6cd90ef03 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,22 @@ fun ContentView(
LaunchedEffect(appViewModel) {
appViewModel.mainScreenEffect.collect {
when (it) {
- is MainScreenEffect.Navigate -> navController.navigate(it.route, navOptions = it.navOptions)
+ is MainScreenEffect.Navigate -> {
+ when (it.route) {
+ is Routes.CriticalUpdate -> {
+ navigator.navigateToCriticalUpdate()
+ }
+
+ else -> {
+ 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 +151,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 +201,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 +258,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 +338,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..88976316d
--- /dev/null
+++ b/app/src/main/java/to/bitkit/ui/nav/Navigator.kt
@@ -0,0 +1,64 @@
+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
+ )
+
+ fun navigateToCriticalUpdate() {
+ backStack.clear()
+ backStack.add(Routes.CriticalUpdate)
+ }
+}
+
+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..4d852b165
--- /dev/null
+++ b/app/src/main/java/to/bitkit/ui/nav/SheetSceneStrategy.kt
@@ -0,0 +1,165 @@
+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.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.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)
+ )
+
+ var isInitialExpansion by remember { mutableStateOf(true) }
+
+ LaunchedEffect(Unit) {
+ scaffoldState.bottomSheetState.expand()
+ isInitialExpansion = false
+ }
+
+ LaunchedEffect(scaffoldState.bottomSheetState) {
+ snapshotFlow { scaffoldState.bottomSheetState.currentValue }
+ .collect { value: SheetValue ->
+ if (!isInitialExpansion && value == SheetValue.Hidden) {
+ onBack()
+ }
+ }
+ }
+
+ 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..ee73ef5d2
--- /dev/null
+++ b/app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt
@@ -0,0 +1,903 @@
+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.DisposableEffect
+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