diff --git a/AGENTS.md b/AGENTS.md index 621ddfe61..efd561f20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -192,6 +192,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS be mindful of thread safety when working with mutable lists & state - ALWAYS split screen composables into parent accepting viewmodel + inner private child accepting state and callbacks `Content()` - ALWAYS name lambda parameters in a composable function using present tense, NEVER use past tense +- ALWAYS use a single root layout node in composables that emit UI - ALWAYS list 3 suggested commit messages after implementation work for the entire set of uncommitted changes - NEVER use `wheneverBlocking` in unit test expression body functions wrapped in a `= test {}` lambda - ALWAYS wrap unit tests `setUp` methods mocking suspending calls with `runBlocking`, e.g `setUp() = runBlocking { }` 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/data/backup/VssBackupClient.kt b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt index f5a6e0a0b..01a18ab60 100644 --- a/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt +++ b/app/src/main/java/to/bitkit/data/backup/VssBackupClient.kt @@ -155,7 +155,7 @@ class VssBackupClient @Inject constructor( } } - companion object Companion { + companion object { private const val TAG = "VssBackupClient" } } diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 0220b40ed..f5da38ac0 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -1,181 +1,52 @@ package to.bitkit.ui -import android.content.Intent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavDestination.Companion.hasRoute -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavHostController -import androidx.navigation.NavOptions -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import dev.chrisbanes.haze.hazeSource -import dev.chrisbanes.haze.rememberHazeState +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.serialization.Serializable -import to.bitkit.env.Env import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.Toast -import to.bitkit.models.WidgetType -import to.bitkit.ui.Routes.ExternalConnection -import to.bitkit.ui.components.AuthCheckScreen import to.bitkit.ui.components.DrawerMenu -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.components.SheetHost import to.bitkit.ui.components.TabBar -import to.bitkit.ui.components.TimedSheetType +import to.bitkit.ui.nav.MS_NAV_DELAY +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.SheetSceneStrategy +import to.bitkit.ui.nav.Transitions +import to.bitkit.ui.nav.entries.homeEntries +import to.bitkit.ui.nav.entries.settingsEntries +import to.bitkit.ui.nav.entries.sheetEntries +import to.bitkit.ui.nav.entries.transferEntries +import to.bitkit.ui.nav.entries.widgetEntries import to.bitkit.ui.onboarding.InitializingWalletView import to.bitkit.ui.onboarding.WalletRestoreErrorView import to.bitkit.ui.onboarding.WalletRestoreSuccessView -import to.bitkit.ui.screens.CriticalUpdateScreen -import to.bitkit.ui.screens.profile.CreateProfileScreen -import to.bitkit.ui.screens.profile.ProfileIntroScreen -import to.bitkit.ui.screens.recovery.RecoveryMnemonicScreen -import to.bitkit.ui.screens.recovery.RecoveryModeScreen -import to.bitkit.ui.screens.scanner.QrScanningScreen -import to.bitkit.ui.screens.scanner.SCAN_REQUEST_KEY -import to.bitkit.ui.screens.settings.DevSettingsScreen -import to.bitkit.ui.screens.settings.FeeSettingsScreen -import to.bitkit.ui.screens.settings.LdkDebugScreen -import to.bitkit.ui.screens.shop.ShopIntroScreen -import to.bitkit.ui.screens.shop.shopDiscover.ShopDiscoverScreen -import to.bitkit.ui.screens.shop.shopWebView.ShopWebViewScreen -import to.bitkit.ui.screens.transfer.FundingAdvancedScreen -import to.bitkit.ui.screens.transfer.FundingScreen -import to.bitkit.ui.screens.transfer.LiquidityScreen -import to.bitkit.ui.screens.transfer.SavingsAdvancedScreen -import to.bitkit.ui.screens.transfer.SavingsAvailabilityScreen -import to.bitkit.ui.screens.transfer.SavingsConfirmScreen -import to.bitkit.ui.screens.transfer.SavingsIntroScreen -import to.bitkit.ui.screens.transfer.SavingsProgressScreen -import to.bitkit.ui.screens.transfer.SettingUpScreen -import to.bitkit.ui.screens.transfer.SpendingAdvancedScreen -import to.bitkit.ui.screens.transfer.SpendingAmountScreen -import to.bitkit.ui.screens.transfer.SpendingConfirmScreen -import to.bitkit.ui.screens.transfer.SpendingIntroScreen -import to.bitkit.ui.screens.transfer.TransferIntroScreen -import to.bitkit.ui.screens.transfer.external.ExternalAmountScreen -import to.bitkit.ui.screens.transfer.external.ExternalConfirmScreen -import to.bitkit.ui.screens.transfer.external.ExternalConnectionScreen -import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen -import to.bitkit.ui.screens.transfer.external.ExternalNodeViewModel -import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen -import to.bitkit.ui.screens.transfer.external.LnurlChannelScreen -import to.bitkit.ui.screens.wallets.HomeScreen -import to.bitkit.ui.screens.wallets.SavingsWalletScreen -import to.bitkit.ui.screens.wallets.SpendingWalletScreen -import to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen -import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen -import to.bitkit.ui.screens.wallets.activity.AllActivityScreen -import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorSheet -import to.bitkit.ui.screens.wallets.activity.TagSelectorSheet -import to.bitkit.ui.screens.wallets.receive.ReceiveSheet -import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen -import to.bitkit.ui.screens.widgets.AddWidgetsScreen -import to.bitkit.ui.screens.widgets.WidgetsIntroScreen -import to.bitkit.ui.screens.widgets.blocks.BlocksEditScreen -import to.bitkit.ui.screens.widgets.blocks.BlocksPreviewScreen -import to.bitkit.ui.screens.widgets.blocks.BlocksViewModel -import to.bitkit.ui.screens.widgets.calculator.CalculatorPreviewScreen -import to.bitkit.ui.screens.widgets.facts.FactsEditScreen -import to.bitkit.ui.screens.widgets.facts.FactsPreviewScreen -import to.bitkit.ui.screens.widgets.facts.FactsViewModel -import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditScreen -import to.bitkit.ui.screens.widgets.headlines.HeadlinesPreviewScreen -import to.bitkit.ui.screens.widgets.headlines.HeadlinesViewModel -import to.bitkit.ui.screens.widgets.price.PriceEditScreen -import to.bitkit.ui.screens.widgets.price.PricePreviewScreen -import to.bitkit.ui.screens.widgets.price.PriceViewModel -import to.bitkit.ui.screens.widgets.weather.WeatherEditScreen -import to.bitkit.ui.screens.widgets.weather.WeatherPreviewScreen -import to.bitkit.ui.screens.widgets.weather.WeatherViewModel -import to.bitkit.ui.settings.AboutScreen -import to.bitkit.ui.settings.AdvancedSettingsScreen -import to.bitkit.ui.settings.BackupSettingsScreen -import to.bitkit.ui.settings.BlocktankRegtestScreen -import to.bitkit.ui.settings.CJitDetailScreen -import to.bitkit.ui.settings.ChannelOrdersScreen -import to.bitkit.ui.settings.LanguageSettingsScreen -import to.bitkit.ui.settings.LogDetailScreen -import to.bitkit.ui.settings.LogsScreen -import to.bitkit.ui.settings.OrderDetailScreen -import to.bitkit.ui.settings.SecuritySettingsScreen -import to.bitkit.ui.settings.SettingsScreen -import to.bitkit.ui.settings.advanced.AddressViewerScreen -import to.bitkit.ui.settings.advanced.CoinSelectPreferenceScreen -import to.bitkit.ui.settings.advanced.ElectrumConfigScreen -import to.bitkit.ui.settings.advanced.RgsServerScreen -import to.bitkit.ui.settings.appStatus.AppStatusScreen -import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsIntroScreen -import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsSettings -import to.bitkit.ui.settings.backups.ResetAndRestoreScreen -import to.bitkit.ui.settings.general.DefaultUnitSettingsScreen -import to.bitkit.ui.settings.general.GeneralSettingsScreen -import to.bitkit.ui.settings.general.LocalCurrencySettingsScreen -import to.bitkit.ui.settings.general.TagsSettingsScreen -import to.bitkit.ui.settings.general.WidgetsSettingsScreen -import to.bitkit.ui.settings.lightning.ChannelDetailScreen -import to.bitkit.ui.settings.lightning.CloseConnectionScreen -import to.bitkit.ui.settings.lightning.LightningConnectionsScreen import to.bitkit.ui.settings.lightning.LightningConnectionsViewModel -import to.bitkit.ui.settings.pin.ChangePinConfirmScreen -import to.bitkit.ui.settings.pin.ChangePinNewScreen -import to.bitkit.ui.settings.pin.ChangePinResultScreen -import to.bitkit.ui.settings.pin.ChangePinScreen -import to.bitkit.ui.settings.pin.DisablePinScreen -import to.bitkit.ui.settings.quickPay.QuickPayIntroScreen -import to.bitkit.ui.settings.quickPay.QuickPaySettingsScreen -import to.bitkit.ui.settings.support.ReportIssueResultScreen -import to.bitkit.ui.settings.support.ReportIssueScreen -import to.bitkit.ui.settings.support.SupportScreen -import to.bitkit.ui.settings.transactionSpeed.CustomFeeSettingsScreen -import to.bitkit.ui.settings.transactionSpeed.TransactionSpeedSettingsScreen -import to.bitkit.ui.sheets.BackgroundPaymentsIntroSheet -import to.bitkit.ui.sheets.BackupRoute -import to.bitkit.ui.sheets.BackupSheet -import to.bitkit.ui.sheets.ForceTransferSheet -import to.bitkit.ui.sheets.GiftSheet -import to.bitkit.ui.sheets.HighBalanceWarningSheet -import to.bitkit.ui.sheets.LnurlAuthSheet -import to.bitkit.ui.sheets.PinSheet -import to.bitkit.ui.sheets.QuickPayIntroSheet -import to.bitkit.ui.sheets.SendSheet -import to.bitkit.ui.sheets.UpdateSheet -import to.bitkit.ui.theme.TRANSITION_SHEET_MS import to.bitkit.ui.utils.AutoReadClipboardHandler -import to.bitkit.ui.utils.RequestNotificationPermissions -import to.bitkit.ui.utils.Transitions -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.ui.utils.navigationWithDefaultTransitions import to.bitkit.utils.Logger import to.bitkit.viewmodels.ActivityListViewModel import to.bitkit.viewmodels.AppViewModel @@ -184,11 +55,12 @@ import to.bitkit.viewmodels.BlocktankViewModel import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.MainScreenEffect import to.bitkit.viewmodels.RestoreState +import to.bitkit.viewmodels.SendEffect import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel -@Suppress("CyclomaticComplexMethod") +@Suppress("CyclomaticComplexMethod", "LongMethod") @Composable fun ContentView( appViewModel: AppViewModel, @@ -201,10 +73,11 @@ fun ContentView( backupsViewModel: BackupsViewModel, modifier: Modifier = Modifier, ) { - val navController = rememberNavController() + val backStack = rememberNavBackStack(Routes.Home) + val navigator = remember(backStack) { Navigator(backStack) } + val lightningConnectionsViewModel = hiltViewModel() val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val context = LocalContext.current val lifecycle = LocalLifecycleOwner.current.lifecycle val walletUiState by walletViewModel.walletState.collectAsStateWithLifecycle() @@ -253,12 +126,11 @@ fun ContentView( LaunchedEffect(appViewModel) { appViewModel.mainScreenEffect.collect { when (it) { - is MainScreenEffect.Navigate -> navController.navigate(it.route, navOptions = it.navOptions) + is MainScreenEffect.Navigate -> navigator.navigate(it.route) is MainScreenEffect.ProcessClipboardAutoRead -> { - val isOnHome = navController.currentDestination?.hasRoute() == true - if (!isOnHome) { - navController.navigateToHome() - delay(100) // Small delay to ensure navigation completes + if (!navigator.isAtHome()) { + navigator.navigateToHome() + delay(MS_NAV_DELAY) } appViewModel.onScanResult(it.data) } @@ -268,6 +140,30 @@ fun ContentView( } } + // Handle Send flow navigation effects + LaunchedEffect(appViewModel, navigator) { + appViewModel.sendEffect.collect { effect -> + when (effect) { + is SendEffect.NavigateToAddress -> navigator.navigate(Routes.Send.Address) + is SendEffect.NavigateToAmount -> navigator.navigate(Routes.Send.Amount()) + is SendEffect.NavigateToScan -> navigator.navigate(Routes.Send.QrScanner) + is SendEffect.NavigateToCoinSelection -> navigator.navigate(Routes.Send.CoinSelection) + is SendEffect.NavigateToConfirm -> navigator.navigate(Routes.Send.Confirm) + is SendEffect.NavigateToQuickPay -> navigator.navigate(Routes.Send.QuickPay) + is SendEffect.NavigateToWithdrawConfirm -> navigator.navigate(Routes.Send.WithdrawConfirm) + is SendEffect.NavigateToWithdrawError -> navigator.navigate(Routes.Send.WithdrawError) + is SendEffect.NavigateToFee -> navigator.navigate(Routes.Send.FeeRate) + is SendEffect.NavigateToFeeCustom -> navigator.navigate(Routes.Send.FeeCustom) + is SendEffect.PaymentSuccess -> { + appViewModel.clearClipboardForAutoRead() + navigator.navigate(Routes.Send.Success) + } + + is SendEffect.PopBack -> navigator.popBackTo(effect.route) + } + } + } + var walletIsInitializing by remember { mutableStateOf(nodeLifecycleState == NodeLifecycleState.Initializing) } var walletInitShouldFinish by remember { mutableStateOf(false) } @@ -294,7 +190,6 @@ fun ContentView( var restoreRetryCount by remember { mutableIntStateOf(0) } if (walletIsInitializing) { - // TODO ADAPT THIS LOGIC TO WORK WITH LightningNodeService if (nodeLifecycleState is NodeLifecycleState.ErrorStarting) { WalletRestoreErrorView( retryCount = restoreRetryCount, @@ -352,1606 +247,82 @@ 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() - } - ) - } - - 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() }) - } - - TimedSheetType.BACKUP -> { - BackupSheet( - sheet = Sheet.Backup(BackupRoute.Intro), - onDismiss = { appViewModel.dismissTimedSheet() } - ) - } - - TimedSheetType.NOTIFICATIONS -> { - BackgroundPaymentsIntroSheet( - onContinue = { - appViewModel.dismissTimedSheet(skipQueue = true) - navController.navigate(Routes.BackgroundPaymentsSettings) - settingsViewModel.setBgPaymentsIntroSeen(true) - }, - ) - } - - TimedSheetType.QUICK_PAY -> { - QuickPayIntroSheet( - onContinue = { - appViewModel.dismissTimedSheet(skipQueue = true) - navController.navigate(Routes.QuickPaySettings) - }, - ) - } - 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) - } - ) - } - } - } - } - } - ) { - 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, - ) + AutoReadClipboardHandler() - if (showTabBar) { - TabBar( - onSendClick = { appViewModel.showSheet(Sheet.Send()) }, - onReceiveClick = { appViewModel.showSheet(Sheet.Receive) }, - onScanClick = { navController.navigateToScanner() }, - modifier = Modifier.align(Alignment.BottomCenter) + 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, ) - } - } - } - DrawerMenu( - drawerState = drawerState, - rootNavController = navController, - hasSeenWidgetsIntro = hasSeenWidgetsIntro, - hasSeenShopIntro = hasSeenShopIntro, - modifier = Modifier.align(Alignment.TopEnd), - ) - } - } -} + settingsEntries( + navigator = navigator, + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, + currencyViewModel = currencyViewModel, + lightningConnectionsViewModel = lightningConnectionsViewModel, + ) -@Composable -private fun RootNavHost( - navController: NavHostController, - drawerState: DrawerState, - walletViewModel: WalletViewModel, - appViewModel: AppViewModel, - activityListViewModel: ActivityListViewModel, - settingsViewModel: SettingsViewModel, - currencyViewModel: CurrencyViewModel, - transferViewModel: TransferViewModel, -) { - val scope = rememberCoroutineScope() + transferEntries( + navigator = navigator, + appViewModel = appViewModel, + walletViewModel = walletViewModel, + transferViewModel = transferViewModel, + settingsViewModel = settingsViewModel, + ) - 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) + widgetEntries( + navigator = navigator, + currencyViewModel = currencyViewModel, + settingsViewModel = settingsViewModel, + ) - // 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 + sheetEntries( + navigator = navigator, + appViewModel = appViewModel, + walletViewModel = walletViewModel, + activityListViewModel = activityListViewModel, + transferViewModel = transferViewModel, ) - }, - ) - } - 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() }, + AnimatedVisibility( + visible = navigator.shouldShowTabBar(), + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + modifier = Modifier.align(Alignment.BottomCenter), + ) { + TabBar( + onSendClick = { navigator.navigate(Routes.Send.Recipient) }, + onReceiveClick = { navigator.navigate(Routes.Receive.Qr) }, + onScanClick = { navigator.navigate(Routes.QrScanner) }, ) } } - } - } -} - -// 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, + DrawerMenu( 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, + navigator = navigator, + hasSeenWidgetsIntro = hasSeenWidgetsIntro, + hasSeenShopIntro = hasSeenShopIntro, ) } } } - -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..0df8a27b4 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.Onboarding.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..7eb502cf7 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.Settings.ResetAndRestore) } } }, - 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..50b9b5233 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,70 +56,92 @@ private val drawerWidth = 200.dp @Composable fun DrawerMenu( drawerState: DrawerState, - rootNavController: NavController, + navigator: Navigator, hasSeenWidgetsIntro: Boolean, hasSeenShopIntro: Boolean, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - Scrim( - visible = drawerState.currentValue == DrawerValue.Open, - onClick = { - scope.launch { - drawerState.close() - } - }, - modifier = Modifier - .fillMaxSize() - .zIndex(zIndexScrim) - ) + Box(modifier = modifier.fillMaxSize()) { + Scrim( + visible = drawerState.currentValue == DrawerValue.Open, + onClick = { + scope.launch { + drawerState.close() + } + }, + modifier = Modifier + .fillMaxSize() + .zIndex(zIndexScrim) + ) - AnimatedVisibility( - visible = drawerState.currentValue == DrawerValue.Open, - enter = slideInHorizontally( - initialOffsetX = { it } - ), - exit = slideOutHorizontally( - targetOffsetX = { it } - ), - modifier = modifier.then( - Modifier + AnimatedVisibility( + visible = drawerState.currentValue == DrawerValue.Open, + enter = slideInHorizontally( + initialOffsetX = { it } + ), + exit = slideOutHorizontally( + targetOffsetX = { it } + ), + modifier = Modifier + .align(Alignment.TopEnd) .fillMaxHeight() .zIndex(zIndexMenu) .blockPointerInputPassthrough() - ) - ) { - Menu( - rootNavController = rootNavController, - drawerState = drawerState, - onClickAddWidget = { - if (!hasSeenWidgetsIntro) { - rootNavController.navigateIfNotCurrent(Routes.WidgetsIntro) - } else { - rootNavController.navigateIfNotCurrent(Routes.AddWidget) - } - }, - onClickShop = { - if (!hasSeenShopIntro) { - rootNavController.navigateIfNotCurrent(Routes.ShopIntro) - } else { - rootNavController.navigateIfNotCurrent(Routes.ShopDiscover) - } - }, - ) + ) { + MenuContent( + onWalletClick = { + if (!navigator.isAtHome()) navigator.navigateToHome() + scope.launch { drawerState.close() } + }, + onActivityClick = { + navigator.navigate(Routes.Activity.All) + scope.launch { drawerState.close() } + }, + onContactsClick = null, // TODO IMPLEMENT CONTACTS + onProfileClick = null, // TODO IMPLEMENT PROFILE + onWidgetsClick = { + if (!hasSeenWidgetsIntro) { + navigator.navigate(Routes.Widgets.Intro) + } else { + navigator.navigate(Routes.Widgets.Add) + } + scope.launch { drawerState.close() } + }, + onShopClick = { + if (!hasSeenShopIntro) { + navigator.navigate(Routes.Shop.Intro) + } else { + navigator.navigate(Routes.Shop.Discover) + } + scope.launch { drawerState.close() } + }, + onSettingsClick = { + navigator.navigate(Routes.Settings.Main) + 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 +154,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 +206,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,15 +295,19 @@ 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), - ) + Box(modifier = Modifier.fillMaxSize()) { + Box(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..2a0250d3b 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -1,55 +1,58 @@ package to.bitkit.ui.components import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Column 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.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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.draw.clip +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset 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.shared.modifiers.sheetHeight import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.Colors +import kotlin.math.abs +import kotlin.math.roundToInt -enum class SheetSize { LARGE, MEDIUM, SMALL, CALENDAR; } - -private val sheetContainerColor = Color(0xFF141414) // Equivalent to White08 on a Black background +private const val MS_DURATION_ANIM = 300 +private const val THRESHOLD_DISMISS = 0.33f +private const val VELOCITY_DISMISS = 2500f +private const val OFFSET_MIN_DRAG = -0.1f +private val sheetContainerColor = Color.Black -@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 -} +enum class SheetSize { LARGE, MEDIUM, SMALL, CALENDAR; } -/**@param priority Priority levels for timed sheets (higher number = higher priority)*/ +/** @param priority Priority levels for timed sheets (higher number = higher priority)*/ enum class TimedSheetType(val priority: Int) { APP_UPDATE(priority = 5), BACKUP(priority = 4), @@ -58,60 +61,123 @@ enum class TimedSheetType(val priority: Int) { HIGH_BALANCE(priority = 1) } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SheetHost( - shouldExpand: Boolean, - onDismiss: () -> Unit = {}, - sheets: @Composable ColumnScope.() -> Unit, + sheetSize: SheetSize, + onDismiss: () -> Unit, content: @Composable () -> Unit, ) { val scope = rememberCoroutineScope() - val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - ) + var sheetHeightPx by remember { mutableFloatStateOf(0f) } + val offsetY = remember { Animatable(1f) } + var sheetVisible by remember { mutableStateOf(false) } + val velocityTracker = remember { VelocityTracker() } - // Automatically expand or hide the bottom sheet based on bool flag - LaunchedEffect(shouldExpand) { - if (shouldExpand) { - scaffoldState.bottomSheetState.expand() - } else { - scaffoldState.bottomSheetState.hide() - } + LaunchedEffect(Unit) { + sheetVisible = true + offsetY.animateTo(0f, animationSpec = tween(MS_DURATION_ANIM)) } - // 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) { + fun dismiss() { + scope.launch { + offsetY.animateTo(1f, animationSpec = tween(MS_DURATION_ANIM)) + sheetVisible = false onDismiss() } } + BackHandler(enabled = offsetY.value < 1f) { + dismiss() + } + Box(modifier = Modifier.fillMaxSize()) { - BottomSheetScaffold( - scaffoldState = scaffoldState, - sheetPeekHeight = 0.dp, - sheetShape = AppShapes.sheet, - sheetContent = sheets, - sheetDragHandle = { SheetDragHandle() }, - sheetContainerColor = sheetContainerColor, - sheetContentColor = MaterialTheme.colorScheme.onSurface, - ) { - content() + Scrim( + isVisible = offsetY.value < 1f, + progress = 1f - offsetY.value, + onClick = { dismiss() }, + ) - // Dismiss on back - BackHandler(enabled = scaffoldState.bottomSheetState.isVisible) { - scope.launch { - scaffoldState.bottomSheetState.hide() - onDismiss() - } - } + // Fixed background extension - covers gap when sheet drags up + val density = LocalDensity.current + if (sheetVisible && sheetHeightPx > 0f) { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(with(density) { (sheetHeightPx * abs(OFFSET_MIN_DRAG)).toDp() }) + .background(sheetContainerColor) + ) + } - Scrim(scaffoldState.bottomSheetState) { - scope.launch { - scaffoldState.bottomSheetState.hide() - onDismiss() + AnimatedVisibility( + visible = sheetVisible, + enter = slideInVertically { it } + fadeIn(), + exit = slideOutVertically { it } + fadeOut(), + modifier = Modifier.align(Alignment.BottomCenter), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .offset { IntOffset(0, (offsetY.value * sheetHeightPx).roundToInt()) } + .onSizeChanged { sheetHeightPx = it.height.toFloat() } + .clip(AppShapes.sheet) + .background(sheetContainerColor) + .fillMaxWidth() + .pointerInput(Unit) { + var dragPosition = 0f + detectVerticalDragGestures( + onDragStart = { + velocityTracker.resetTracking() + dragPosition = 0f + }, + onDragEnd = { + val velocity = velocityTracker.calculateVelocity().y + val shouldDismiss = velocity > VELOCITY_DISMISS || + offsetY.value > THRESHOLD_DISMISS + scope.launch { + if (shouldDismiss) { + offsetY.animateTo( + 1f, + animationSpec = tween(MS_DURATION_ANIM) + ) + sheetVisible = false + onDismiss() + } else { + offsetY.animateTo( + 0f, + animationSpec = tween(MS_DURATION_ANIM) + ) + } + } + }, + onVerticalDrag = { change, dragAmount -> + dragPosition += dragAmount + velocityTracker.addPosition( + change.uptimeMillis, + Offset(0f, dragPosition) + ) + val newOffset = offsetY.value + (dragAmount / sheetHeightPx) + scope.launch { + offsetY.snapTo(newOffset.coerceIn(OFFSET_MIN_DRAG, 1f)) + } + } + ) + } + ) { + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .fillMaxWidth() + .background(Colors.White08) + ) { + SheetDragHandle() + } + Box( + modifier = Modifier + .sheetHeight(sheetSize) + .navigationBarsPadding() + ) { + content() } } } @@ -119,18 +185,17 @@ fun SheetHost( } @Composable -@OptIn(ExperimentalMaterial3Api::class) private fun Scrim( - bottomSheetState: SheetState, + isVisible: Boolean, + progress: Float, onClick: () -> Unit, ) { - val isBottomSheetVisible = bottomSheetState.targetValue != SheetValue.Hidden val scrimAlpha by animateFloatAsState( - targetValue = if (isBottomSheetVisible) 0.5f else 0f, - animationSpec = tween(durationMillis = 300), + targetValue = if (isVisible) progress * 0.5f else 0f, + animationSpec = tween(durationMillis = MS_DURATION_ANIM), label = "sheetScrimAlpha" ) - if (scrimAlpha > 0f || isBottomSheetVisible) { + if (scrimAlpha > 0f || isVisible) { Box( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt index 8c1a36fb8..9bdf63a53 100644 --- a/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt +++ b/app/src/main/java/to/bitkit/ui/components/SuggestionCard.kt @@ -48,11 +48,11 @@ private const val MAX_ALPHA_GRADIENT = 0.9f @Composable fun SuggestionCard( - modifier: Modifier = Modifier, gradientColor: Color, title: String, description: String, @DrawableRes icon: Int, + modifier: Modifier = Modifier, onClose: (() -> Unit)? = null, size: Int = 152, disableGlow: Boolean = false, @@ -122,7 +122,7 @@ fun SuggestionCard( painter = painterResource(icon), contentDescription = null, contentScale = ContentScale.FillHeight, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), ) if (onClose != null) { @@ -184,7 +184,7 @@ private fun Preview() { description = stringResource(item.description), icon = item.icon, onClose = {}, - onClick = {}, // All cards are clickable + onClick = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index aef3abf9d..a61679c27 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -56,6 +56,7 @@ import to.bitkit.models.Toast import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import kotlin.math.abs import kotlin.math.roundToInt private const val DISMISS_THRESHOLD_DP = 40 @@ -120,8 +121,8 @@ fun ToastView( } coroutineScope.launch { - val horizontalSwipeDistance = kotlin.math.abs(dragOffsetX.value) - val verticalSwipeDistance = kotlin.math.abs(dragOffsetY.value) + val horizontalSwipeDistance = abs(dragOffsetX.value) + val verticalSwipeDistance = abs(dragOffsetY.value) // Determine if this is primarily horizontal or vertical swipe val isHorizontalSwipe = horizontalSwipeDistance > verticalSwipeDistance 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..0ef703380 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/DeepLinks.kt @@ -0,0 +1,175 @@ +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 supported deep link patterns for type-safe documentation and early pattern matching. + * + * Note: [com.synonym.bitkitcore.decode] remains the primary parser for complex Bitcoin/Lightning URIs. + */ +object DeepLinkPatterns { + // bitkit:// scheme patterns + val RECOVERY_MODE = DeepLinkPattern( + routeClass = Routes.Recovery.Mode::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.Send.Address::class, + uriPattern = "${UriScheme.BITCOIN.withSlashes}{address}".toUri() + ) + + // lightning:// scheme patterns + val SEND_LIGHTNING = DeepLinkPattern( + routeClass = Routes.Send.Address::class, + uriPattern = "${UriScheme.LIGHTNING.withSlashes}{invoice}".toUri() + ) + + // lnurl:// scheme patterns + val LNURL_PAY = DeepLinkPattern( + routeClass = Routes.Send.Address::class, + uriPattern = "${UriScheme.LNURL_PAY.withSlashes}{data}".toUri() + ) + + val LNURL_WITHDRAW = DeepLinkPattern( + routeClass = Routes.Receive.Qr::class, + uriPattern = "${UriScheme.LNURL_WITHDRAW.withSlashes}{data}".toUri() + ) + + val LNURL_CHANNEL = DeepLinkPattern( + routeClass = Routes.Sheet.LnurlChannel::class, + uriPattern = "${UriScheme.LNURL_CHANNEL.withSlashes}{data}".toUri() + ) + + // https:// scheme patterns (App Links) + val TREASURE_HUNT = DeepLinkPattern( + routeClass = Routes.Gift.Loading::class, + uriPattern = "${UriScheme.HTTPS.withSlashes}www.bitkit.to/treasure-hunt".toUri() + ) + + 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 via bitkit-core +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..9ec4513e8 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/Navigator.kt @@ -0,0 +1,60 @@ +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) { + if (backStack.lastOrNull() != route) { + 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.Activity.All -> true + else -> false + } +} + +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..23f221581 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/Routes.kt @@ -0,0 +1,553 @@ +package to.bitkit.ui.nav + +import androidx.compose.runtime.Stable +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable +import com.synonym.bitkitcore.Activity as BitkitCoreActivity + +@Stable +sealed interface Routes : NavKey { + + @Serializable + data object Home : Routes + + @Serializable + data object Savings : Routes + + @Serializable + data object Spending : Routes + + @Serializable + data object QrScanner : Routes + + @Serializable + data object AppStatus : Routes + + @Serializable + data object Support : Routes + + @Serializable + data object BuyIntro : Routes + + @Serializable + data object CriticalUpdate : Routes + + @Serializable + data class AuthCheck( + val showLogoOnPin: Boolean = false, + val requirePin: Boolean = false, + val requireBiometrics: Boolean = false, + val onSuccessActionId: String, + ) : Routes + + object Activity { + @Serializable + data object All : Routes + + @Serializable + data class Detail(val activity: BitkitCoreActivity) : Routes + + @Serializable + data class Explore(val id: String) : Routes + + @Serializable + data object DateRangeSelectorSheet : Routes + + @Serializable + data object TagSelectorSheet : Routes + } + + object 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 + } + + object Settings { + @Serializable + data object Main : Routes + + @Serializable + data object General : Routes + + @Serializable + data object NodeInfo : Routes + + @Serializable + data object TransactionSpeed : Routes + + @Serializable + data object Widgets : Routes + + @Serializable + data object Tags : Routes + + @Serializable + data object Advanced : Routes + + @Serializable + data object CoinSelectPreference : Routes + + @Serializable + data object ElectrumConfig : Routes + + @Serializable + data object RgsServer : Routes + + @Serializable + data object AddressViewer : Routes + + @Serializable + data object About : Routes + + @Serializable + data object CustomFee : Routes + + @Serializable + data object Security : Routes + + @Serializable + data object DisablePin : Routes + + @Serializable + data object DefaultUnit : Routes + + @Serializable + data object LocalCurrency : Routes + + @Serializable + data object BackupSettings : Routes + + @Serializable + data object ResetAndRestore : Routes + + @Serializable + data object LightningConnections : Routes + + @Serializable + data object ChannelDetail : Routes + + @Serializable + data object CloseConnection : Routes + + @Serializable + data object Fee : Routes + + @Serializable + data object Language : Routes + + object Dev { + @Serializable + data object Main : Routes + + @Serializable + data object ChannelOrders : Routes + + @Serializable + data object Regtest : Routes + + @Serializable + data object LdkDebug : Routes + + @Serializable + data class OrderDetail(val orderId: String) : Routes + + @Serializable + data class CjitDetail(val entryId: String) : Routes + + object Log { + @Serializable + data object List : Routes + + @Serializable + data class Detail(val fileName: String) : Routes + } + } + } + + object Profile { + @Serializable + data object Intro : Routes + + @Serializable + data object Create : Routes + } + + object Widgets { + @Serializable + data object Intro : Routes + + @Serializable + data object Add : Routes + + object Headlines { + @Serializable + data object Main : Routes + + @Serializable + data object Preview : Routes + + @Serializable + data object Edit : Routes + } + + object Facts { + @Serializable + data object Main : Routes + + @Serializable + data object Preview : Routes + + @Serializable + data object Edit : Routes + } + + object Blocks { + @Serializable + data object Main : Routes + + @Serializable + data object Preview : Routes + + @Serializable + data object Edit : Routes + } + + object Weather { + @Serializable + data object Main : Routes + + @Serializable + data object Preview : Routes + + @Serializable + data object Edit : Routes + } + + object Price { + @Serializable + data object Main : Routes + + @Serializable + data object Preview : Routes + + @Serializable + data object Edit : Routes + } + + object Calculator { + @Serializable + data object Preview : Routes + } + } + + object Send { + @Serializable + data object Recipient : Routes + + @Serializable + data object Address : Routes + + @Serializable + data class Amount(val prefill: String? = null) : Routes + + @Serializable + data object QrScanner : Routes + + @Serializable + data object CoinSelection : Routes + + @Serializable + data object FeeRate : Routes + + @Serializable + data object FeeCustom : Routes + + @Serializable + data object Confirm : Routes + + @Serializable + data object Success : Routes + + @Serializable + data class Error(val message: String) : Routes + + @Serializable + data object WithdrawConfirm : Routes + + @Serializable + data object WithdrawError : Routes + + @Serializable + data object Support : Routes + + @Serializable + data object AddTag : Routes + + @Serializable + data object PinCheck : Routes + + @Serializable + data object QuickPay : Routes + } + + object Receive { + @Serializable + data object Qr : Routes + + @Serializable + data object Amount : Routes + + @Serializable + data object Confirm : Routes + + @Serializable + data object ConfirmInbound : Routes + + @Serializable + data object Liquidity : Routes + + @Serializable + data object LiquidityAdditional : Routes + + @Serializable + data object EditInvoice : Routes + + @Serializable + data object AddTag : Routes + + @Serializable + data object GeoBlock : Routes + } + + object Pin { + @Serializable + data class Prompt(val showLaterButton: Boolean = false) : Routes + + @Serializable + data object Choose : Routes + + @Serializable + data class Confirm(val pin: String) : Routes + + @Serializable + data object Biometrics : Routes + + @Serializable + data class Result(val isBioOn: Boolean) : Routes + + object Change { + @Serializable + data object Start : Routes + + @Serializable + data object New : Routes + + @Serializable + data class Confirm(val newPin: String) : Routes + + @Serializable + data object Result : Routes + } + } + + object Backup { + @Serializable + data object Intro : Routes + + @Serializable + data object ShowMnemonic : Routes + + @Serializable + data object ShowPassphrase : Routes + + @Serializable + data object ConfirmMnemonic : Routes + + @Serializable + data object ConfirmPassphrase : Routes + + @Serializable + data object Warning : Routes + + @Serializable + data object Success : Routes + + @Serializable + data object MultipleDevices : Routes + + @Serializable + data object Metadata : Routes + } + + object Gift { + @Serializable + data class Loading(val code: String, val amount: ULong) : Routes + + @Serializable + data object Used : Routes + + @Serializable + data object UsedUp : Routes + + @Serializable + data object Error : Routes + + @Serializable + data object Success : Routes + } + + object Sheet { + @Serializable + data object Update : Routes + + @Serializable + data object Backup : Routes + + @Serializable + data object Notifications : Routes + + @Serializable + data object QuickPay : Routes + + @Serializable + data object HighBalance : Routes + + @Serializable + data object ForceTransfer : Routes + + @Serializable + data class LnurlChannel(val uri: String, val callback: String, val k1: String) : Routes + + @Serializable + data class LnurlAuth(val domain: String, val lnurl: String, val k1: String) : Routes + } + + object External { + @Serializable + data class Connection(val scannedNodeUri: String? = null) : Routes + + @Serializable + data object Amount : Routes + + @Serializable + data object Confirm : Routes + + @Serializable + data object Success : Routes + + @Serializable + data object FeeCustom : Routes + + @Serializable + data object NodeScanner : Routes + } + + object Transfer { + @Serializable + data object Intro : Routes + + @Serializable + data object Liquidity : Routes + + @Serializable + data object SettingUp : Routes + + @Serializable + data object Funding : Routes + + @Serializable + data object FundingAdvanced : Routes + + object ToSavings { + @Serializable + data object Intro : Routes + + @Serializable + data object Availability : Routes + + @Serializable + data object Confirm : Routes + + @Serializable + data object Advanced : Routes + + @Serializable + data object Progress : Routes + } + + object ToSpending { + @Serializable + data object Intro : Routes + + @Serializable + data object Amount : Routes + + @Serializable + data object Confirm : Routes + + @Serializable + data object Advanced : Routes + } + } + + object QuickPay { + @Serializable + data object Intro : Routes + + @Serializable + data object Settings : Routes + } + + object BackgroundPayments { + @Serializable + data object Intro : Routes + + @Serializable + data object Settings : Routes + } + + object Shop { + @Serializable + data object Intro : Routes + + @Serializable + data object Discover : Routes + + @Serializable + data class WebView(val page: String, val title: String) : Routes + } + + object ReportIssue { + @Serializable + data object Form : Routes + + @Serializable + data object Success : Routes + + @Serializable + data object Failure : Routes + } + + object Recovery { + @Serializable + data object Mode : Routes + + @Serializable + data object Mnemonic : Routes + } +} 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..7c74fb7b6 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/SheetSceneStrategy.kt @@ -0,0 +1,62 @@ +package to.bitkit.ui.nav + +import androidx.compose.runtime.Composable +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 to.bitkit.ui.components.SheetHost +import to.bitkit.ui.components.SheetSize + +class SheetSceneStrategy : SceneStrategy { + + override fun SceneStrategyScope.calculateScene(entries: List>): Scene? { + val lastEntry = entries.lastOrNull() + val sheetProperties = lastEntry?.metadata?.get(KEY_SHEET) as? SheetProperties + return sheetProperties?.let { props -> + @Suppress("UNCHECKED_CAST") + SheetScene( + key = lastEntry.contentKey as T, + previousEntries = entries.dropLast(1), + overlaidEntries = entries.dropLast(1), + entry = lastEntry, + sheetSize = props.size, + onBack = onBack, + ) + } + } + + companion object { + fun sheet(size: SheetSize = SheetSize.LARGE): Map = mapOf( + KEY_SHEET to SheetProperties(size), + ) + + internal const val KEY_SHEET = "bitkit_sheet" + } +} + +data class SheetProperties( + val size: SheetSize = SheetSize.LARGE, +) + +internal class SheetScene( + 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) = { + SheetHost( + sheetSize = sheetSize, + onDismiss = onBack, + ) { + entry.Content() + } + } +} 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..bd0e07b1c --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/HomeEntries.kt @@ -0,0 +1,292 @@ +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 + +@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.Activity.Detail(it)) }, + onTagClick = { navigator.navigate(Routes.Activity.TagSelectorSheet) }, + onDateRangeClick = { navigator.navigate(Routes.Activity.DateRangeSelectorSheet) }, + onEmptyActivityRowClick = { navigator.navigate(Routes.Receive.Qr) }, + ) + } + + 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) + }, + ) + } + + profileEntries(navigator, settingsViewModel) + + shopEntries(navigator, appViewModel, settingsViewModel) + + entry { + BuyIntroScreen( + onBackClick = { navigator.goBack() }, + ) + } + + entry { + CriticalUpdateScreen() + } + + 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.Activity.All) }, + onActivityItemClick = { navigator.navigate(Routes.Activity.Detail(it)) }, + onEmptyActivityRowClick = { navigator.navigate(Routes.Receive.Qr) }, + onTransferToSpendingClick = { + if (!hasSeenSpendingIntro) { + navigator.navigate(Routes.Transfer.ToSpending.Intro) + } else { + navigator.navigate(Routes.Transfer.ToSpending.Amount) + } + }, + 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.Activity.All) }, + onActivityItemClick = { navigator.navigate(Routes.Activity.Detail(it)) }, + onEmptyActivityRowClick = { navigator.navigate(Routes.Receive.Qr) }, + onTransferToSavingsClick = { + if (!hasSeenSavingsIntro) { + navigator.navigate(Routes.Transfer.ToSavings.Intro) + } else { + navigator.navigate(Routes.Transfer.ToSavings.Availability) + } + }, + 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, + ) +} + +private fun EntryProviderScope.profileEntries( + navigator: Navigator, + settingsViewModel: SettingsViewModel, +) { + entry { + ProfileIntroScreen( + onContinue = { + settingsViewModel.setHasSeenProfileIntro(true) + navigator.navigate(Routes.Profile.Create) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + CreateProfileScreen( + onBack = { navigator.goBack() }, + ) + } +} + +private fun EntryProviderScope.shopEntries( + navigator: Navigator, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, +) { + entry { + ShopIntroScreen( + onContinue = { + settingsViewModel.setHasSeenShopIntro(true) + navigator.navigate(Routes.Shop.Discover) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + ShopDiscoverScreen( + onBack = { navigator.goBack() }, + navigateWebView = { page, title -> + navigator.navigate(Routes.Shop.WebView(page, title)) + }, + ) + } + + entry { route -> + ShopWebViewScreen( + page = route.page, + title = route.title, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + onPaymentIntent = { data -> + appViewModel.onScanResult(data) + }, + ) + } +} + +private fun EntryProviderScope.recoveryEntries( + navigator: Navigator, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, +) { + entry { + RecoveryModeScreen( + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, + onNavigateToSeed = { navigator.navigate(Routes.Recovery.Mnemonic) }, + ) + } + + 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..d5c6e170c --- /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 INDEX_LAST_SLIDE = 4 + +fun EntryProviderScope.onboardingEntries( + navigator: Navigator, + isGeoBlocked: Boolean, + onCreateWallet: (passphrase: String?) -> Unit, + onRestoreWallet: (mnemonic: String, passphrase: String?) -> Unit, +) { + entry { + TermsOfUseScreen( + onNavigateToIntro = { navigator.navigate(Routes.Onboarding.Intro) } + ) + } + + entry { + IntroScreen( + onStartClick = { navigator.navigate(Routes.Onboarding.Slides()) }, + onSkipClick = { navigator.navigate(Routes.Onboarding.Slides(INDEX_LAST_SLIDE)) }, + ) + } + + entry { route -> + OnboardingSlidesScreen( + currentTab = route.tab, + isGeoBlocked = isGeoBlocked, + onAdvancedSetupClick = { navigator.navigate(Routes.Onboarding.Advanced) }, + onCreateClick = { onCreateWallet(null) }, + onRestoreClick = { navigator.navigate(Routes.Onboarding.WarningMultipleDevices) }, + ) + } + + entry { + WarningMultipleDevicesScreen( + onBackClick = { navigator.goBack() }, + onConfirmClick = { navigator.navigate(Routes.Onboarding.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..e53675700 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/SettingsEntries.kt @@ -0,0 +1,286 @@ +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 + +@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.Settings.Dev.OrderDetail(orderId)) }, + onCjitItemClick = { entryId -> navigator.navigate(Routes.Settings.Dev.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.navigate(Routes.QuickPay.Settings) }, + ) + } + + entry { + QuickPaySettingsScreen(onBack = { navigator.goBack() }) + } + + entry { + LanguageSettingsScreen(onBackClick = { navigator.goBack() }) + } + + entry { + BackgroundPaymentsIntroScreen( + onBack = { navigator.goBack() }, + onContinue = { navigator.navigate(Routes.BackgroundPayments.Settings) }, + ) + } + + 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.ReportIssue.Success) + } else { + navigator.navigate(Routes.ReportIssue.Failure) + } + } + ) + } + + 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..4a98f092d --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt @@ -0,0 +1,862 @@ +package to.bitkit.ui.nav.entries + +import android.content.Intent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.core.net.toUri +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.R +import to.bitkit.env.Env +import to.bitkit.models.NewTransactionSheetDetails +import to.bitkit.models.NewTransactionSheetDirection +import to.bitkit.models.NewTransactionSheetType +import to.bitkit.ui.LocalBalances +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.SheetSceneStrategy +import to.bitkit.ui.screens.scanner.QrScanningScreen +import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorContent +import to.bitkit.ui.screens.wallets.activity.TagSelectorContent +import to.bitkit.ui.screens.wallets.receive.EditInvoiceScreen +import to.bitkit.ui.screens.wallets.receive.LocationBlockScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveAmountScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveConfirmScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveLiquidityScreen +import to.bitkit.ui.screens.wallets.receive.ReceiveQrScreen +import to.bitkit.ui.screens.wallets.send.AddTagScreen +import to.bitkit.ui.screens.wallets.send.SendAddressScreen +import to.bitkit.ui.screens.wallets.send.SendAmountScreen +import to.bitkit.ui.screens.wallets.send.SendCoinSelectionScreen +import to.bitkit.ui.screens.wallets.send.SendConfirmScreen +import to.bitkit.ui.screens.wallets.send.SendErrorScreen +import to.bitkit.ui.screens.wallets.send.SendFeeCustomScreen +import to.bitkit.ui.screens.wallets.send.SendFeeRateScreen +import to.bitkit.ui.screens.wallets.send.SendFeeViewModel +import to.bitkit.ui.screens.wallets.send.SendPinCheckScreen +import to.bitkit.ui.screens.wallets.send.SendQuickPayScreen +import to.bitkit.ui.screens.wallets.send.SendRecipientScreen +import to.bitkit.ui.screens.wallets.withdraw.WithdrawConfirmScreen +import to.bitkit.ui.screens.wallets.withdraw.WithdrawErrorScreen +import to.bitkit.ui.settings.backups.BackupIntroScreen +import to.bitkit.ui.settings.backups.BackupNavSheetViewModel +import to.bitkit.ui.settings.backups.ConfirmMnemonicScreen +import to.bitkit.ui.settings.backups.ConfirmPassphraseScreen +import to.bitkit.ui.settings.backups.MetadataScreen +import to.bitkit.ui.settings.backups.MultipleDevicesScreen +import to.bitkit.ui.settings.backups.ShowMnemonicScreen +import to.bitkit.ui.settings.backups.ShowPassphraseScreen +import to.bitkit.ui.settings.backups.SuccessScreen +import to.bitkit.ui.settings.backups.WarningScreen +import to.bitkit.ui.settings.pin.PinBiometricsScreen +import to.bitkit.ui.settings.pin.PinChooseScreen +import to.bitkit.ui.settings.pin.PinConfirmScreen +import to.bitkit.ui.settings.pin.PinPromptScreen +import to.bitkit.ui.settings.pin.PinResultScreen +import to.bitkit.ui.settings.support.SupportScreen +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.sheets.BackgroundPaymentsIntroSheet +import to.bitkit.ui.sheets.ForceTransferContent +import to.bitkit.ui.sheets.GiftErrorSheet +import to.bitkit.ui.sheets.GiftLoading +import to.bitkit.ui.sheets.GiftViewModel +import to.bitkit.ui.sheets.HighBalanceWarningSheet +import to.bitkit.ui.sheets.LnurlAuthContent +import to.bitkit.ui.sheets.NewTransactionSheetView +import to.bitkit.ui.sheets.QuickPayIntroSheet +import to.bitkit.ui.sheets.UpdateSheet +import to.bitkit.ui.utils.NotificationUtils +import to.bitkit.viewmodels.ActivityListViewModel +import to.bitkit.viewmodels.AmountInputViewModel +import to.bitkit.viewmodels.AppViewModel +import to.bitkit.viewmodels.SendEvent +import to.bitkit.viewmodels.SettingsViewModel +import to.bitkit.viewmodels.TransferViewModel +import to.bitkit.viewmodels.WalletViewModel + +@Suppress("LongMethod", "LongParameterList") +fun EntryProviderScope.sheetEntries( + navigator: Navigator, + appViewModel: AppViewModel, + walletViewModel: WalletViewModel, + activityListViewModel: ActivityListViewModel, + transferViewModel: TransferViewModel, +) { + simpleSheetEntries(navigator, appViewModel, activityListViewModel, transferViewModel) + pinFlowEntries(navigator) + backupFlowEntries(navigator) + sendFlowEntries(navigator, appViewModel, walletViewModel) + receiveFlowEntries(navigator, walletViewModel) + giftFlowEntries(navigator, appViewModel) + sheetFlowEntries(navigator, appViewModel) +} + +@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() }, + ) + } +} + +private fun EntryProviderScope.pinFlowEntries(navigator: Navigator) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + PinPromptScreen( + showLaterButton = route.showLaterButton, + onContinue = { navigator.navigate(Routes.Pin.Choose) }, + onLater = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + PinChooseScreen( + onPinChosen = { pin -> navigator.navigate(Routes.Pin.Confirm(pin)) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + PinConfirmScreen( + originalPin = route.pin, + onPinConfirmed = { navigator.navigate(Routes.Pin.Biometrics) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + PinBiometricsScreen( + onContinue = { isBioOn -> navigator.navigate(Routes.Pin.Result(isBioOn)) }, + onSkip = { navigator.navigate(Routes.Pin.Result(isBioOn = false)) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + PinResultScreen( + isBioOn = route.isBioOn, + onDismiss = { navigator.navigateToHome() }, + onBack = { navigator.navigateToHome() }, + ) + } +} + +@Suppress("LongMethod") +private fun EntryProviderScope.backupFlowEntries(navigator: Navigator) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + BackupIntroScreen( + hasFunds = LocalBalances.current.totalSats > 0u, + onClose = { navigator.goBack() }, + onConfirm = { navigator.navigate(Routes.Backup.ShowMnemonic) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + ShowMnemonicScreen( + uiState = uiState, + onRevealClick = viewModel::onRevealMnemonic, + onContinueClick = { navigator.navigate(Routes.Backup.ShowPassphrase) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + ShowPassphraseScreen( + uiState = uiState, + onContinue = { navigator.navigate(Routes.Backup.ConfirmMnemonic) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + ConfirmMnemonicScreen( + uiState = uiState, + onContinue = { navigator.navigate(Routes.Backup.ConfirmPassphrase) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + ConfirmPassphraseScreen( + uiState = uiState, + onPassphraseChange = viewModel::onPassphraseInput, + onContinue = { navigator.navigate(Routes.Backup.Warning) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + WarningScreen( + onContinue = { navigator.navigate(Routes.Backup.Success) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + SuccessScreen( + onContinue = { navigator.navigate(Routes.Backup.MultipleDevices) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + MultipleDevicesScreen( + onContinue = { navigator.navigate(Routes.Backup.Metadata) }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + MetadataScreen( + uiState = uiState, + onDismiss = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + ) + } +} + +@Suppress("LongMethod") +private fun EntryProviderScope.sendFlowEntries( + navigator: Navigator, + appViewModel: AppViewModel, + walletViewModel: WalletViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + LaunchedEffect(Unit) { + appViewModel.resetSendState() + appViewModel.resetQuickPayData() + } + SendRecipientScreen( + onEvent = { appViewModel.setSendEvent(it) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendAddressScreen( + uiState = uiState, + onBack = { navigator.goBack() }, + onEvent = { appViewModel.setSendEvent(it) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(route.prefill) { + route.prefill?.let { prefill -> + appViewModel.setSendEvent(SendEvent.AddressContinue(prefill)) + } + } + + SendAmountScreen( + uiState = uiState, + walletUiState = walletUiState, + canGoBack = true, + onBack = { navigator.goBack() }, + onEvent = { appViewModel.setSendEvent(it) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + QrScanningScreen( + navigator = navigator, + onScanSuccess = { qrCode -> + navigator.goBack() + appViewModel.onScanResult(data = qrCode) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + SendCoinSelectionScreen( + requiredAmount = sendUiState.amount, + address = sendUiState.address, + onBack = { navigator.goBack() }, + onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val viewModel = hiltViewModel() + SendFeeRateScreen( + sendUiState = sendUiState, + viewModel = viewModel, + onBack = { navigator.goBack() }, + onContinue = { navigator.goBack() }, + onSelect = { speed -> appViewModel.onSelectSpeed(speed) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val viewModel = hiltViewModel() + SendFeeCustomScreen( + viewModel = viewModel, + onBack = { navigator.goBack() }, + onContinue = { speed -> appViewModel.setTransactionSpeed(speed) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() + + SendConfirmScreen( + savedStateHandle = remember { androidx.lifecycle.SavedStateHandle() }, + uiState = uiState, + isNodeRunning = walletUiState.nodeLifecycleState.isRunning(), + canGoBack = true, + onBack = { navigator.goBack() }, + onEvent = { e -> appViewModel.setSendEvent(e) }, + onClickAddTag = { navigator.navigate(Routes.Send.AddTag) }, + onClickTag = { tag -> appViewModel.removeTag(tag) }, + onNavigateToPin = { navigator.navigate(Routes.Send.PinCheck) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val sendDetail by appViewModel.successSendUiState.collectAsStateWithLifecycle() + NewTransactionSheetView( + details = sendDetail, + onCloseClick = { navigator.navigateToHome() }, + onDetailClick = { appViewModel.onClickSendDetail() }, + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .navigationBarsPadding() + .testTag("SendSuccess"), + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + SendErrorScreen( + errorMessage = route.message, + onRetry = { navigator.navigate(Routes.Send.Recipient) }, + onClose = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + WithdrawConfirmScreen( + uiState = uiState, + onBack = { navigator.goBack() }, + onConfirm = { appViewModel.onConfirmWithdraw() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() + WithdrawErrorScreen( + uiState = uiState, + onBack = { navigator.goBack() }, + onClickScan = { navigator.navigate(Routes.Send.QrScanner) }, + onClickSupport = { navigator.navigate(Routes.Send.Support) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + SupportScreen(navigator) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + AddTagScreen( + onBack = { navigator.goBack() }, + onTagSelected = { tag -> + appViewModel.addTagToSelected(tag) + navigator.goBack() + }, + tqgInputTestTag = "TagInputSend", + addButtonTestTag = "SendTagsSubmit", + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + SendPinCheckScreen( + onBack = { navigator.goBack() }, + onSuccess = { + navigator.goBack() + appViewModel.setSendEvent(SendEvent.PayConfirmed) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val quickPayData by appViewModel.quickPayData.collectAsStateWithLifecycle() + quickPayData?.let { data -> + SendQuickPayScreen( + quickPayData = data, + onPaymentComplete = { paymentHash, amountWithFee -> + appViewModel.handlePaymentSuccess( + NewTransactionSheetDetails( + type = NewTransactionSheetType.LIGHTNING, + direction = NewTransactionSheetDirection.SENT, + paymentHashOrTxId = paymentHash, + sats = amountWithFee, + ), + ) + }, + onShowError = { errorMessage -> + navigator.navigate(Routes.Send.Error(errorMessage)) + }, + ) + } + } +} + +@Suppress("LongMethod") +private fun EntryProviderScope.receiveFlowEntries( + navigator: Navigator, + walletViewModel: WalletViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() + val cjitInvoice by walletViewModel.pendingCjitInvoice.collectAsStateWithLifecycle() + val lightningState by walletViewModel.lightningState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + walletViewModel.resetPreActivityMetadataTagsForCurrentInvoice() + walletViewModel.refreshReceiveState() + } + + ReceiveQrScreen( + cjitInvoice = cjitInvoice, + walletState = walletUiState, + onClickEditInvoice = { navigator.navigate(Routes.Receive.EditInvoice) }, + onClickReceiveCjit = { + if (lightningState.isGeoBlocked) { + navigator.navigate(Routes.Receive.GeoBlock) + } else { + navigator.navigate(Routes.Receive.Amount) + } + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + ReceiveAmountScreen( + onCjitCreated = { entry -> + walletViewModel.setPendingCjitEntry(entry) + navigator.navigate(Routes.Receive.Confirm) + }, + onBack = { navigator.goBack() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + LocationBlockScreen( + onBackPressed = { navigator.goBack() }, + navigateAdvancedSetup = { navigator.navigate(Routes.External.Connection()) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + entry?.let { entryDetails -> + ReceiveConfirmScreen( + entry = entryDetails, + onLearnMore = { navigator.navigate(Routes.Receive.Liquidity) }, + onContinue = { invoice -> + walletViewModel.setPendingCjitInvoice(invoice) + navigator.popBackTo(Routes.Receive.Qr) + }, + onBack = { navigator.goBack() }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + entry?.let { entryDetails -> + ReceiveConfirmScreen( + entry = entryDetails, + isAdditional = true, + onLearnMore = { navigator.navigate(Routes.Receive.LiquidityAdditional) }, + onContinue = { invoice -> + walletViewModel.setPendingCjitInvoice(invoice) + navigator.popBackTo(Routes.Receive.Qr) + }, + onBack = { navigator.goBack() }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + val settingsViewModel = hiltViewModel() + val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + val context = LocalContext.current + + entry?.let { entryDetails -> + ReceiveLiquidityScreen( + entry = entryDetails, + onContinue = { navigator.goBack() }, + onBack = { navigator.goBack() }, + hasNotificationPermission = notificationsGranted, + onSwitchClick = { NotificationUtils.openNotificationSettings(context) }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val entry by walletViewModel.pendingCjitEntry.collectAsStateWithLifecycle() + val settingsViewModel = hiltViewModel() + val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + val context = LocalContext.current + + entry?.let { entryDetails -> + ReceiveLiquidityScreen( + entry = entryDetails, + isAdditional = true, + onContinue = { navigator.goBack() }, + onBack = { navigator.goBack() }, + hasNotificationPermission = notificationsGranted, + onSwitchClick = { NotificationUtils.openNotificationSettings(context) }, + ) + } + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val walletState by walletViewModel.walletState.collectAsStateWithLifecycle() + val editInvoiceAmountViewModel = hiltViewModel() + + LaunchedEffect(Unit) { editInvoiceAmountViewModel.clearInput() } + + EditInvoiceScreen( + amountInputViewModel = editInvoiceAmountViewModel, + walletUiState = walletState, + onBack = { navigator.goBack() }, + updateInvoice = walletViewModel::updateBip21Invoice, + onClickAddTag = { navigator.navigate(Routes.Receive.AddTag) }, + onClickTag = walletViewModel::removeTag, + onDescriptionUpdate = walletViewModel::updateBip21Description, + navigateReceiveConfirm = { entry -> + walletViewModel.setPendingCjitEntry(entry) + navigator.navigate(Routes.Receive.ConfirmInbound) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + AddTagScreen( + onBack = { navigator.goBack() }, + onTagSelected = { tag -> + walletViewModel.addTagToSelected(tag) + navigator.goBack() + }, + tqgInputTestTag = "TagInputReceive", + addButtonTestTag = "ReceiveTagsSubmit", + ) + } +} + +@Suppress("LongMethod") +private fun EntryProviderScope.giftFlowEntries( + navigator: Navigator, + appViewModel: AppViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { route -> + val viewModel = hiltViewModel() + + LaunchedEffect(route.code, route.amount) { + viewModel.initialize(route.code, route.amount) + } + + LaunchedEffect(viewModel) { + viewModel.successEvent.collect { details -> + navigator.navigateToHome() + appViewModel.showTransactionSheet(details) + } + } + + LaunchedEffect(viewModel) { + viewModel.navigationEvent.collect { route -> + when (route) { + is Routes.Gift.Used -> navigator.navigate(Routes.Gift.Used) + is Routes.Gift.UsedUp -> navigator.navigate(Routes.Gift.UsedUp) + is Routes.Gift.Error -> navigator.navigate(Routes.Gift.Error) + is Routes.Gift.Success -> navigator.navigateToHome() + else -> { /* Ignore other routes */ } + } + } + } + + GiftLoading(viewModel = viewModel) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + GiftErrorSheet( + titleRes = R.string.other__gift__used__title, + textRes = R.string.other__gift__used__text, + testTag = "GiftUsed", + onDismiss = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + GiftErrorSheet( + titleRes = R.string.other__gift__used_up__title, + textRes = R.string.other__gift__used_up__text, + testTag = "GiftUsedUp", + onDismiss = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + GiftErrorSheet( + titleRes = R.string.other__gift__error__title, + textRes = R.string.other__gift__error__text, + testTag = "GiftError", + onDismiss = { navigator.navigateToHome() }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + // This route is not navigated directly, success triggers navigation to home and shows transaction sheet. + LaunchedEffect(Unit) { + navigator.navigateToHome() + } + } +} + +@Suppress("LongMethod") +private fun EntryProviderScope.sheetFlowEntries( + navigator: Navigator, + appViewModel: AppViewModel, +) { + entry( + metadata = SheetSceneStrategy.sheet() + ) { + UpdateSheet( + onCancel = { + appViewModel.dismissTimedSheet() + navigator.goBack() + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + BackupIntroScreen( + hasFunds = LocalBalances.current.totalSats > 0u, + onClose = { + appViewModel.dismissTimedSheet() + navigator.goBack() + }, + onConfirm = { navigator.navigate(Routes.Backup.ShowMnemonic) }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + BackgroundPaymentsIntroSheet( + onContinue = { + appViewModel.dismissTimedSheet(skipQueue = true) + navigator.navigate(Routes.BackgroundPayments.Settings) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + QuickPayIntroSheet( + onContinue = { + appViewModel.dismissTimedSheet(skipQueue = true) + navigator.navigate(Routes.QuickPay.Settings) + }, + ) + } + + entry( + metadata = SheetSceneStrategy.sheet() + ) { + val context = LocalContext.current + HighBalanceWarningSheet( + understoodClick = { + appViewModel.dismissTimedSheet() + navigator.goBack() + }, + learnMoreClick = { + val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri()) + context.startActivity(intent) + appViewModel.dismissTimedSheet(skipQueue = true) + navigator.goBack() + }, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/TransferEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/TransferEntries.kt new file mode 100644 index 000000000..126747a78 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/TransferEntries.kt @@ -0,0 +1,316 @@ +package to.bitkit.ui.nav.entries + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.models.Toast +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.screens.scanner.QrScanningScreen +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.viewmodels.AppViewModel +import to.bitkit.viewmodels.SettingsViewModel +import to.bitkit.viewmodels.TransferViewModel +import to.bitkit.viewmodels.WalletViewModel + +@Suppress("LongMethod", "LongParameterList") +fun EntryProviderScope.transferEntries( + navigator: Navigator, + appViewModel: AppViewModel, + walletViewModel: WalletViewModel, + transferViewModel: TransferViewModel, + settingsViewModel: SettingsViewModel, +) { + entry { + TransferIntroScreen( + onContinueClick = { + navigator.navigate(Routes.Transfer.Funding) + settingsViewModel.setHasSeenTransferIntro(true) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SavingsIntroScreen( + onContinueClick = { + navigator.navigate(Routes.Transfer.ToSavings.Availability) + settingsViewModel.setHasSeenSavingsIntro(true) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SavingsAvailabilityScreen( + onBackClick = { navigator.goBack() }, + onCancelClick = { navigator.navigateToHome() }, + onContinueClick = { navigator.navigate(Routes.Transfer.ToSavings.Confirm) }, + ) + } + + entry { + SavingsConfirmScreen( + onConfirm = { navigator.navigate(Routes.Transfer.ToSavings.Progress) }, + onAdvancedClick = { navigator.navigate(Routes.Transfer.ToSavings.Advanced) }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SavingsAdvancedScreen( + onContinueClick = { navigator.goBack() }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SavingsProgressScreen( + wallet = walletViewModel, + transfer = transferViewModel, + onContinueClick = { navigator.navigateToHome() }, + onForceTransfer = { navigator.navigate(Routes.Sheet.ForceTransfer) }, + ) + } + + entry { + SpendingIntroScreen( + onContinueClick = { + navigator.navigate(Routes.Transfer.ToSpending.Amount) + settingsViewModel.setHasSeenSpendingIntro(true) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + SpendingAmountScreen( + viewModel = transferViewModel, + onBackClick = { navigator.goBack() }, + onOrderCreated = { navigator.navigate(Routes.Transfer.ToSpending.Confirm) }, + toastException = { appViewModel.toast(it) }, + toast = { title, description -> + appViewModel.toast( + type = Toast.ToastType.ERROR, + title = title, + description = description, + ) + }, + ) + } + + entry { + SpendingConfirmScreen( + viewModel = transferViewModel, + onBackClick = { navigator.goBack() }, + onCloseClick = { navigator.navigateToHome() }, + onLearnMoreClick = { navigator.navigate(Routes.Transfer.Liquidity) }, + onAdvancedClick = { navigator.navigate(Routes.Transfer.ToSpending.Advanced) }, + onConfirm = { navigator.navigate(Routes.Transfer.SettingUp) }, + ) + } + + entry { + SpendingAdvancedScreen( + viewModel = transferViewModel, + onBackClick = { navigator.goBack() }, + onOrderCreated = { navigator.goBack() }, + ) + } + + entry { + LiquidityScreen( + onBackClick = { navigator.goBack() }, + onContinueClick = { navigator.goBack() }, + ) + } + + entry { + SettingUpScreen( + viewModel = transferViewModel, + onContinueClick = { navigator.navigateToHome() }, + ) + } + + entry { + FundingEntry( + navigator = navigator, + appViewModel = appViewModel, + settingsViewModel = settingsViewModel, + ) + } + + entry { + FundingAdvancedScreen( + onLnurl = { navigator.navigate(Routes.QrScanner) }, + onManual = { navigator.navigate(Routes.External.Connection()) }, + onBackClick = { navigator.goBack() }, + ) + } + + externalNodeEntries( + navigator = navigator, + walletViewModel = walletViewModel, + ) +} + +@Composable +private fun FundingEntry( + navigator: Navigator, + appViewModel: AppViewModel, + settingsViewModel: SettingsViewModel, +) { + val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() + val isGeoBlocked by appViewModel.isGeoBlocked.collectAsStateWithLifecycle() + + FundingScreen( + onTransfer = { + if (!hasSeenSpendingIntro) { + navigator.navigate(Routes.Transfer.ToSpending.Intro) + } else { + navigator.navigate(Routes.Transfer.ToSpending.Amount) + } + }, + onFund = { + navigator.navigate(Routes.Receive.Qr) + }, + onAdvanced = { navigator.navigate(Routes.Transfer.FundingAdvanced) }, + onBackClick = { navigator.goBack() }, + isGeoBlocked = isGeoBlocked, + ) +} + +/** + * External node connection flow entries. + * Note: Uses a shared ViewModel across screens for the external node connection flow. + */ +private fun EntryProviderScope.externalNodeEntries( + navigator: Navigator, + walletViewModel: WalletViewModel, +) { + entry { route -> + ExternalConnectionEntry( + navigator = navigator, + scannedNodeUri = route.scannedNodeUri, + ) + } + + entry { + ExternalAmountEntry(navigator = navigator) + } + + entry { + ExternalConfirmEntry( + navigator = navigator, + walletViewModel = walletViewModel, + ) + } + + entry { + ExternalFeeCustomEntry(navigator = navigator) + } + + entry { + ExternalSuccessScreen( + onContinue = { navigator.navigateToHome() }, + ) + } + + entry { route -> + LnurlChannelScreen( + uri = route.uri, + callback = route.callback, + k1 = route.k1, + onConnected = { navigator.navigate(Routes.External.Success) }, + onBack = { navigator.goBack() }, + onClose = { navigator.navigateToHome() }, + ) + } + + entry { + QrScanningScreen( + navigator = navigator, + onScanSuccess = { qrCode -> + navigator.navigate(Routes.External.Connection(scannedNodeUri = qrCode)) + }, + ) + } +} + +@Composable +private fun ExternalConnectionEntry( + navigator: Navigator, + scannedNodeUri: String?, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalConnectionScreen( + scannedNodeUri = scannedNodeUri, + viewModel = viewModel, + onNodeConnected = { navigator.navigate(Routes.External.Amount) }, + onScanClick = { navigator.navigate(Routes.External.NodeScanner) }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ExternalAmountEntry( + navigator: Navigator, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalAmountScreen( + viewModel = viewModel, + onContinue = { navigator.navigate(Routes.External.Confirm) }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ExternalConfirmEntry( + navigator: Navigator, + walletViewModel: WalletViewModel, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalConfirmScreen( + viewModel = viewModel, + onConfirm = { + walletViewModel.refreshState() + navigator.navigate(Routes.External.Success) + }, + onNetworkFeeClick = { navigator.navigate(Routes.External.FeeCustom) }, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ExternalFeeCustomEntry( + navigator: Navigator, + viewModel: ExternalNodeViewModel = hiltViewModel(), +) { + ExternalFeeCustomScreen( + viewModel = viewModel, + onBack = { navigator.goBack() }, + ) +} diff --git a/app/src/main/java/to/bitkit/ui/nav/entries/WidgetEntries.kt b/app/src/main/java/to/bitkit/ui/nav/entries/WidgetEntries.kt new file mode 100644 index 000000000..ccac81737 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/nav/entries/WidgetEntries.kt @@ -0,0 +1,291 @@ +package to.bitkit.ui.nav.entries + +import androidx.compose.runtime.Composable +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.runtime.NavKey +import to.bitkit.models.WidgetType +import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.screens.widgets.AddWidgetsScreen +import to.bitkit.ui.screens.widgets.WidgetsIntroScreen +import to.bitkit.ui.screens.widgets.blocks.BlocksEditScreen +import to.bitkit.ui.screens.widgets.blocks.BlocksPreviewScreen +import to.bitkit.ui.screens.widgets.blocks.BlocksViewModel +import to.bitkit.ui.screens.widgets.calculator.CalculatorPreviewScreen +import to.bitkit.ui.screens.widgets.calculator.CalculatorViewModel +import to.bitkit.ui.screens.widgets.facts.FactsEditScreen +import to.bitkit.ui.screens.widgets.facts.FactsPreviewScreen +import to.bitkit.ui.screens.widgets.facts.FactsViewModel +import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditScreen +import to.bitkit.ui.screens.widgets.headlines.HeadlinesPreviewScreen +import to.bitkit.ui.screens.widgets.headlines.HeadlinesViewModel +import to.bitkit.ui.screens.widgets.price.PriceEditScreen +import to.bitkit.ui.screens.widgets.price.PricePreviewScreen +import to.bitkit.ui.screens.widgets.price.PriceViewModel +import to.bitkit.ui.screens.widgets.weather.WeatherEditScreen +import to.bitkit.ui.screens.widgets.weather.WeatherPreviewScreen +import to.bitkit.ui.screens.widgets.weather.WeatherViewModel +import to.bitkit.viewmodels.CurrencyViewModel +import to.bitkit.viewmodels.SettingsViewModel + +@Suppress("LongMethod") +fun EntryProviderScope.widgetEntries( + navigator: Navigator, + currencyViewModel: CurrencyViewModel, + settingsViewModel: SettingsViewModel, +) { + entry { + WidgetsIntroScreen( + onContinue = { + settingsViewModel.setHasSeenWidgetsIntro(true) + navigator.navigate(Routes.Widgets.Add) + }, + onBackClick = { navigator.goBack() }, + ) + } + + entry { + AddWidgetsScreen( + fiatSymbol = LocalCurrencies.current.currencySymbol, + onWidgetSelected = { widgetType -> + when (widgetType) { + WidgetType.NEWS -> navigator.navigate(Routes.Widgets.Headlines.Main) + WidgetType.FACTS -> navigator.navigate(Routes.Widgets.Facts.Main) + WidgetType.BLOCK -> navigator.navigate(Routes.Widgets.Blocks.Main) + WidgetType.WEATHER -> navigator.navigate(Routes.Widgets.Weather.Main) + WidgetType.PRICE -> navigator.navigate(Routes.Widgets.Price.Main) + WidgetType.CALCULATOR -> navigator.navigate(Routes.Widgets.Calculator.Preview) + } + }, + onBackCLick = { navigator.goBack() }, + ) + } + + headlinesEntries(navigator) + + factsEntries(navigator) + + blocksEntries(navigator) + + weatherEntries(navigator) + + priceEntries(navigator) + + entry { + CalculatorEntry( + navigator = navigator, + currencyViewModel = currencyViewModel, + ) + } +} + +private fun EntryProviderScope.headlinesEntries(navigator: Navigator) { + entry { + HeadlinesPreviewEntry(navigator) + } + + entry { + HeadlinesPreviewEntry(navigator) + } + + entry { + HeadlinesEditEntry(navigator) + } +} + +@Composable +private fun HeadlinesPreviewEntry( + navigator: Navigator, + viewModel: HeadlinesViewModel = hiltViewModel(), +) { + HeadlinesPreviewScreen( + headlinesViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.Widgets.Headlines.Edit) }, + ) +} + +@Composable +private fun HeadlinesEditEntry( + navigator: Navigator, + viewModel: HeadlinesViewModel = hiltViewModel(), +) { + HeadlinesEditScreen( + headlinesViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.Widgets.Headlines.Preview) }, + ) +} + +private fun EntryProviderScope.factsEntries(navigator: Navigator) { + entry { + FactsPreviewEntry(navigator) + } + + entry { + FactsPreviewEntry(navigator) + } + + entry { + FactsEditEntry(navigator) + } +} + +@Composable +private fun FactsPreviewEntry( + navigator: Navigator, + viewModel: FactsViewModel = hiltViewModel(), +) { + FactsPreviewScreen( + factsViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.Widgets.Facts.Edit) }, + ) +} + +@Composable +private fun FactsEditEntry( + navigator: Navigator, + viewModel: FactsViewModel = hiltViewModel(), +) { + FactsEditScreen( + factsViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.Widgets.Facts.Preview) }, + ) +} + +private fun EntryProviderScope.blocksEntries(navigator: Navigator) { + entry { + BlocksPreviewEntry(navigator) + } + + entry { + BlocksPreviewEntry(navigator) + } + + entry { + BlocksEditEntry(navigator) + } +} + +@Composable +private fun BlocksPreviewEntry( + navigator: Navigator, + viewModel: BlocksViewModel = hiltViewModel(), +) { + BlocksPreviewScreen( + blocksViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.Widgets.Blocks.Edit) }, + ) +} + +@Composable +private fun BlocksEditEntry( + navigator: Navigator, + viewModel: BlocksViewModel = hiltViewModel(), +) { + BlocksEditScreen( + blocksViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.Widgets.Blocks.Preview) }, + ) +} + +private fun EntryProviderScope.weatherEntries(navigator: Navigator) { + entry { + WeatherPreviewEntry(navigator) + } + + entry { + WeatherPreviewEntry(navigator) + } + + entry { + WeatherEditEntry(navigator) + } +} + +@Composable +private fun WeatherPreviewEntry( + navigator: Navigator, + viewModel: WeatherViewModel = hiltViewModel(), +) { + WeatherPreviewScreen( + weatherViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.Widgets.Weather.Edit) }, + ) +} + +@Composable +private fun WeatherEditEntry( + navigator: Navigator, + viewModel: WeatherViewModel = hiltViewModel(), +) { + WeatherEditScreen( + weatherViewModel = viewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.Widgets.Weather.Preview) }, + ) +} + +private fun EntryProviderScope.priceEntries(navigator: Navigator) { + entry { + PricePreviewEntry(navigator) + } + + entry { + PricePreviewEntry(navigator) + } + + entry { + PriceEditEntry(navigator) + } +} + +@Composable +private fun PricePreviewEntry( + navigator: Navigator, + viewModel: PriceViewModel = hiltViewModel(), +) { + PricePreviewScreen( + priceViewModel = viewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + navigateEditWidget = { navigator.navigate(Routes.Widgets.Price.Edit) }, + ) +} + +@Composable +private fun PriceEditEntry( + navigator: Navigator, + priceViewModel: PriceViewModel = hiltViewModel(), +) { + PriceEditScreen( + viewModel = priceViewModel, + onBack = { navigator.goBack() }, + navigatePreview = { navigator.navigate(Routes.Widgets.Price.Preview) }, + ) +} + +@Composable +private fun CalculatorEntry( + navigator: Navigator, + currencyViewModel: CurrencyViewModel, + viewModel: CalculatorViewModel = hiltViewModel(), +) { + CalculatorPreviewScreen( + viewModel = viewModel, + currencyViewModel = currencyViewModel, + onClose = { navigator.navigateToHome() }, + onBack = { navigator.goBack() }, + ) +} diff --git a/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt b/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt index 30b60c475..920a82d54 100644 --- a/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/onboarding/TermsOfUseScreen.kt @@ -135,7 +135,6 @@ private fun TermsText( } } - @Preview(showSystemUi = true) @Composable private fun TermsPreview() { diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt index e6c122822..7e6952b64 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt @@ -29,8 +29,8 @@ import to.bitkit.ui.LocalDrawerState import to.bitkit.ui.components.Title import to.bitkit.ui.theme.AppThemeSurface -@Composable @OptIn(ExperimentalMaterial3Api::class) +@Composable fun AppTopBar( titleText: String?, onBackClick: (() -> Unit)?, @@ -60,12 +60,15 @@ fun AppTopBar( .size(32.dp) ) } - Title(text = titleText, maxLines = 1) + Title( + text = titleText, + maxLines = 1, + ) } } }, actions = actions, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent, ), diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index ee78f6339..8340a001e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.shouldShowRationale @@ -71,6 +70,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.SheetTopBar @@ -80,16 +80,25 @@ import to.bitkit.utils.Logger import to.bitkit.viewmodels.AppViewModel import java.util.concurrent.Executors -const val SCAN_REQUEST_KEY = "SCAN_REQUEST" -const val SCAN_RESULT_KEY = "SCAN_RESULT" - @OptIn(ExperimentalPermissionsApi::class) @Composable fun QrScanningScreen( - navController: NavController, - inSheet: Boolean = false, - onBack: () -> Unit = { navController.popBackStack() }, + navigator: Navigator, + onScanSuccess: (String) -> Unit, +) { + QrScanningScreenContent( + onBack = { navigator.goBack() }, + onScanSuccess = onScanSuccess, + inSheet = false, + ) +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun QrScanningScreenContent( + onBack: () -> Unit, onScanSuccess: (String) -> Unit, + inSheet: Boolean, ) { val app = appViewModel ?: return @@ -99,18 +108,8 @@ fun QrScanningScreen( LaunchedEffect(scanResult) { scanResult?.let { qrCode -> delay(100) // wait to prevent navigation result race conditions - - val prev = navController.previousBackStackEntry - val wasCalledForResult = prev?.savedStateHandle?.contains(SCAN_REQUEST_KEY) == true - if (wasCalledForResult) { - prev.savedStateHandle[SCAN_RESULT_KEY] = qrCode - onBack() - prev.savedStateHandle.remove(SCAN_REQUEST_KEY) - } else { - onBack() - onScanSuccess(qrCode) - } - + onBack() + onScanSuccess(qrCode) // Reset scan result to allow new scans setScanResult(null) } diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index 8e0fd9659..5f0f2fbb9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -10,17 +10,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsTextButtonRow +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -30,7 +30,7 @@ import to.bitkit.viewmodels.DevSettingsViewModel @Composable fun DevSettingsScreen( - navController: NavController, + navigator: Navigator, viewModel: DevSettingsViewModel = hiltViewModel(), ) { val app = appViewModel ?: return @@ -41,7 +41,7 @@ fun DevSettingsScreen( ScreenColumn { AppTopBar( titleText = stringResource(R.string.settings__dev_title), - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, actions = { DrawerNavIcon() }, ) Column( @@ -49,12 +49,12 @@ fun DevSettingsScreen( .padding(horizontal = 16.dp) .verticalScroll(rememberScrollState()) ) { - SettingsButtonRow("Fee Settings") { navController.navigate(Routes.FeeSettings) } - SettingsButtonRow("Channel Orders") { navController.navigate(Routes.ChannelOrdersSettings) } - SettingsButtonRow("LDK Debug") { navController.navigate(Routes.LdkDebug) } + SettingsButtonRow("Fee Settings") { navigator.navigate(Routes.Settings.Fee) } + SettingsButtonRow("Channel Orders") { navigator.navigate(Routes.Settings.Dev.ChannelOrders) } + SettingsButtonRow("LDK Debug") { navigator.navigate(Routes.Settings.Dev.LdkDebug) } SectionHeader("LOGS") - SettingsButtonRow("Logs") { navController.navigate(Routes.Logs) } + SettingsButtonRow("Logs") { navigator.navigate(Routes.Settings.Dev.Log.List) } SettingsTextButtonRow( title = "Export Logs", onClick = { @@ -65,7 +65,7 @@ fun DevSettingsScreen( if (Env.network == Network.REGTEST) { SectionHeader("REGTEST") - SettingsButtonRow("Blocktank Regtest") { navController.navigate(Routes.RegtestSettings) } + SettingsButtonRow("Blocktank Regtest") { navigator.navigate(Routes.Settings.Dev.Regtest) } } SectionHeader("APP CACHE") diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt index 34cb37e89..f26cf5a58 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/FeeSettingsScreen.kt @@ -13,11 +13,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.synonym.bitkitcore.FeeRates import to.bitkit.R import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsTextButtonRow +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -27,7 +27,7 @@ import to.bitkit.viewmodels.FeeSettingsViewModel @Composable fun FeeSettingsScreen( - navController: NavController, + navigator: Navigator, viewModel: FeeSettingsViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -38,7 +38,7 @@ fun FeeSettingsScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt index ba3808df1..124cc6473 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/LdkDebugScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.synonym.vssclient.KeyVersion import to.bitkit.R import to.bitkit.env.Env @@ -41,6 +40,7 @@ import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.settings.SectionFooter import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsTextButtonRow +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -54,14 +54,14 @@ import java.io.File @Composable fun LdkDebugScreen( - navController: NavController, + navigator: Navigator, viewModel: LdkDebugViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LdkDebugContent( uiState = uiState, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, onNodeUriChange = viewModel::updateNodeUri, onAddPeer = viewModel::addPeer, onPasteAndAddPeer = viewModel::pasteAndAddPeer, diff --git a/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt b/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt index 17fa1ea7e..9b707ed7f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/shop/ShopIntroScreen.kt @@ -48,7 +48,7 @@ fun ShopIntroScreen( Display( text = stringResource(R.string.other__shop__intro__title).withAccent(accentColor = Colors.Brand), - color = Colors.Yellow + color = Colors.Yellow, ) Spacer(Modifier.height(8.dp)) BodyM(text = stringResource(R.string.other__shop__intro__description), color = Colors.White64) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index c67e05c49..adb3c20b6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -30,7 +30,6 @@ import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton -import to.bitkit.ui.components.Sheet import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +39,6 @@ import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.removeAccentTags import to.bitkit.ui.utils.withAccent import to.bitkit.ui.utils.withAccentBoldBright -import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel @@ -48,10 +46,10 @@ enum class SavingsProgressState { PROGRESS, SUCCESS, INTERRUPTED } @Composable fun SavingsProgressScreen( - app: AppViewModel, transfer: TransferViewModel, wallet: WalletViewModel, onContinueClick: () -> Unit = {}, + onForceTransfer: () -> Unit = {}, ) { val window = LocalActivity.current?.window var progressState by remember { mutableStateOf(SavingsProgressState.PROGRESS) } @@ -69,7 +67,7 @@ fun SavingsProgressScreen( progressState = SavingsProgressState.SUCCESS } else { transfer.startCoopCloseRetries(channelsFailedToCoopClose) { - app.showSheet(Sheet.ForceTransfer) + onForceTransfer() } delay(2500) progressState = SavingsProgressState.INTERRUPTED diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt index 9ccb4a8ee..1688db82a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt @@ -20,11 +20,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.synonym.bitkitcore.regtestMine -import kotlinx.coroutines.delay -import org.lightningdevkit.ldknode.Network import to.bitkit.R -import to.bitkit.env.Env import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display @@ -51,20 +47,7 @@ fun SettingUpScreen( val lightningSetupStep by viewModel.lightningSetupStep.collectAsState() LaunchedEffect(Unit) { - Logger.debug("SettingUp view appeared - TransferViewModel is handling order updates") - - // Auto-mine a block on regtest after a 5-seconds delay - if (Env.network == Network.REGTEST) { - delay(5000) - - try { - Logger.debug("Auto-mining a block", context = "SettingUpScreen") - regtestMine(1u) - Logger.debug("Successfully mined a block", context = "SettingUpScreen") - } catch (e: Throwable) { - Logger.error("Failed to mine block: $e", context = "SettingUpScreen") - } - } + Logger.debug("SettingUp screen appeared - TransferViewModel is handling order updates") } // Effect to disable new transaction sheet for channel purchase diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt index 5d45f2430..3c8e0bace 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalConnectionScreen.kt @@ -31,15 +31,12 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.SavedStateHandle -import kotlinx.coroutines.flow.filterNotNull import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.ext.from import to.bitkit.ext.getClipboardText import to.bitkit.ext.host import to.bitkit.ext.port -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption13Up @@ -50,7 +47,6 @@ import to.bitkit.ui.components.TextInput import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -58,8 +54,24 @@ import to.bitkit.ui.utils.withAccent @Composable fun ExternalConnectionScreen( - route: Routes.ExternalConnection, - savedStateHandle: SavedStateHandle, + scannedNodeUri: String?, + viewModel: ExternalNodeViewModel, + onNodeConnected: () -> Unit, + onScanClick: () -> Unit, + onBackClick: () -> Unit, +) { + ExternalConnectionScreenContent( + scannedNodeUri = scannedNodeUri, + viewModel = viewModel, + onNodeConnected = onNodeConnected, + onScanClick = onScanClick, + onBackClick = onBackClick, + ) +} + +@Composable +private fun ExternalConnectionScreenContent( + scannedNodeUri: String?, viewModel: ExternalNodeViewModel, onNodeConnected: () -> Unit, onScanClick: () -> Unit, @@ -68,23 +80,13 @@ fun ExternalConnectionScreen( val context = LocalContext.current val uiState by viewModel.uiState.collectAsState() - // Handle result from scanner opened from home - LaunchedEffect(route.scannedNodeUri) { - if (route.scannedNodeUri != null) { - viewModel.parseNodeUri(route.scannedNodeUri) + // Handle scanned node URI + LaunchedEffect(scannedNodeUri) { + if (scannedNodeUri != null) { + viewModel.parseNodeUri(scannedNodeUri) } } - // Handle result from scanner opened from this screen - LaunchedEffect(savedStateHandle) { - savedStateHandle.getStateFlow(SCAN_RESULT_KEY, null) - .filterNotNull() - .collect { scannedData -> - viewModel.parseNodeUri(scannedData) - savedStateHandle.remove(SCAN_RESULT_KEY) - } - } - LaunchedEffect(viewModel, onNodeConnected) { viewModel.effects.collect { when (it) { diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt index 78105bfa3..39998c3a7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelScreen.kt @@ -26,7 +26,6 @@ import to.bitkit.R import to.bitkit.env.Peers import to.bitkit.ext.host import to.bitkit.ext.port -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB @@ -45,16 +44,39 @@ import to.bitkit.ui.utils.withAccent @Composable fun LnurlChannelScreen( - route: Routes.LnurlChannel, + uri: String, + callback: String, + k1: String, onConnected: () -> Unit, onBack: () -> Unit, onClose: () -> Unit, viewModel: LnurlChannelViewModel = hiltViewModel(), +) { + LnurlChannelScreenContent( + uri = uri, + callback = callback, + k1 = k1, + onConnected = onConnected, + onBack = onBack, + onClose = onClose, + viewModel = viewModel, + ) +} + +@Composable +private fun LnurlChannelScreenContent( + uri: String, + callback: String, + k1: String, + onConnected: () -> Unit, + onBack: () -> Unit, + onClose: () -> Unit, + viewModel: LnurlChannelViewModel, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.init(route) + viewModel.init(uri, callback, k1) } LaunchedEffect(uiState.isConnected) { diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 21891070d..91176127f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -14,7 +14,6 @@ import to.bitkit.R import to.bitkit.ext.parse import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo -import to.bitkit.ui.Routes import to.bitkit.ui.shared.toast.ToastEventBus import javax.inject.Inject @@ -27,16 +26,20 @@ class LnurlChannelViewModel @Inject constructor( private val _uiState = MutableStateFlow(LnurlChannelUiState()) val uiState = _uiState.asStateFlow() - private lateinit var params: Routes.LnurlChannel + private lateinit var uri: String + private lateinit var callback: String + private lateinit var k1: String - fun init(route: Routes.LnurlChannel) { - this.params = route + fun init(uri: String, callback: String, k1: String) { + this.uri = uri + this.callback = callback + this.k1 = k1 fetchChannelInfo() } private fun fetchChannelInfo() { viewModelScope.launch { - lightningRepo.fetchLnurlChannelInfo(params.uri) + lightningRepo.fetchLnurlChannelInfo(uri) .onSuccess { channelInfo -> val peer = runCatching { PeerDetails.parse(channelInfo.uri) }.getOrElse { errorToast(it) @@ -66,7 +69,7 @@ class LnurlChannelViewModel @Inject constructor( // Connect to peer if not connected lightningRepo.connectPeer(peer) - lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) + lightningRepo.requestLnurlChannel(callback = callback, k1 = k1, nodeId = nodeId) .onSuccess { ToastEventBus.send( type = Toast.ToastType.SUCCESS, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 5e8a6c64d..6be7cc420 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material3.DrawerState -import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -36,7 +35,6 @@ import androidx.compose.material3.VerticalDivider import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState -import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -56,16 +54,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController import com.synonym.bitkitcore.Activity import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.rememberHazeState -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.env.Env @@ -74,13 +68,11 @@ import to.bitkit.models.BalanceState import to.bitkit.models.Suggestion import to.bitkit.models.WidgetType import to.bitkit.ui.LocalBalances -import to.bitkit.ui.Routes import to.bitkit.ui.components.ActivityBanner import to.bitkit.ui.components.AppStatus import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.EmptyStateView import to.bitkit.ui.components.HorizontalSpacer -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.StatusBarSpacer import to.bitkit.ui.components.SuggestionCard import to.bitkit.ui.components.TabBar @@ -91,10 +83,8 @@ import to.bitkit.ui.components.TopBarSpacer import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.WalletBalanceView import to.bitkit.ui.currencyViewModel -import to.bitkit.ui.navigateToActivityItem -import to.bitkit.ui.navigateToAllActivity -import to.bitkit.ui.navigateToTransferFunding -import to.bitkit.ui.navigateToTransferIntro +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.screens.wallets.activity.components.ActivityListSimple import to.bitkit.ui.screens.wallets.activity.utils.previewActivityItems @@ -108,8 +98,6 @@ import to.bitkit.ui.screens.widgets.price.PriceCard import to.bitkit.ui.screens.widgets.weather.WeatherCard import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.util.shareText -import to.bitkit.ui.sheets.BackupRoute -import to.bitkit.ui.sheets.PinRoute import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -123,8 +111,7 @@ import to.bitkit.viewmodels.WalletViewModel fun HomeScreen( mainUiState: MainUiState, drawerState: DrawerState, - rootNavController: NavController, - walletNavController: NavHostController, + navigator: Navigator, settingsViewModel: SettingsViewModel, walletViewModel: WalletViewModel, appViewModel: AppViewModel, @@ -156,12 +143,11 @@ fun HomeScreen( DeleteWidgetAlert(type, homeViewModel) } + val scope = rememberCoroutineScope() + Content( mainUiState = mainUiState, homeUiState = homeUiState, - rootNavController = rootNavController, - walletNavController = walletNavController, - drawerState = drawerState, latestActivities = latestActivities, onRefresh = { activityListViewModel.resync() @@ -170,9 +156,9 @@ fun HomeScreen( }, onClickProfile = { if (!hasSeenProfileIntro) { - rootNavController.navigate(Routes.ProfileIntro) + navigator.navigate(Routes.Profile.Intro) } else { - rootNavController.navigate(Routes.CreateProfile) + navigator.navigate(Routes.Profile.Create) } }, onRemoveSuggestion = { suggestion -> @@ -181,27 +167,27 @@ fun HomeScreen( onClickSuggestion = { suggestion -> when (suggestion) { Suggestion.BUY -> { - rootNavController.navigate(Routes.BuyIntro) + navigator.navigate(Routes.BuyIntro) } Suggestion.LIGHTNING -> { if (!hasSeenTransferIntro) { - rootNavController.navigateToTransferIntro() + navigator.navigate(Routes.Transfer.Intro) } else { - rootNavController.navigateToTransferFunding() + navigator.navigate(Routes.Transfer.Funding) } } Suggestion.BACK_UP -> { - appViewModel.showSheet(Sheet.Backup(BackupRoute.Intro)) + navigator.navigate(Routes.Backup.Intro) } Suggestion.SECURE -> { - appViewModel.showSheet(Sheet.Pin(PinRoute.Prompt(showLaterButton = true))) + navigator.navigate(Routes.Pin.Prompt(showLaterButton = true)) } Suggestion.SUPPORT -> { - rootNavController.navigate(Routes.Support) + navigator.navigate(Routes.Support) } Suggestion.INVITE -> { @@ -215,54 +201,51 @@ fun HomeScreen( Suggestion.PROFILE -> { if (!hasSeenProfileIntro) { - rootNavController.navigate(Routes.ProfileIntro) + navigator.navigate(Routes.Profile.Intro) } else { - rootNavController.navigate(Routes.CreateProfile) + navigator.navigate(Routes.Profile.Create) } } Suggestion.SHOP -> { if (!hasSeenShopIntro) { - rootNavController.navigate(Routes.ShopIntro) + navigator.navigate(Routes.Shop.Intro) } else { - rootNavController.navigate(Routes.ShopDiscover) + navigator.navigate(Routes.Shop.Discover) } } Suggestion.QUICK_PAY -> { - if (!quickPayIntroSeen) { - rootNavController.navigate(Routes.QuickPayIntro) - } else { - rootNavController.navigate(Routes.QuickPaySettings) - } + val route = if (quickPayIntroSeen) Routes.QuickPay.Settings else Routes.QuickPay.Intro + navigator.navigate(route) } Suggestion.NOTIFICATIONS -> { if (bgPaymentsIntroSeen) { - rootNavController.navigate(Routes.BackgroundPaymentsSettings) + navigator.navigate(Routes.BackgroundPayments.Settings) } else { - rootNavController.navigate(Routes.BackgroundPaymentsIntro) + navigator.navigate(Routes.BackgroundPayments.Intro) } } } }, onClickAddWidget = { if (!hasSeenWidgetsIntro) { - rootNavController.navigate(Routes.WidgetsIntro) + navigator.navigate(Routes.Widgets.Intro) } else { - rootNavController.navigate(Routes.AddWidget) + navigator.navigate(Routes.Widgets.Add) } }, onClickEditWidgetList = homeViewModel::onClickEditWidgetList, onClickEditWidget = { widgetType -> homeViewModel.disableEditMode() when (widgetType) { - WidgetType.BLOCK -> rootNavController.navigate(Routes.BlocksPreview) - WidgetType.CALCULATOR -> rootNavController.navigate(Routes.CalculatorPreview) - WidgetType.FACTS -> rootNavController.navigate(Routes.FactsPreview) - WidgetType.NEWS -> rootNavController.navigate(Routes.HeadlinesPreview) - WidgetType.PRICE -> rootNavController.navigate(Routes.PricePreview) - WidgetType.WEATHER -> rootNavController.navigate(Routes.WeatherPreview) + WidgetType.BLOCK -> navigator.navigate(Routes.Widgets.Blocks.Preview) + WidgetType.CALCULATOR -> navigator.navigate(Routes.Widgets.Calculator.Preview) + WidgetType.FACTS -> navigator.navigate(Routes.Widgets.Facts.Preview) + WidgetType.NEWS -> navigator.navigate(Routes.Widgets.Headlines.Preview) + WidgetType.PRICE -> navigator.navigate(Routes.Widgets.Price.Preview) + WidgetType.WEATHER -> navigator.navigate(Routes.Widgets.Weather.Preview) } }, onClickDeleteWidget = { widgetType -> @@ -272,7 +255,14 @@ fun HomeScreen( homeViewModel.moveWidget(fromIndex, toIndex) }, onDismissEmptyState = homeViewModel::dismissEmptyState, - onClickEmptyActivityRow = { appViewModel.showSheet(Sheet.Receive) }, + onClickEmptyActivityRow = { navigator.navigate(Routes.Receive.Qr) }, + onClickSavings = { navigator.navigate(Routes.Savings) }, + onClickSpending = { navigator.navigate(Routes.Spending) }, + onAllActivityClick = { navigator.navigate(Routes.Activity.All) }, + onActivityItemClick = { navigator.navigate(Routes.Activity.Detail(it)) }, + onClickSettingUp = { navigator.navigate(Routes.Transfer.SettingUp) }, + onClickAppStatus = { navigator.navigate(Routes.AppStatus) }, + onOpenDrawer = { scope.launch { drawerState.open() } }, ) } @@ -281,9 +271,6 @@ fun HomeScreen( private fun Content( mainUiState: MainUiState, homeUiState: HomeUiState, - rootNavController: NavController, - walletNavController: NavController, - drawerState: DrawerState, hazeState: HazeState = rememberHazeState(), latestActivities: List?, onClickProfile: () -> Unit = {}, @@ -297,6 +284,13 @@ private fun Content( onMoveWidget: (Int, Int) -> Unit = { _, _ -> }, onDismissEmptyState: () -> Unit = {}, onClickEmptyActivityRow: () -> Unit = {}, + onClickSavings: () -> Unit = {}, + onClickSpending: () -> Unit = {}, + onAllActivityClick: () -> Unit = {}, + onActivityItemClick: (Activity) -> Unit = {}, + onClickSettingUp: () -> Unit = {}, + onClickAppStatus: () -> Unit = {}, + onOpenDrawer: () -> Unit = {}, balances: BalanceState = LocalBalances.current, ) { val scope = rememberCoroutineScope() @@ -306,9 +300,8 @@ private fun Content( TopBar( hazeState = hazeState, onClickProfile = onClickProfile, - rootNavController = rootNavController, - scope = scope, - drawerState = drawerState, + onClickAppStatus = onClickAppStatus, + onOpenDrawer = onOpenDrawer, ) val pullToRefreshState = rememberPullToRefreshState() PullToRefreshBox( @@ -359,7 +352,7 @@ private fun Content( sats = balances.totalOnchainSats.toLong(), icon = painterResource(id = R.drawable.ic_btc_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Savings) } + .clickableAlpha { onClickSavings() } .padding(vertical = 4.dp) .testTag("ActivitySavings") ) @@ -370,7 +363,7 @@ private fun Content( sats = balances.totalLightningSats.toLong(), icon = painterResource(id = R.drawable.ic_ln_circle), modifier = Modifier - .clickableAlpha { walletNavController.navigate(Routes.Spending) } + .clickableAlpha { onClickSpending() } .padding(vertical = 4.dp) .testTag("ActivitySpending") ) @@ -401,8 +394,8 @@ private fun Content( title = stringResource(item.title), description = stringResource(item.description), icon = item.icon, - onClose = { onRemoveSuggestion(item) }.takeIf { item.dismissible }, onClick = { onClickSuggestion(item) }, + onClose = { onRemoveSuggestion(item) }.takeIf { item.dismissible }, modifier = Modifier.testTag("Suggestion-${item.name.lowercase()}") ) } @@ -490,7 +483,7 @@ private fun Content( icon = banner.icon, onClick = { when (banner) { - ActivityBannerType.SPENDING -> rootNavController.navigate(Routes.SettingUp) + ActivityBannerType.SPENDING -> onClickSettingUp() ActivityBannerType.SAVINGS -> Unit } }, @@ -502,8 +495,8 @@ private fun Content( ActivityListSimple( items = latestActivities, - onAllActivityClick = { rootNavController.navigateToAllActivity() }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, + onAllActivityClick = onAllActivityClick, + onActivityItemClick = onActivityItemClick, onEmptyActivityRowClick = onClickEmptyActivityRow, ) @@ -625,9 +618,8 @@ private fun Widgets(homeUiState: HomeUiState) { private fun TopBar( hazeState: HazeState, onClickProfile: () -> Unit, - rootNavController: NavController, - scope: CoroutineScope, - drawerState: DrawerState, + onClickAppStatus: () -> Unit, + onOpenDrawer: () -> Unit, ) { val topbarGradient = Brush.verticalGradient( colorStops = arrayOf( @@ -667,10 +659,10 @@ private fun TopBar( } }, actions = { - AppStatus(onClick = { rootNavController.navigate(Routes.AppStatus) }) + AppStatus(onClick = onClickAppStatus) HorizontalSpacer(4.dp) IconButton( - onClick = { scope.launch { drawerState.open() } }, + onClick = onOpenDrawer, modifier = Modifier.testTag("HeaderMenu") ) { Icon( @@ -679,7 +671,7 @@ private fun TopBar( ) } }, - colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(Color.Transparent), modifier = Modifier.fillMaxWidth() ) } @@ -692,14 +684,11 @@ private fun DeleteWidgetAlert( ) { AppAlertDialog( title = stringResource(R.string.widgets__delete__title), - text = stringResource(R.string.widgets__delete__description) - .replace("{name}", stringResource(type.title)), + text = stringResource(R.string.widgets__delete__description).replace("{name}", stringResource(type.title)), confirmText = stringResource(R.string.common__delete_yes), dismissText = stringResource(R.string.common__dialog_cancel), onConfirm = { homeViewModel.deleteWidget(widgetType = type) }, - onDismiss = { - homeViewModel.dismissAlertDeleteWidget() - }, + onDismiss = homeViewModel::dismissAlertDeleteWidget, ) } @@ -713,9 +702,6 @@ private fun Preview() { homeUiState = HomeUiState( showWidgets = true, ), - rootNavController = rememberNavController(), - walletNavController = rememberNavController(), - drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), latestActivities = previewActivityItems.take(3), balances = BalanceState( totalOnchainSats = 165_000u, @@ -737,9 +723,6 @@ private fun PreviewEmpty() { homeUiState = HomeUiState( showEmptyState = true, ), - rootNavController = rememberNavController(), - walletNavController = rememberNavController(), - drawerState = rememberDrawerState(initialValue = DrawerValue.Closed), latestActivities = previewActivityItems.take(3), balances = BalanceState() ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt index d071dfe6d..00f5b098c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SavingsWalletScreen.kt @@ -49,7 +49,7 @@ fun SavingsWalletScreen( onchainActivities: List, onAllActivityButtonClick: () -> Unit, onEmptyActivityRowClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onTransferToSpendingClick: () -> Unit, onBackClick: () -> Unit, balances: BalanceState = LocalBalances.current, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt index 82460dca1..a9b9997b6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/SpendingWalletScreen.kt @@ -50,7 +50,7 @@ fun SpendingWalletScreen( uiState: MainUiState, lightningActivities: List, onAllActivityButtonClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, onTransferToSavingsClick: () -> Unit, onBackClick: () -> Unit, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 06afb3e21..6611845ca 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -59,7 +59,6 @@ import to.bitkit.ext.toActivityItemTime import to.bitkit.ext.totalValue import to.bitkit.models.FeeRate import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BalanceHeaderView @@ -71,6 +70,8 @@ import to.bitkit.ui.components.MoneySSB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.Title +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.screens.wallets.activity.components.ActivityAddTagSheet @@ -85,12 +86,30 @@ import to.bitkit.ui.utils.getScreenTitleRes import to.bitkit.viewmodels.ActivityDetailViewModel import to.bitkit.viewmodels.ActivityListViewModel -@Suppress("CyclomaticComplexMethod") @Composable fun ActivityDetailScreen( + navigator: Navigator, + activityId: String, listViewModel: ActivityListViewModel, detailViewModel: ActivityDetailViewModel = hiltViewModel(), - route: Routes.ActivityDetail, +) { + ActivityDetailScreenContent( + activityId = activityId, + listViewModel = listViewModel, + detailViewModel = detailViewModel, + onExploreClick = { id -> navigator.navigate(Routes.Activity.Explore(id)) }, + onBackClick = { navigator.goBack() }, + onCloseClick = { navigator.navigateToHome() }, + onChannelClick = { navigator.navigate(Routes.Settings.ChannelDetail) }, + ) +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun ActivityDetailScreenContent( + activityId: String, + listViewModel: ActivityListViewModel, + detailViewModel: ActivityDetailViewModel, onExploreClick: (String) -> Unit, onBackClick: () -> Unit, onCloseClick: () -> Unit, @@ -99,8 +118,8 @@ fun ActivityDetailScreen( val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() // Load activity on composition - LaunchedEffect(route.id) { - detailViewModel.loadActivity(route.id) + LaunchedEffect(activityId) { + detailViewModel.loadActivity(activityId) } // Clear state on disposal diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 7ac978f5f..31f337968 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -46,12 +46,12 @@ import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -67,15 +67,28 @@ import to.bitkit.viewmodels.ActivityDetailViewModel @Composable fun ActivityExploreScreen( + navigator: Navigator, + activityId: String, detailViewModel: ActivityDetailViewModel = hiltViewModel(), - route: Routes.ActivityExplore, +) { + ActivityExploreScreenContent( + activityId = activityId, + detailViewModel = detailViewModel, + onBackClick = { navigator.goBack() }, + ) +} + +@Composable +private fun ActivityExploreScreenContent( + activityId: String, + detailViewModel: ActivityDetailViewModel, onBackClick: () -> Unit, ) { val uiState by detailViewModel.uiState.collectAsStateWithLifecycle() // Load activity on composition - LaunchedEffect(route.id) { - detailViewModel.loadActivity(route.id) + LaunchedEffect(activityId) { + detailViewModel.loadActivity(activityId) } // Clear state on disposal diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt index a6558b341..e90e6e1cc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt @@ -20,8 +20,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.synonym.bitkitcore.Activity import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import to.bitkit.R -import to.bitkit.ui.appViewModel -import to.bitkit.ui.components.Sheet import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.screens.wallets.activity.components.ActivityListFilter @@ -38,9 +36,11 @@ import to.bitkit.viewmodels.ActivityListViewModel fun AllActivityScreen( viewModel: ActivityListViewModel, onBack: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, + onTagClick: () -> Unit, + onDateRangeClick: () -> Unit, + onEmptyActivityRowClick: () -> Unit, ) { - val app = appViewModel ?: return val filteredActivities by viewModel.filteredActivities.collectAsStateWithLifecycle() val searchText by viewModel.searchText.collectAsStateWithLifecycle() @@ -65,10 +65,10 @@ fun AllActivityScreen( onRemoveTag = { viewModel.toggleTag(it) }, onTabChange = { viewModel.setTab(tabs[it]) }, onBackClick = onBack, - onTagClick = { app.showSheet(Sheet.ActivityTagSelector) }, - onDateRangeClick = { app.showSheet(Sheet.ActivityDateRangeSelector) }, + onTagClick = onTagClick, + onDateRangeClick = onDateRangeClick, onActivityItemClick = onActivityItemClick, - onEmptyActivityRowClick = { app.showSheet(Sheet.Receive) }, + onEmptyActivityRowClick = onEmptyActivityRowClick, ) } @@ -88,7 +88,7 @@ private fun AllActivityScreenContent( onBackClick: () -> Unit, onTagClick: () -> Unit, onDateRangeClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, ) { Column( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt index 6c47947fa..daf73414b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/DateRangeSelectorSheet.kt @@ -119,7 +119,7 @@ fun DateRangeSelectorSheet() { app.hideSheet() } - Content( + DateRangeSelectorContent( initialStartDate = startDate, initialEndDate = endDate, onClearClick = { @@ -137,7 +137,7 @@ fun DateRangeSelectorSheet() { @Suppress("MaxLineLength", "CyclomaticComplexMethod") @Composable -private fun Content( +internal fun DateRangeSelectorContent( initialStartDate: Long? = null, initialEndDate: Long? = null, onClearClick: () -> Unit = {}, @@ -638,7 +638,7 @@ private fun LocalDate.toFormattedString(): String { private fun PreviewEmpty() { AppThemeSurface { BottomSheetPreview { - Content() + DateRangeSelectorContent() } } } @@ -648,7 +648,7 @@ private fun PreviewEmpty() { private fun PreviewWithSelection() { AppThemeSurface { BottomSheetPreview { - Content( + DateRangeSelectorContent( initialStartDate = now() .minus(CalendarConstants.DAYS_IN_WEEK.days) .toEpochMilliseconds(), diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt index 3d178f85a..f99371b35 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt @@ -42,7 +42,7 @@ fun TagSelectorSheet() { activity.updateAvailableTags() } - Content( + TagSelectorContent( availableTags = availableTags, selectedTags = selectedTags, onTagClick = { @@ -53,7 +53,7 @@ fun TagSelectorSheet() { } @Composable -private fun Content( +internal fun TagSelectorContent( availableTags: List, selectedTags: Set, onTagClick: (String) -> Unit = {}, @@ -102,7 +102,7 @@ private fun Content( private fun Preview() { AppThemeSurface { BottomSheetPreview { - Content( + TagSelectorContent( availableTags = listOf("Bitcoin", "Lightning", "Sent", "Received"), selectedTags = setOf("Bitcoin", "Received"), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt index 3fabceb3a..ec982142c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListGrouped.kt @@ -37,7 +37,7 @@ import java.util.Locale @Composable fun ActivityListGrouped( items: List?, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, modifier: Modifier = Modifier, showFooter: Boolean = false, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt index cc476b42a..cf6768700 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityListSimple.kt @@ -22,7 +22,7 @@ import to.bitkit.ui.theme.AppThemeSurface fun ActivityListSimple( items: List?, onAllActivityClick: () -> Unit, - onActivityItemClick: (String) -> Unit, + onActivityItemClick: (Activity) -> Unit, onEmptyActivityRowClick: () -> Unit, ) { Column( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt index b7c9d7638..69bc6fef0 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt @@ -33,7 +33,6 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.formatted import to.bitkit.ext.isSent import to.bitkit.ext.isTransfer -import to.bitkit.ext.rawId import to.bitkit.ext.timestamp import to.bitkit.ext.totalValue import to.bitkit.ext.txType @@ -61,7 +60,7 @@ import java.time.ZoneId @Composable fun ActivityRow( item: Activity, - onClick: (String) -> Unit, + onClick: (Activity) -> Unit, testTag: String, ) { val blocktankInfo by blocktankViewModel?.info?.collectAsStateWithLifecycle() ?: remember { @@ -99,7 +98,7 @@ fun ActivityRow( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickableAlpha { onClick(item.rawId()) } + .clickableAlpha { onClick(item) } .background(color = Colors.Gray6, shape = Shapes.medium) .padding(16.dp) .testTag(testTag) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index 09484c735..0429ef582 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -10,11 +10,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -90,8 +88,6 @@ fun ReceiveQrScreen( modifier: Modifier = Modifier, initialTab: ReceiveTab? = null, ) { - SetMaxBrightness() - val haptic = LocalHapticFeedback.current val hasUsableChannels = walletState.channels.any { it.isChannelReady } @@ -175,9 +171,12 @@ fun ReceiveQrScreen( } val showingCjitOnboarding = remember(walletState, cjitInvoice, hasUsableChannels) { - !hasUsableChannels && - walletState.nodeLifecycleState.isRunning() && - cjitInvoice.isNullOrEmpty() + !hasUsableChannels && walletState.nodeLifecycleState.isRunning() && cjitInvoice.isNullOrEmpty() + } + + val showingQrCode = !showDetails && !(showingCjitOnboarding && selectedTab == ReceiveTab.SPENDING) + if (showingQrCode) { + SetMaxBrightness() } Column( @@ -189,7 +188,7 @@ fun ReceiveQrScreen( ) { SheetTopBar(stringResource(R.string.wallet__receive_bitcoin)) Column { - Spacer(Modifier.height(16.dp)) + VerticalSpacer(16.dp) // Tab row CustomTabRowWithSpacing( @@ -211,7 +210,7 @@ fun ReceiveQrScreen( modifier = Modifier.padding(horizontal = 16.dp) ) - Spacer(Modifier.height(24.dp)) + VerticalSpacer(24.dp) // Content area (QR or Details) with LazyRow LazyRow( @@ -271,7 +270,7 @@ fun ReceiveQrScreen( } } - Spacer(Modifier.height(24.dp)) + VerticalSpacer(24.dp) AnimatedVisibility(visible = walletState.nodeLifecycleState.isRunning()) { val showCjitButton = showingCjitOnboarding && selectedTab == ReceiveTab.SPENDING @@ -305,16 +304,12 @@ fun ReceiveQrScreen( modifier = Modifier .padding(horizontal = 16.dp) .testTag( - if (showDetails) { - "QRCode" - } else { - "ShowDetails" - } + if (showDetails) "QRCode" else "ShowDetails" ) ) } - Spacer(Modifier.height(16.dp)) + VerticalSpacer(16.dp) } } } @@ -346,7 +341,7 @@ private fun ReceiveQrView( modifier = Modifier.weight(1f, fill = false) ) - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) Row( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Top, @@ -417,18 +412,18 @@ private fun ReceiveQrView( modifier = Modifier.weight(1f) ) } - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) } } @Composable fun CjitOnBoardingView(modifier: Modifier = Modifier) { Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier .clip(AppShapes.small) .background(color = Colors.Black) - .padding(32.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(32.dp) ) { Display(stringResource(R.string.wallet__receive_onboarding_title).withAccent(accentColor = Colors.Purple)) VerticalSpacer(8.dp) @@ -560,12 +555,12 @@ private fun CopyAddressCard( .padding(24.dp) ) { Caption13Up(text = title, color = Colors.White64) - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) BodyS( text = (body ?: address).truncate(32).uppercase(), modifier = testTag?.let { Modifier.testTag(it) } ?: Modifier ) - Spacer(modifier = Modifier.height(16.dp)) + VerticalSpacer(16.dp) Row( horizontalArrangement = Arrangement.spacedBy(16.dp) ) { diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt deleted file mode 100644 index 3cee808dc..000000000 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ /dev/null @@ -1,217 +0,0 @@ -package to.bitkit.ui.screens.wallets.receive - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable -import to.bitkit.repositories.LightningState -import to.bitkit.ui.screens.wallets.send.AddTagScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.NotificationUtils -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.ui.walletViewModel -import to.bitkit.viewmodels.AmountInputViewModel -import to.bitkit.viewmodels.MainUiState -import to.bitkit.viewmodels.SettingsViewModel - -@Composable -fun ReceiveSheet( - navigateToExternalConnection: () -> Unit, - walletState: MainUiState, - editInvoiceAmountViewModel: AmountInputViewModel = hiltViewModel(), - settingsViewModel: SettingsViewModel = hiltViewModel(), -) { - val wallet = requireNotNull(walletViewModel) - val navController = rememberNavController() - - LaunchedEffect(Unit) { editInvoiceAmountViewModel.clearInput() } - - val cjitInvoice = remember { mutableStateOf(null) } - val showCreateCjit = remember { mutableStateOf(false) } - val cjitEntryDetails = remember { mutableStateOf(null) } - val lightningState: LightningState by wallet.lightningState.collectAsStateWithLifecycle() - - LaunchedEffect(Unit) { - wallet.resetPreActivityMetadataTagsForCurrentInvoice() - wallet.refreshReceiveState() - } - - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("ReceiveScreen") - ) { - NavHost( - navController = navController, - startDestination = ReceiveRoute.QR, - ) { - composableWithDefaultTransitions { - LaunchedEffect(cjitInvoice.value) { - showCreateCjit.value = !cjitInvoice.value.isNullOrBlank() - } - - ReceiveQrScreen( - cjitInvoice = cjitInvoice.value, - walletState = walletState, - onClickReceiveCjit = { - if (lightningState.isGeoBlocked) { - navController.navigate(ReceiveRoute.GeoBlock) - } else { - showCreateCjit.value = true - navController.navigate(ReceiveRoute.Amount) - } - }, - onClickEditInvoice = { navController.navigate(ReceiveRoute.EditInvoice) }, - ) - } - composableWithDefaultTransitions { - ReceiveAmountScreen( - onCjitCreated = { entry -> - cjitEntryDetails.value = entry - navController.navigate(ReceiveRoute.Confirm) - }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - LocationBlockScreen( - onBackPressed = { navController.popBackStack() }, - navigateAdvancedSetup = navigateToExternalConnection, - ) - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - ReceiveConfirmScreen( - entry = entryDetails, - onLearnMore = { navController.navigate(ReceiveRoute.Liquidity) }, - onContinue = { invoice -> - cjitInvoice.value = invoice - navController.navigate(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } - }, - onBack = { navController.popBackStack() }, - ) - } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - ReceiveConfirmScreen( - entry = entryDetails, - onLearnMore = { navController.navigate(ReceiveRoute.LiquidityAdditional) }, - onContinue = { invoice -> - cjitInvoice.value = invoice - navController.navigate(ReceiveRoute.QR) { popUpTo(ReceiveRoute.QR) { inclusive = true } } - }, - isAdditional = true, - onBack = { navController.popBackStack() }, - ) - } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - val context = LocalContext.current - val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - - ReceiveLiquidityScreen( - entry = entryDetails, - onContinue = { navController.popBackStack() }, - onBack = { navController.popBackStack() }, - hasNotificationPermission = notificationsGranted, - onSwitchClick = { - NotificationUtils.openNotificationSettings(context) - }, - ) - } - } - composableWithDefaultTransitions { - cjitEntryDetails.value?.let { entryDetails -> - val context = LocalContext.current - val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - - ReceiveLiquidityScreen( - entry = entryDetails, - onContinue = { navController.popBackStack() }, - isAdditional = true, - onBack = { navController.popBackStack() }, - hasNotificationPermission = notificationsGranted, - onSwitchClick = { - NotificationUtils.openNotificationSettings(context) - }, - ) - } - } - composableWithDefaultTransitions { - val walletUiState by wallet.walletState.collectAsStateWithLifecycle() - @Suppress("ViewModelForwarding") - EditInvoiceScreen( - amountInputViewModel = editInvoiceAmountViewModel, - walletUiState = walletUiState, - onBack = { navController.popBackStack() }, - updateInvoice = wallet::updateBip21Invoice, - onClickAddTag = { navController.navigate(ReceiveRoute.AddTag) }, - onClickTag = wallet::removeTag, - onDescriptionUpdate = wallet::updateBip21Description, - navigateReceiveConfirm = { entry -> - cjitEntryDetails.value = entry - navController.navigate(ReceiveRoute.ConfirmIncreaseInbound) - } - ) - } - composableWithDefaultTransitions { - AddTagScreen( - onBack = { - navController.popBackStack() - }, - onTagSelected = { tag -> - wallet.addTagToSelected(tag) - navController.popBackStack() - }, - tqgInputTestTag = "TagInputReceive", - addButtonTestTag = "ReceiveTagsSubmit", - ) - } - } - } -} - -sealed interface ReceiveRoute { - @Serializable - data object QR : ReceiveRoute - - @Serializable - data object Amount : ReceiveRoute - - @Serializable - data object Confirm : ReceiveRoute - - @Serializable - data object ConfirmIncreaseInbound : ReceiveRoute - - @Serializable - data object Liquidity : ReceiveRoute - - @Serializable - data object LiquidityAdditional : ReceiveRoute - - @Serializable - data object EditInvoice : ReceiveRoute - - @Serializable - data object AddTag : ReceiveRoute - - @Serializable - data object GeoBlock : ReceiveRoute -} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt index 1de166d58..34a24c75e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt @@ -32,12 +32,12 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.viewmodels.AddTagUiState import to.bitkit.viewmodels.TagsViewModel @@ -81,7 +81,7 @@ fun AddTagContent( val focusRequester = remember { FocusRequester() } LaunchedEffect(focusOnShow) { if (focusOnShow) { - delay(TRANSITION_SCREEN_MS) + delay(MS_TRANSITION_SCREEN) focusRequester.requestFocus() } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt index 7ced344e8..49b8f9d4e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAddressScreen.kt @@ -27,13 +27,13 @@ import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.viewmodels.SendEvent import to.bitkit.viewmodels.SendUiState @@ -46,7 +46,7 @@ fun SendAddressScreen( ) { val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { - delay(TRANSITION_SCREEN_MS) + delay(MS_TRANSITION_SCREEN) focusRequester.requestFocus() } Column( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 640e47aa8..18d1ab96c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -68,6 +68,7 @@ import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.RectangleButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.scanner.QrCodeAnalyzer import to.bitkit.ui.shared.modifiers.sheetHeight @@ -75,7 +76,6 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.ui.utils.withAccent import to.bitkit.utils.Logger import to.bitkit.viewmodels.SendEvent @@ -150,7 +150,7 @@ fun SendRecipientScreen( LaunchedEffect(cameraPermissionState.status, isCameraInitialized) { if (cameraPermissionState.status.isGranted && !isCameraInitialized) { runCatching { - delay(TRANSITION_SCREEN_MS) + delay(MS_TRANSITION_SCREEN) imageAnalysis.setAnalyzer(executor, analyzer) val cameraProvider = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt index 1bd2beb90..be81759d0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt @@ -16,13 +16,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow -import to.bitkit.ui.navigateToHome +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -31,37 +30,37 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun AdvancedSettingsScreen( - navController: NavController, + navigator: Navigator, viewModel: AdvancedSettingsViewModel = hiltViewModel(), ) { var showResetSuggestionsDialog by remember { mutableStateOf(false) } Content( showResetSuggestionsDialog = showResetSuggestionsDialog, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onCoinSelectionClick = { - navController.navigate(Routes.CoinSelectPreference) + navigator.navigate(Routes.Settings.CoinSelectPreference) }, onLightningConnectionsClick = { - navController.navigate(Routes.LightningConnections) + navigator.navigate(Routes.Settings.LightningConnections) }, onLightningNodeClick = { - navController.navigate(Routes.NodeInfo) + navigator.navigate(Routes.Settings.NodeInfo) }, onElectrumServerClick = { - navController.navigate(Routes.ElectrumConfig) + navigator.navigate(Routes.Settings.ElectrumConfig) }, onRgsServerClick = { - navController.navigate(Routes.RgsServer) + navigator.navigate(Routes.Settings.RgsServer) }, onAddressViewerClick = { - navController.navigate(Routes.AddressViewer) + navigator.navigate(Routes.Settings.AddressViewer) }, onSuggestionsResetClick = { showResetSuggestionsDialog = true }, onResetSuggestionsDialogConfirm = { viewModel.resetSuggestions() showResetSuggestionsDialog = false - navController.navigateToHome() + navigator.navigateToHome() }, onResetSuggestionsDialogCancel = { showResetSuggestionsDialog = false }, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt index 558d127f2..591cdc2cd 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt @@ -25,24 +25,21 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.toRelativeTimeString import to.bitkit.models.BackupCategory import to.bitkit.models.BackupItemStatus -import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.backupsViewModel import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.FillWidth -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow -import to.bitkit.ui.navigateToAuthCheck +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -57,9 +54,8 @@ import kotlin.time.ExperimentalTime @Composable fun BackupSettingsScreen( - navController: NavController, + navigator: Navigator, ) { - val app = appViewModel ?: return val settings = settingsViewModel ?: return val viewModel = backupsViewModel ?: return @@ -68,16 +64,16 @@ fun BackupSettingsScreen( BackupSettingsScreenContent( uiState = uiState, - onBackupClick = { app.showSheet(Sheet.Backup()) }, + onBackupClick = { navigator.navigate(Routes.Backup.Intro) }, onResetAndRestoreClick = { if (isPinEnabled) { - navController.navigateToAuthCheck(onSuccessActionId = AuthCheckAction.NAV_TO_RESET) + navigator.navigate(Routes.AuthCheck(onSuccessActionId = AuthCheckAction.NAV_TO_RESET)) } else { - navController.navigate(Routes.ResetAndRestoreSettings) + navigator.navigate(Routes.Settings.ResetAndRestore) } }, onRetryBackup = { category -> viewModel.retryBackup(category) }, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index 47778d650..60ca7cba0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast @@ -35,6 +34,7 @@ import to.bitkit.ui.components.Caption import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -44,8 +44,19 @@ import to.bitkit.utils.Logger @Composable fun BlocktankRegtestScreen( - navController: NavController, + navigator: Navigator, viewModel: BlocktankRegtestViewModel = hiltViewModel(), +) { + BlocktankRegtestContent( + onBack = { navigator.goBack() }, + viewModel = viewModel, + ) +} + +@Composable +private fun BlocktankRegtestContent( + onBack: () -> Unit, + viewModel: BlocktankRegtestViewModel, ) { val coroutineScope = rememberCoroutineScope() val wallet = walletViewModel ?: return @@ -55,7 +66,7 @@ fun BlocktankRegtestScreen( ScreenColumn { AppTopBar( titleText = "Blocktank Regtest", - onBackClick = { navController.popBackStack() }, + onBackClick = onBack, actions = { DrawerNavIcon() }, ) Column( diff --git a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt index ed5acd4ed..cb3f345e6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/ChannelOrdersScreen.kt @@ -42,7 +42,6 @@ import com.synonym.bitkitcore.ILspNode import com.synonym.bitkitcore.IcJitEntry import kotlinx.coroutines.launch import to.bitkit.models.formatToModernDisplay -import to.bitkit.ui.Routes import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BodySSB @@ -135,12 +134,12 @@ private fun Content( @Composable fun OrderDetailScreen( - orderItem: Routes.OrderDetail, + orderId: String, onBackClick: () -> Unit = {}, ) { val blocktank = blocktankViewModel ?: return val orders by blocktank.orders.collectAsStateWithLifecycle() - val order = orders.find { it.id == orderItem.id } ?: return + val order = orders.find { it.id == orderId } ?: return val coroutineScope = rememberCoroutineScope() OrderDetailContent( @@ -239,12 +238,12 @@ private fun OrderDetailContent( @Composable fun CJitDetailScreen( - cjitItem: Routes.CjitDetail, + entryId: String, onBackClick: () -> Unit = {}, ) { val blocktank = blocktankViewModel ?: return val cJitEntries by blocktank.cJitEntries.collectAsStateWithLifecycle() - val entry = cJitEntries.find { it.id == cjitItem.id } ?: return + val entry = cJitEntries.find { it.id == entryId } ?: return CJitDetailContent( entry = entry, onBack = onBackClick, diff --git a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt index 0d7607cb3..aeeacf0db 100644 --- a/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/LogsScreen.kt @@ -33,10 +33,10 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavController import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption -import to.bitkit.ui.navigateToLogDetail +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha @@ -45,7 +45,7 @@ import to.bitkit.viewmodels.LogsViewModel @Composable fun LogsScreen( - navController: NavController, + navigator: Navigator, viewModel: LogsViewModel = hiltViewModel(), ) { val logs by viewModel.logs.collectAsState() @@ -58,7 +58,7 @@ fun LogsScreen( ScreenColumn { AppTopBar( titleText = "Log Files", - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, actions = { IconButton( onClick = { showDeleteConfirmation = true }, @@ -85,7 +85,7 @@ fun LogsScreen( ) }, modifier = Modifier.clickableAlpha { - navController.navigateToLogDetail(logFile.fileName) + navigator.navigate(Routes.Settings.Dev.Log.Detail(logFile.fileName)) } ) HorizontalDivider() @@ -120,7 +120,7 @@ fun LogsScreen( @Composable fun LogDetailScreen( - navController: NavController, + navigator: Navigator, fileName: String, viewModel: LogsViewModel = hiltViewModel(), ) { @@ -152,7 +152,7 @@ fun LogDetailScreen( ScreenColumn { AppTopBar( titleText = logs.find { it.fileName == fileName }?.displayName ?: "Log Content", - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, actions = { IconButton( onClick = { diff --git a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt index 5ca72a89f..1232a717d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt @@ -12,18 +12,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyS -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue import to.bitkit.ui.components.settings.SettingsSwitchRow -import to.bitkit.ui.navigateToAuthCheck -import to.bitkit.ui.navigateToChangePin -import to.bitkit.ui.navigateToDisablePin +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -34,10 +30,9 @@ import to.bitkit.ui.utils.rememberBiometricAuthSupported @Composable fun SecuritySettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return - val app = appViewModel ?: return val isPinEnabled by settings.isPinEnabled.collectAsStateWithLifecycle() val isPinOnLaunchEnabled by settings.isPinOnLaunchEnabled.collectAsStateWithLifecycle() @@ -62,33 +57,35 @@ fun SecuritySettingsScreen( isBiometrySupported = rememberBiometricAuthSupported(), onPinClick = { if (!isPinEnabled) { - app.showSheet(Sheet.Pin()) + navigator.navigate(Routes.Pin.Prompt()) } else { - navController.navigateToDisablePin() + navigator.navigate(Routes.Settings.DisablePin) } }, onChangePinClick = { - navController.navigateToChangePin() + navigator.navigate(Routes.Pin.Change.Start) }, onPinOnLaunchClick = { - navController.navigateToAuthCheck( - onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_LAUNCH, + navigator.navigate( + Routes.AuthCheck(onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_LAUNCH) ) }, onPinOnIdleClick = { - navController.navigateToAuthCheck( - onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_IDLE, + navigator.navigate( + Routes.AuthCheck(onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_IDLE) ) }, onPinForPaymentsClick = { - navController.navigateToAuthCheck( - onSuccessActionId = AuthCheckAction.TOGGLE_PIN_FOR_PAYMENTS, + navigator.navigate( + Routes.AuthCheck(onSuccessActionId = AuthCheckAction.TOGGLE_PIN_FOR_PAYMENTS) ) }, onUseBiometricsClick = { - navController.navigateToAuthCheck( - requireBiometrics = true, - onSuccessActionId = AuthCheckAction.TOGGLE_BIOMETRICS, + navigator.navigate( + Routes.AuthCheck( + requireBiometrics = true, + onSuccessActionId = AuthCheckAction.TOGGLE_BIOMETRICS, + ) ) }, onSwipeToHideBalanceClick = { @@ -103,7 +100,7 @@ fun SecuritySettingsScreen( onSendAmountWarningClick = { settings.setEnableSendAmountWarning(!enableSendAmountWarning) }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index 823a5d24d..c233d116e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -24,18 +24,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SettingsButtonRow -import to.bitkit.ui.navigateToAboutSettings -import to.bitkit.ui.navigateToAdvancedSettings -import to.bitkit.ui.navigateToBackupSettings -import to.bitkit.ui.navigateToDevSettings -import to.bitkit.ui.navigateToGeneralSettings -import to.bitkit.ui.navigateToSecuritySettings +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -47,7 +41,7 @@ private const val DEV_MODE_TAP_THRESHOLD = 5 @Composable fun SettingsScreen( - navController: NavController, + navigator: Navigator, ) { val app = appViewModel ?: return val settings = settingsViewModel ?: return @@ -58,14 +52,14 @@ fun SettingsScreen( SettingsScreenContent( isDevModeEnabled = isDevModeEnabled, - onGeneralClick = { navController.navigateToGeneralSettings() }, - onSecurityClick = { navController.navigateToSecuritySettings() }, - onBackupClick = { navController.navigateToBackupSettings() }, - onAdvancedClick = { navController.navigateToAdvancedSettings() }, - onSupportClick = { navController.navigate(Routes.Support) }, - onAboutClick = { navController.navigateToAboutSettings() }, - onDevClick = { navController.navigateToDevSettings() }, - onBackClick = { navController.popBackStack() }, + onGeneralClick = { navigator.navigate(Routes.Settings.General) }, + onSecurityClick = { navigator.navigate(Routes.Settings.Security) }, + onBackupClick = { navigator.navigate(Routes.Settings.BackupSettings) }, + onAdvancedClick = { navigator.navigate(Routes.Settings.Advanced) }, + onSupportClick = { navigator.navigate(Routes.Support) }, + onAboutClick = { navigator.navigate(Routes.Settings.About) }, + onDevClick = { navigator.navigate(Routes.Settings.Dev.Main) }, + onBackClick = { navigator.goBack() }, onCogTap = { haptic.performHapticFeedback(HapticFeedbackType.Confirm) enableDevModeTapCount = enableDevModeTapCount + 1 diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 59a555bb5..d9b677247 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel @@ -42,6 +41,7 @@ import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.components.SearchInput import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -54,7 +54,7 @@ import to.bitkit.ui.utils.getBlockExplorerUrl @Composable fun AddressViewerScreen( - navController: NavController, + navigator: Navigator, viewModel: AddressViewerViewModel = hiltViewModel(), ) { val app = appViewModel ?: return @@ -64,7 +64,7 @@ fun AddressViewerScreen( AddressViewerContent( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onSearchTextChanged = viewModel::updateSearchText, onAddressSelected = { address -> viewModel.selectAddress(address) }, onSwitchAddressType = viewModel::switchAddressType, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt index 9555246b5..c112d3665 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/CoinSelectPreferenceScreen.kt @@ -14,12 +14,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.CoinSelectionPreference import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -38,14 +38,14 @@ object CoinSelectPreferenceTestTags { @Composable fun CoinSelectPreferenceScreen( - navController: NavController, + navigator: Navigator, viewModel: CoinSelectPreferenceViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() Content( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onClickManual = { viewModel.setAutoMode(false) }, onClickAutopilot = { viewModel.setAutoMode(true) }, onClickCoinSelectionPreference = { preference -> viewModel.setCoinSelectionPreference(preference) }, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index cd091065f..fea065cde 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -24,10 +24,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer @@ -42,32 +39,27 @@ import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue -import to.bitkit.ui.navigateToScanner +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable fun ElectrumConfigScreen( - savedStateHandle: SavedStateHandle, - navController: NavController, + navigator: Navigator, + scanResult: String? = null, viewModel: ElectrumConfigViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val app = appViewModel ?: return val context = LocalContext.current - // Handle result from Scanner - LaunchedEffect(savedStateHandle) { - savedStateHandle.getStateFlow(SCAN_RESULT_KEY, null) - .filterNotNull() - .collect { scannedData -> - viewModel.onScan(scannedData) - savedStateHandle.remove(SCAN_RESULT_KEY) - } + // Handle scan result passed via navigation + LaunchedEffect(scanResult) { + scanResult?.let { viewModel.onScan(it) } } // Monitor connection results @@ -96,8 +88,8 @@ fun ElectrumConfigScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, - onScan = { navController.navigateToScanner(isCalledForResult = true) }, + onBack = { navigator.goBack() }, + onScan = { navigator.navigate(Routes.QrScanner) }, onChangeHost = viewModel::setHost, onChangePort = viewModel::setPort, onChangeProtocol = viewModel::setProtocol, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index cf74847db..0aa72b2b6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -21,10 +21,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.ui.appViewModel @@ -35,32 +32,27 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput import to.bitkit.ui.components.VerticalSpacer -import to.bitkit.ui.navigateToScanner +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable fun RgsServerScreen( - savedStateHandle: SavedStateHandle, - navController: NavController, + navigator: Navigator, + scanResult: String? = null, viewModel: RgsServerViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val app = appViewModel ?: return val context = LocalContext.current - // Handle result from Scanner - LaunchedEffect(savedStateHandle) { - savedStateHandle.getStateFlow(SCAN_RESULT_KEY, null) - .filterNotNull() - .collect { scannedData -> - viewModel.onScan(scannedData) - savedStateHandle.remove(SCAN_RESULT_KEY) - } + // Handle scan result passed via navigation + LaunchedEffect(scanResult) { + scanResult?.let { viewModel.onScan(it) } } // Monitor connection results @@ -87,8 +79,8 @@ fun RgsServerScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, - onScan = { navController.navigateToScanner(isCalledForResult = true) }, + onBack = { navigator.goBack() }, + onScan = { navigator.navigate(Routes.QrScanner) }, onChangeUrl = viewModel::setRgsUrl, onClickReset = viewModel::resetToDefault, onClickConnect = viewModel::onClickConnect, diff --git a/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt b/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt index 9fe6a8799..9ba189285 100644 --- a/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/appStatus/AppStatusScreen.kt @@ -26,18 +26,18 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.startActivityAppSettings import to.bitkit.ext.toLocalizedTimestamp import to.bitkit.models.HealthState import to.bitkit.models.NodeLifecycleState import to.bitkit.repositories.AppHealthState -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.HorizontalSpacer import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -50,7 +50,7 @@ import kotlin.time.ExperimentalTime @Composable fun AppStatusScreen( - navController: NavController, + navigator: Navigator, viewModel: AppStatusViewModel = hiltViewModel(), ) { val context = LocalContext.current @@ -58,12 +58,12 @@ fun AppStatusScreen( Content( uiState = uiState, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onInternetClick = { context.startActivityAppSettings() }, - onElectrumClick = { navController.navigate(Routes.ElectrumConfig) }, - onNodeClick = { navController.navigate(Routes.NodeInfo) }, - onChannelsClick = { navController.navigate(Routes.LightningConnections) }, - onBackupClick = { navController.navigate(Routes.BackupSettings) }, + onElectrumClick = { navigator.navigate(Routes.Settings.ElectrumConfig) }, + onNodeClick = { navigator.navigate(Routes.Settings.NodeInfo) }, + onChannelsClick = { navigator.navigate(Routes.Settings.LightningConnections) }, + onBackupClick = { navigator.navigate(Routes.Settings.BackupSettings) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt index 562bbfe62..38da9b71b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt @@ -43,17 +43,13 @@ fun BackgroundPaymentsSettings( val showNotificationDetails by settingsViewModel.showNotificationDetails.collectAsStateWithLifecycle() RequestNotificationPermissions( - onPermissionChange = { granted -> - settingsViewModel.setNotificationPreference(granted) - }, - showPermissionDialog = false + onPermissionChange = settingsViewModel::setNotificationPreference, + showPermissionDialog = false, ) Content( onBack = onBack, - onSystemSettingsClick = { - NotificationUtils.openNotificationSettings(context) - }, + onSystemSettingsClick = { NotificationUtils.openNotificationSettings(context) }, hasPermission = notificationsGranted, showDetails = showNotificationDetails, toggleNotificationDetails = settingsViewModel::toggleNotificationDetails, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt index 4efd1aa55..f4173fcf3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupIntroScreen.kt @@ -56,7 +56,7 @@ fun BackupIntroScreen( modifier = Modifier .fillMaxWidth() .weight(1f) - .testTag("BackupIntroViewImage") + .testTag("BackupIntroViewImage"), ) Display( text = stringResource(R.string.security__backup_title).withAccent(accentColor = Colors.Blue), diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt index e9b83d1fc..e30fec2ce 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ResetAndRestoreScreen.kt @@ -23,13 +23,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton -import to.bitkit.ui.components.Sheet +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon @@ -47,19 +46,18 @@ object ResetAndRestoreTestTags { @Composable fun ResetAndRestoreScreen( - navController: NavController, + navigator: Navigator, ) { - val app = appViewModel ?: return val wallet = walletViewModel ?: return var showDialog by remember { mutableStateOf(false) } Content( showConfirmDialog = showDialog, - onClickBackup = { app.showSheet(Sheet.Backup()) }, + onClickBackup = { navigator.navigate(Routes.Backup.Intro) }, onClickReset = { showDialog = true }, onResetConfirm = { wallet.wipeWallet() }, onResetDismiss = { showDialog = false }, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index c5650ef5f..35b1d0d0b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -47,12 +47,12 @@ import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.MnemonicWordsGrid import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SheetSize +import to.bitkit.ui.nav.MS_TRANSITION_SCREEN import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.ui.utils.withAccent @Composable @@ -97,7 +97,7 @@ private fun ShowMnemonicContent( // Scroll to bottom when mnemonic is revealed LaunchedEffect(showMnemonic) { if (showMnemonic) { - delay(TRANSITION_SCREEN_MS) // Wait for the animation to start + delay(MS_TRANSITION_SCREEN) // Wait for the animation to start scope.launch { scrollState.animateScrollTo(scrollState.maxValue) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt index 623984f03..809b0d200 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/DefaultUnitSettingsScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.PrimaryDisplay @@ -21,6 +20,7 @@ import to.bitkit.ui.components.settings.SectionFooter import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -30,7 +30,7 @@ import to.bitkit.viewmodels.CurrencyViewModel @Composable fun DefaultUnitSettingsScreen( currencyViewModel: CurrencyViewModel, - navController: NavController, + navigator: Navigator, ) { val (_, _, _, selectedCurrency, _, displayUnit, primaryDisplay) = LocalCurrencies.current @@ -40,7 +40,7 @@ fun DefaultUnitSettingsScreen( primaryDisplay = primaryDisplay, onPrimaryUnitClick = currencyViewModel::setPrimaryDisplayUnit, onBitcoinUnitClick = currencyViewModel::setBtcDisplayUnit, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt index cc63bfc56..4338e58ee 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/GeneralSettingsScreen.kt @@ -14,23 +14,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.Language import to.bitkit.models.PrimaryDisplay import to.bitkit.models.TransactionSpeed import to.bitkit.models.transactionSpeedUiText import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.Routes import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue -import to.bitkit.ui.navigateToDefaultUnitSettings -import to.bitkit.ui.navigateToLanguageSettings -import to.bitkit.ui.navigateToLocalCurrencySettings -import to.bitkit.ui.navigateToQuickPaySettings -import to.bitkit.ui.navigateToTagsSettings -import to.bitkit.ui.navigateToTransactionSpeedSettings -import to.bitkit.ui.navigateToWidgetsSettings +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +33,7 @@ import to.bitkit.viewmodels.LanguageViewModel @Composable fun GeneralSettingsScreen( - navController: NavController, + navigator: Navigator, languageViewModel: LanguageViewModel = hiltViewModel(), ) { val settings = settingsViewModel ?: return @@ -59,19 +52,22 @@ fun GeneralSettingsScreen( primaryDisplay = currencies.primaryDisplay, defaultTransactionSpeed = defaultTransactionSpeed, showTagsButton = lastUsedTags.isNotEmpty(), - onBackClick = { navController.popBackStack() }, - onLocalCurrencyClick = { navController.navigateToLocalCurrencySettings() }, - onDefaultUnitClick = { navController.navigateToDefaultUnitSettings() }, - onTransactionSpeedClick = { navController.navigateToTransactionSpeedSettings() }, - onWidgetsClick = { navController.navigateToWidgetsSettings() }, - onQuickPayClick = { navController.navigateToQuickPaySettings(quickPayIntroSeen) }, - onTagsClick = { navController.navigateToTagsSettings() }, - onLanguageSettingsClick = { navController.navigateToLanguageSettings() }, + onBackClick = { navigator.goBack() }, + onLocalCurrencyClick = { navigator.navigate(Routes.Settings.LocalCurrency) }, + onDefaultUnitClick = { navigator.navigate(Routes.Settings.DefaultUnit) }, + onTransactionSpeedClick = { navigator.navigate(Routes.Settings.TransactionSpeed) }, + onWidgetsClick = { navigator.navigate(Routes.Settings.Widgets) }, + onQuickPayClick = { + val route = if (quickPayIntroSeen) Routes.QuickPay.Settings else Routes.QuickPay.Intro + navigator.navigate(route) + }, + onTagsClick = { navigator.navigate(Routes.Settings.Tags) }, + onLanguageSettingsClick = { navigator.navigate(Routes.Settings.Language) }, onBgPaymentsClick = { if (bgPaymentsIntroSeen || notificationsGranted) { - navController.navigate(Routes.BackgroundPaymentsSettings) + navigator.navigate(Routes.BackgroundPayments.Settings) } else { - navController.navigate(Routes.BackgroundPaymentsIntro) + navigator.navigate(Routes.BackgroundPayments.Intro) } }, selectedLanguage = languageUiState.selectedLanguage.displayName, diff --git a/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt index 6f887099d..91fdc576e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/LocalCurrencySettingsScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.FxRate import to.bitkit.ui.LocalCurrencies @@ -24,6 +23,7 @@ import to.bitkit.ui.components.SearchInput import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -34,7 +34,7 @@ import to.bitkit.viewmodels.CurrencyViewModel @Composable fun LocalCurrencySettingsScreen( currencyViewModel: CurrencyViewModel, - navController: NavController, + navigator: Navigator, ) { val (rates, _, _, selectedCurrency) = LocalCurrencies.current var searchText by remember { mutableStateOf("") } @@ -74,7 +74,7 @@ fun LocalCurrencySettingsScreen( otherCurrencies = otherCurrencies, selectedCurrency = selectedCurrency, onCurrencyClick = { currencyViewModel.setSelectedCurrency(it) }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt index 2006e8f39..274761b31 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/TagsSettingsScreen.kt @@ -13,10 +13,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.settings.SectionHeader +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -25,7 +25,7 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun TagsSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return @@ -36,10 +36,10 @@ fun TagsSettingsScreen( onClickTag = { tag -> settings.deleteLastUsedTag(tag) if (tags.size == 1) { - navController.popBackStack() + navigator.goBack() } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt index 9a860baa4..c086cf562 100644 --- a/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/general/WidgetsSettingsScreen.kt @@ -9,9 +9,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ui.components.settings.SettingsSwitchRow +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -20,7 +20,7 @@ import to.bitkit.ui.theme.AppThemeSurface @Composable fun WidgetsSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return @@ -28,7 +28,7 @@ fun WidgetsSettingsScreen( val showWidgetTitles by settings.showWidgetTitles.collectAsStateWithLifecycle() WidgetsSettingsContent( - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, showWidgets = showWidgets, showWidgetTitles = showWidgetTitles, onShowWidgetsClick = { settings.setShowWidgets(!showWidgets) }, diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 3081689de..82ece9f3e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import com.synonym.bitkitcore.BtBolt11InvoiceState import com.synonym.bitkitcore.BtOpenChannelState import com.synonym.bitkitcore.BtOrderState @@ -56,7 +55,6 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText import to.bitkit.models.Toast -import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB @@ -67,6 +65,8 @@ import to.bitkit.ui.components.MoneyCaptionB import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -83,7 +83,7 @@ import java.util.Locale @Composable fun ChannelDetailScreen( - navController: NavController, + navigator: Navigator, viewModel: LightningConnectionsViewModel, ) { val context = LocalContext.current @@ -123,7 +123,7 @@ fun ChannelDetailScreen( txTime = txTime, isRefreshing = uiState.isRefreshing, isClosedChannel = isClosedChannel, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onRefresh = { viewModel.onPullToRefresh() }, @@ -141,7 +141,7 @@ fun ChannelDetailScreen( context.startActivity(intent) }, onSupport = { order -> contactSupport(order, channel, walletState.nodeId, context) }, - onCloseConnection = { navController.navigate(Routes.CloseConnection) }, + onCloseConnection = { navigator.navigate(Routes.Settings.CloseConnection) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt index f31de0ea5..2a16c1758 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/CloseConnectionScreen.kt @@ -22,13 +22,13 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.VerticalSpacer +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -38,7 +38,7 @@ import to.bitkit.ui.utils.withAccentBoldBright @Composable fun CloseConnectionScreen( - navController: NavController, + navigator: Navigator, viewModel: LightningConnectionsViewModel, ) { val uiState by viewModel.closeConnectionUiState.collectAsState() @@ -51,13 +51,13 @@ fun CloseConnectionScreen( // Handle success navigation LaunchedEffect(uiState.closeSuccess) { if (uiState.closeSuccess) { - navController.popBackStack(inclusive = false) + navigator.navigate(Routes.Settings.LightningConnections) } } Content( isLoading = uiState.isLoading, - onBack = { navController.popBackStack() }, + onBack = { navigator.goBack() }, onClickClose = { viewModel.closeChannel() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index d409a81b3..d2bc47b22 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -39,13 +39,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.models.formatToModernDisplay -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.ChannelStatusUi @@ -57,7 +54,8 @@ import to.bitkit.ui.components.SyncNodeView import to.bitkit.ui.components.TertiaryButton import to.bitkit.ui.components.Title import to.bitkit.ui.components.VerticalSpacer -import to.bitkit.ui.navigateToTransferFunding +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha @@ -66,7 +64,6 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors private const val CLOSED_CHANNEL_ALPHA = 0.64f -private const val CHANNEL_SELECTION_DELAY_MS = 200L object LightningConnectionsTestTags { const val SCREEN = "lightning_connections_screen" @@ -78,7 +75,7 @@ object LightningConnectionsTestTags { @Composable fun LightningConnectionsScreen( - navController: NavController, + navigator: Navigator, viewModel: LightningConnectionsViewModel, ) { val context = LocalContext.current @@ -90,30 +87,16 @@ fun LightningConnectionsScreen( viewModel.clearTransactionDetails() } - LaunchedEffect(navController.currentBackStackEntry) { - val selectedChannelId = navController.previousBackStackEntry?.savedStateHandle?.get("selectedChannelId") - if (selectedChannelId == null) return@LaunchedEffect - - navController.previousBackStackEntry?.savedStateHandle?.remove("selectedChannelId") - delay(CHANNEL_SELECTION_DELAY_MS) - if (viewModel.findAndSelectChannel(selectedChannelId)) { - navController.navigate(Routes.ChannelDetail) { - launchSingleTop = true - popUpTo(Routes.ConnectionsNav) { inclusive = false } - } - } - } - Content( uiState = uiState, - onBack = { navController.popBackStack() }, - onClickAddConnection = { navController.navigateToTransferFunding() }, + onBack = { navigator.goBack() }, + onClickAddConnection = { navigator.navigate(Routes.Transfer.Funding) }, onClickExportLogs = { viewModel.zipLogsForSharing { uri -> context.shareZipFile(uri) } }, onClickChannel = { channelUi -> viewModel.setSelectedChannel(channelUi) - navController.navigate(Routes.ChannelDetail) + navigator.navigate(Routes.Settings.ChannelDetail) }, onRefresh = { viewModel.onPullToRefresh() diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt index 99934e17f..9a9dcc7ba 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinConfirmScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.env.Env @@ -30,7 +29,8 @@ import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PinDots -import to.bitkit.ui.navigateToChangePinResult +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +40,7 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinConfirmScreen( newPin: String, - navController: NavController, + navigator: Navigator, ) { val app = appViewModel ?: return var pin by remember { mutableStateOf("") } @@ -50,7 +50,7 @@ fun ChangePinConfirmScreen( if (pin.length == Env.PIN_LENGTH) { if (pin == newPin) { app.editPin(newPin) - navController.navigateToChangePinResult() + navigator.navigate(Routes.Pin.Change.Result) } else { showError = true delay(500) @@ -71,7 +71,7 @@ fun ChangePinConfirmScreen( pin += key } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt index 79e9620d9..31b0bf4e0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinNewScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.components.BodyM @@ -24,7 +23,8 @@ import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PinDots -import to.bitkit.ui.navigateToChangePinConfirm +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -33,13 +33,13 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinNewScreen( - navController: NavController, + navigator: Navigator, ) { var pin by remember { mutableStateOf("") } LaunchedEffect(pin) { if (pin.length == Env.PIN_LENGTH) { - navController.navigateToChangePinConfirm(pin) + navigator.navigate(Routes.Pin.Change.Confirm(pin)) } } @@ -54,7 +54,7 @@ fun ChangePinNewScreen( pin += key } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt index 8913265f2..120ec684c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinResultScreen.kt @@ -16,11 +16,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface @@ -28,14 +28,14 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinResultScreen( - navController: NavController, + navigator: Navigator, ) { ChangePinResultContent( onOkClick = { - navController.popBackStack(inclusive = false) + navigator.navigate(Routes.Settings.Security) }, onBackClick = { - navController.popBackStack() + navigator.goBack() } ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt index 85c727ed0..719838cb6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.appViewModel @@ -29,7 +28,8 @@ import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PinDots -import to.bitkit.ui.navigateToChangePinNew +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -39,7 +39,7 @@ import to.bitkit.ui.theme.Colors @Composable fun ChangePinScreen( - navController: NavController, + navigator: Navigator, ) { val app = appViewModel ?: return val attemptsRemaining by app.pinAttemptsRemaining.collectAsStateWithLifecycle() @@ -48,7 +48,7 @@ fun ChangePinScreen( LaunchedEffect(pin) { if (pin.length == Env.PIN_LENGTH) { if (app.validatePin(pin)) { - navController.navigateToChangePinNew() + navigator.navigate(Routes.Pin.Change.New) } else { pin = "" } @@ -67,7 +67,7 @@ fun ChangePinScreen( pin += key } }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, onClickForgotPin = { app.setShowForgotPin(true) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt index 0999d011d..4c22b855a 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt @@ -16,14 +16,12 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.navOptions import to.bitkit.R -import to.bitkit.ui.Routes import to.bitkit.ui.components.AuthCheckAction import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton -import to.bitkit.ui.navigateToAuthCheck +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -32,17 +30,18 @@ import to.bitkit.ui.theme.Colors @Composable fun DisablePinScreen( - navController: NavController, + navigator: Navigator, ) { DisablePinContent( onDisableClick = { - navController.navigateToAuthCheck( - requirePin = true, - onSuccessActionId = AuthCheckAction.DISABLE_PIN, - navOptions = navOptions { popUpTo(Routes.SecuritySettings) }, + navigator.navigate( + Routes.AuthCheck( + requirePin = true, + onSuccessActionId = AuthCheckAction.DISABLE_PIN, + ) ) }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt b/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt index 488c6a56a..c7bbe59bc 100644 --- a/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt @@ -16,33 +16,49 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.core.net.toUri -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.Env -import to.bitkit.ui.Routes import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.settings.Links import to.bitkit.ui.components.settings.SettingsButtonRow +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +/** + * Support screen composable. + */ @Composable fun SupportScreen( - navController: NavController, + onBack: () -> Unit, + onClickHelpCenter: () -> Unit, +) { + Content( + onBack = onBack, + onClickReportIssue = {}, + onClickHelpCenter = onClickHelpCenter, + onClickAppStatus = {}, + ) +} + +@Composable +fun SupportScreen( + navigator: Navigator, ) { val context = LocalContext.current Content( - onBack = { navController.popBackStack() }, - onClickReportIssue = { navController.navigate(Routes.ReportIssue) }, + onBack = { navigator.goBack() }, + onClickReportIssue = { navigator.navigate(Routes.ReportIssue.Form) }, onClickHelpCenter = { val intent = Intent(Intent.ACTION_VIEW, Env.BITKIT_HELP_CENTER.toUri()) context.startActivity(intent) }, - onClickAppStatus = { navController.navigate(Routes.AppStatus) }, + onClickAppStatus = { navigator.navigate(Routes.AppStatus) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt index 01ea002d1..8c24bb357 100644 --- a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/CustomFeeSettingsScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.env.TransactionDefaults import to.bitkit.models.BITCOIN_SYMBOL @@ -31,6 +30,7 @@ import to.bitkit.ui.components.NumberPad import to.bitkit.ui.components.NumberPadType import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.currencyViewModel +import to.bitkit.ui.nav.Navigator import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -40,7 +40,7 @@ import to.bitkit.ui.theme.Colors @Composable fun CustomFeeSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return val customFeeRate = settings.defaultTransactionSpeed.collectAsStateWithLifecycle() @@ -83,9 +83,9 @@ fun CustomFeeSettingsScreen( onContinue = { val feeRate = input.toUIntOrNull() ?: 0u settings.setDefaultTransactionSpeed(TransactionSpeed.Custom(feeRate)) - navController.popBackStack() + navigator.goBack() }, - onBackClick = { navController.popBackStack() }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt index 35db5e680..38083b426 100644 --- a/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/transactionSpeed/TransactionSpeedSettingsScreen.kt @@ -11,13 +11,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.TransactionSpeed import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsButtonValue -import to.bitkit.ui.navigateToCustomFeeSettings +import to.bitkit.ui.nav.Navigator +import to.bitkit.ui.nav.Routes import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn @@ -27,7 +27,7 @@ import to.bitkit.ui.theme.Colors @Composable fun TransactionSpeedSettingsScreen( - navController: NavController, + navigator: Navigator, ) { val settings = settingsViewModel ?: return val defaultTransactionSpeed = settings.defaultTransactionSpeed.collectAsStateWithLifecycle() @@ -36,10 +36,10 @@ fun TransactionSpeedSettingsScreen( selectedSpeed = defaultTransactionSpeed.value, onSpeedSelected = { settings.setDefaultTransactionSpeed(it) - navController.popBackStack() + navigator.goBack() }, - onCustomFeeClick = { navController.navigateToCustomFeeSettings() }, - onBackClick = { navController.popBackStack() }, + onCustomFeeClick = { navigator.navigate(Routes.Settings.CustomFee) }, + onBackClick = { navigator.goBack() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt deleted file mode 100644 index 0ccaf924b..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt +++ /dev/null @@ -1,180 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable -import to.bitkit.ui.LocalBalances -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.components.SheetSize -import to.bitkit.ui.settings.backups.BackupContract -import to.bitkit.ui.settings.backups.BackupIntroScreen -import to.bitkit.ui.settings.backups.BackupNavSheetViewModel -import to.bitkit.ui.settings.backups.ConfirmMnemonicScreen -import to.bitkit.ui.settings.backups.ConfirmPassphraseScreen -import to.bitkit.ui.settings.backups.MetadataScreen -import to.bitkit.ui.settings.backups.MultipleDevicesScreen -import to.bitkit.ui.settings.backups.ShowMnemonicScreen -import to.bitkit.ui.settings.backups.ShowPassphraseScreen -import to.bitkit.ui.settings.backups.SuccessScreen -import to.bitkit.ui.settings.backups.WarningScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.composableWithDefaultTransitions - -@Composable -fun BackupSheet( - sheet: Sheet.Backup, - onDismiss: () -> Unit, - viewModel: BackupNavSheetViewModel = hiltViewModel(), -) { - val navController = rememberNavController() - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val currentOnDismiss by rememberUpdatedState(onDismiss) - - LaunchedEffect(Unit) { - viewModel.loadMnemonicData() - } - - DisposableEffect(Unit) { - onDispose { - viewModel.resetState() - } - } - - LaunchedEffect(Unit) { - viewModel.effects.collect { effect -> - when (effect) { - BackupContract.SideEffect.NavigateToShowPassphrase -> navController.navigate(BackupRoute.ShowPassphrase) - BackupContract.SideEffect.NavigateToConfirmMnemonic -> navController.navigate( - BackupRoute.ConfirmMnemonic - ) - - BackupContract.SideEffect.NavigateToConfirmPassphrase -> navController.navigate( - BackupRoute.ConfirmPassphrase - ) - - BackupContract.SideEffect.NavigateToWarning -> navController.navigate(BackupRoute.Warning) - BackupContract.SideEffect.NavigateToSuccess -> navController.navigate(BackupRoute.Success) - BackupContract.SideEffect.NavigateToMultipleDevices -> navController.navigate( - BackupRoute.MultipleDevices - ) - - BackupContract.SideEffect.NavigateToMetadata -> navController.navigate(BackupRoute.Metadata) - BackupContract.SideEffect.DismissSheet -> currentOnDismiss() - } - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight(SheetSize.MEDIUM) - .testTag("backup_navigation_sheet") - ) { - NavHost( - navController = navController, - startDestination = sheet.route, - ) { - composableWithDefaultTransitions { - BackupIntroScreen( - hasFunds = LocalBalances.current.totalSats > 0u, - onClose = currentOnDismiss, - onConfirm = { navController.navigate(BackupRoute.ShowMnemonic) }, - ) - } - composableWithDefaultTransitions { - ShowMnemonicScreen( - uiState = uiState, - onRevealClick = viewModel::onRevealMnemonic, - onContinueClick = viewModel::onShowMnemonicContinue, - ) - } - composableWithDefaultTransitions { - ShowPassphraseScreen( - uiState = uiState, - onContinue = viewModel::onShowPassphraseContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - ConfirmMnemonicScreen( - uiState = uiState, - onContinue = viewModel::onConfirmMnemonicContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - ConfirmPassphraseScreen( - uiState = uiState, - onPassphraseChange = viewModel::onPassphraseInput, - onContinue = viewModel::onConfirmPassphraseContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - WarningScreen( - onContinue = viewModel::onWarningContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - SuccessScreen( - onContinue = viewModel::onSuccessContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - MultipleDevicesScreen( - onContinue = viewModel::onMultipleDevicesContinue, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - MetadataScreen( - uiState = uiState, - onDismiss = viewModel::onMetadataClose, - onBack = { navController.popBackStack() }, - ) - } - } - } -} - -sealed interface BackupRoute { - @Serializable - data object Intro : BackupRoute - - @Serializable - data object ShowMnemonic : BackupRoute - - @Serializable - data object ShowPassphrase : BackupRoute - - @Serializable - data object ConfirmMnemonic : BackupRoute - - @Serializable - data object ConfirmPassphrase : BackupRoute - - @Serializable - data object Warning : BackupRoute - - @Serializable - data object Success : BackupRoute - - @Serializable - data object MultipleDevices : BackupRoute - - @Serializable - data object Metadata : BackupRoute -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt index 0800b4de2..ee7568b5d 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/ForceTransferSheet.kt @@ -39,7 +39,7 @@ fun ForceTransferSheet( transferViewModel: TransferViewModel, ) { val isLoading by transferViewModel.isForceTransferLoading.collectAsStateWithLifecycle() - Content( + ForceTransferContent( isLoading = isLoading, onForceTransfer = { transferViewModel.forceTransfer { @@ -51,7 +51,7 @@ fun ForceTransferSheet( } @Composable -private fun Content( +internal fun ForceTransferContent( modifier: Modifier = Modifier, isLoading: Boolean = false, onForceTransfer: () -> Unit = {}, @@ -118,7 +118,7 @@ private fun Content( private fun Preview() { AppThemeSurface { BottomSheetPreview { - Content() + ForceTransferContent() } } } @@ -128,7 +128,7 @@ private fun Preview() { private fun PreviewLoading() { AppThemeSurface { BottomSheetPreview { - Content(isLoading = true) + ForceTransferContent(isLoading = true) } } } diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt index 23ce441e0..d8f4132ff 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftLoading.kt @@ -33,6 +33,8 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors +internal const val IMAGE_WIDTH_FRACTION = 0.8f + @Composable fun GiftLoading( viewModel: GiftViewModel, diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftRoute.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftRoute.kt deleted file mode 100644 index 060bb8fbb..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftRoute.kt +++ /dev/null @@ -1,23 +0,0 @@ -package to.bitkit.ui.sheets - -import kotlinx.serialization.Serializable - -internal const val IMAGE_WIDTH_FRACTION = 0.8f - -@Serializable -sealed interface GiftRoute { - @Serializable - data object Loading : GiftRoute - - @Serializable - data object Used : GiftRoute - - @Serializable - data object UsedUp : GiftRoute - - @Serializable - data object Error : GiftRoute - - @Serializable - data object Success : GiftRoute -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt deleted file mode 100644 index 8d286dfbc..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftSheet.kt +++ /dev/null @@ -1,98 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import to.bitkit.R -import to.bitkit.models.NewTransactionSheetDetails -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.viewmodels.AppViewModel - -@Composable -fun GiftSheet( - sheet: Sheet.Gift, - appViewModel: AppViewModel, - modifier: Modifier = Modifier, - viewModel: GiftViewModel = hiltViewModel(), -) { - val navController = rememberNavController() - - LaunchedEffect(sheet.code, sheet.amount) { - viewModel.initialize(sheet.code, sheet.amount) - } - - val onSuccessState = rememberUpdatedState { details: NewTransactionSheetDetails -> - appViewModel.hideSheet() - appViewModel.showTransactionSheet(details) - } - - LaunchedEffect(Unit) { - viewModel.successEvent.collect { details -> - onSuccessState.value.invoke(details) - } - } - - LaunchedEffect(Unit) { - viewModel.navigationEvent.collect { route -> - when (route) { - is GiftRoute.Success -> appViewModel.hideSheet() - else -> navController.navigate(route) { - popUpTo(GiftRoute.Loading) { inclusive = false } - } - } - } - } - - Column( - modifier = modifier - .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("GiftSheet") - ) { - NavHost( - navController = navController, - startDestination = GiftRoute.Loading, - ) { - composableWithDefaultTransitions { - GiftLoading( - viewModel = viewModel, - ) - } - composableWithDefaultTransitions { - GiftErrorSheet( - titleRes = R.string.other__gift__used__title, - textRes = R.string.other__gift__used__text, - testTag = "GiftUsed", - onDismiss = { appViewModel.hideSheet() }, - ) - } - composableWithDefaultTransitions { - GiftErrorSheet( - titleRes = R.string.other__gift__used_up__title, - textRes = R.string.other__gift__used_up__text, - testTag = "GiftUsedUp", - onDismiss = { appViewModel.hideSheet() }, - ) - } - composableWithDefaultTransitions { - GiftErrorSheet( - titleRes = R.string.other__gift__error__title, - textRes = R.string.other__gift__error__text, - testTag = "GiftError", - onDismiss = { appViewModel.hideSheet() }, - ) - } - } - } -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt index d7a1bc8e4..ac9117be7 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/GiftViewModel.kt @@ -21,6 +21,7 @@ import to.bitkit.models.NewTransactionSheetType import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.GiftClaimResult +import to.bitkit.ui.nav.Routes import to.bitkit.utils.Logger import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -32,7 +33,7 @@ class GiftViewModel @Inject constructor( private val activityRepo: ActivityRepo, ) : ViewModel() { - private val _navigationEvent = MutableSharedFlow(extraBufferCapacity = 1) + private val _navigationEvent = MutableSharedFlow(extraBufferCapacity = 1) val navigationEvent = _navigationEvent.asSharedFlow() private val _successEvent = MutableSharedFlow(extraBufferCapacity = 1) @@ -46,11 +47,6 @@ class GiftViewModel @Inject constructor( private var isClaiming: Boolean = false fun initialize(code: String, amount: ULong) { - if (!isClaiming) { - viewModelScope.launch { - _navigationEvent.emit(GiftRoute.Loading) - } - } this.code = code this.amount = amount viewModelScope.launch(bgDispatcher) { @@ -71,7 +67,7 @@ class GiftViewModel @Inject constructor( onSuccess = { result -> when (result) { is GiftClaimResult.SuccessWithLiquidity -> { - _navigationEvent.emit(GiftRoute.Success) + _navigationEvent.emit(Routes.Gift.Success) } is GiftClaimResult.SuccessWithoutLiquidity -> { insertGiftActivity(result) @@ -83,7 +79,7 @@ class GiftViewModel @Inject constructor( sats = result.sats, ) ) - _navigationEvent.emit(GiftRoute.Success) + _navigationEvent.emit(Routes.Gift.Success) } } }, @@ -116,9 +112,9 @@ class GiftViewModel @Inject constructor( Logger.error("Gift claim failed: $error", error, context = TAG) val route = when { - errorContains(error, "GIFT_CODE_ALREADY_USED") -> GiftRoute.Used - errorContains(error, "GIFT_CODE_USED_UP") -> GiftRoute.UsedUp - else -> GiftRoute.Error + errorContains(error, "GIFT_CODE_ALREADY_USED") -> Routes.Gift.Used + errorContains(error, "GIFT_CODE_USED_UP") -> Routes.Gift.UsedUp + else -> Routes.Gift.Error } _navigationEvent.emit(route) diff --git a/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt index 36c951e96..f9d5a9be4 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/LnurlAuthSheet.kt @@ -20,7 +20,6 @@ import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.SheetSize import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar @@ -28,28 +27,9 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.viewmodels.AppViewModel @Composable -fun LnurlAuthSheet( - sheet: Sheet.LnurlAuth, - app: AppViewModel, -) { - Content( - domain = sheet.domain, - onContinue = { - app.requestLnurlAuth( - callback = sheet.lnurl, - k1 = sheet.k1, - domain = sheet.domain, - ) - }, - onCancel = { app.hideSheet() }, - ) -} - -@Composable -private fun Content( +internal fun LnurlAuthContent( domain: String, modifier: Modifier = Modifier, onCancel: () -> Unit = {}, @@ -112,7 +92,7 @@ private fun Content( private fun Preview() { AppThemeSurface { BottomSheetPreview { - Content( + LnurlAuthContent( domain = "LNMarkets.com", ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt deleted file mode 100644 index dd51453ee..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/PinSheet.kt +++ /dev/null @@ -1,96 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import kotlinx.serialization.Serializable -import to.bitkit.ui.components.Sheet -import to.bitkit.ui.components.SheetSize -import to.bitkit.ui.settings.pin.PinBiometricsScreen -import to.bitkit.ui.settings.pin.PinChooseScreen -import to.bitkit.ui.settings.pin.PinConfirmScreen -import to.bitkit.ui.settings.pin.PinPromptScreen -import to.bitkit.ui.settings.pin.PinResultScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.viewmodels.AppViewModel - -@Composable -fun PinSheet( - sheet: Sheet.Pin, - app: AppViewModel, -) { - val navController = rememberNavController() - val onDismiss = app::hideSheet - - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight(SheetSize.MEDIUM) - ) { - NavHost( - navController = navController, - startDestination = sheet.route, - ) { - composableWithDefaultTransitions { - PinPromptScreen( - showLaterButton = it.toRoute().showLaterButton, - onContinue = { navController.navigate(PinRoute.Choose) }, - onLater = onDismiss, - ) - } - composableWithDefaultTransitions { - PinChooseScreen( - onPinChosen = { pin -> - navController.navigate(PinRoute.Confirm(pin)) - }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - PinConfirmScreen( - originalPin = it.toRoute().pin, - onPinConfirmed = { navController.navigate(PinRoute.Biometrics) }, - onBack = { navController.popBackStack() }, - ) - } - composableWithDefaultTransitions { - PinBiometricsScreen( - onContinue = { isBioOn -> - navController.navigate(PinRoute.Result(isBioOn)) - }, - onSkip = { navController.navigate(PinRoute.Result(isBioOn = false)) }, - onBack = onDismiss, - ) - } - composableWithDefaultTransitions { - PinResultScreen( - isBioOn = it.toRoute().isBioOn, - onDismiss = onDismiss, - onBack = onDismiss, - ) - } - } - } -} - -sealed interface PinRoute { - @Serializable - data class Prompt(val showLaterButton: Boolean = false) : PinRoute - - @Serializable - data object Choose : PinRoute - - @Serializable - data class Confirm(val pin: String) : PinRoute - - @Serializable - data object Biometrics : PinRoute - - @Serializable - data class Result(val isBioOn: Boolean) : PinRoute -} diff --git a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt deleted file mode 100644 index 5c4f49e59..000000000 --- a/app/src/main/java/to/bitkit/ui/sheets/SendSheet.kt +++ /dev/null @@ -1,342 +0,0 @@ -package to.bitkit.ui.sheets - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute -import kotlinx.serialization.Serializable -import to.bitkit.models.NewTransactionSheetDetails -import to.bitkit.models.NewTransactionSheetDirection -import to.bitkit.models.NewTransactionSheetType -import to.bitkit.ui.screens.scanner.QrScanningScreen -import to.bitkit.ui.screens.wallets.send.AddTagScreen -import to.bitkit.ui.screens.wallets.send.PIN_CHECK_RESULT_KEY -import to.bitkit.ui.screens.wallets.send.SendAddressScreen -import to.bitkit.ui.screens.wallets.send.SendAmountScreen -import to.bitkit.ui.screens.wallets.send.SendCoinSelectionScreen -import to.bitkit.ui.screens.wallets.send.SendConfirmScreen -import to.bitkit.ui.screens.wallets.send.SendErrorScreen -import to.bitkit.ui.screens.wallets.send.SendFeeCustomScreen -import to.bitkit.ui.screens.wallets.send.SendFeeRateScreen -import to.bitkit.ui.screens.wallets.send.SendFeeViewModel -import to.bitkit.ui.screens.wallets.send.SendPinCheckScreen -import to.bitkit.ui.screens.wallets.send.SendQuickPayScreen -import to.bitkit.ui.screens.wallets.send.SendRecipientScreen -import to.bitkit.ui.screens.wallets.withdraw.WithdrawConfirmScreen -import to.bitkit.ui.screens.wallets.withdraw.WithdrawErrorScreen -import to.bitkit.ui.settings.support.SupportScreen -import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.util.gradientBackground -import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.ui.utils.navigationWithDefaultTransitions -import to.bitkit.viewmodels.AppViewModel -import to.bitkit.viewmodels.SendEffect -import to.bitkit.viewmodels.SendEvent -import to.bitkit.viewmodels.WalletViewModel - -@Composable -fun SendSheet( - appViewModel: AppViewModel, - walletViewModel: WalletViewModel, - startDestination: SendRoute = SendRoute.Recipient, -) { - LaunchedEffect(startDestination) { - // always reset state on new user-initiated send - if (startDestination == SendRoute.Recipient) { - appViewModel.resetSendState() - appViewModel.resetQuickPayData() - } - } - Column( - modifier = Modifier - .fillMaxWidth() - .sheetHeight() - .imePadding() - .testTag("SendSheet") - ) { - val navController = rememberNavController() - LaunchedEffect(appViewModel, navController) { - appViewModel.sendEffect.collect { - when (it) { - is SendEffect.NavigateToAmount -> navController.navigate(SendRoute.Amount) - is SendEffect.NavigateToAddress -> navController.navigate(SendRoute.Address) - is SendEffect.NavigateToScan -> navController.navigate(SendRoute.QrScanner) - is SendEffect.NavigateToCoinSelection -> navController.navigate(SendRoute.CoinSelection) - is SendEffect.NavigateToConfirm -> navController.navigate(SendRoute.Confirm) - is SendEffect.PopBack -> navController.popBackStack(it.route, inclusive = false) - is SendEffect.PaymentSuccess -> { - appViewModel.clearClipboardForAutoRead() - navController.navigate(SendRoute.Success) { - popUpTo(startDestination) { inclusive = true } - } - } - - is SendEffect.NavigateToQuickPay -> navController.navigate(SendRoute.QuickPay) - is SendEffect.NavigateToWithdrawConfirm -> navController.navigate(SendRoute.WithdrawConfirm) - is SendEffect.NavigateToWithdrawError -> navController.navigate(SendRoute.WithdrawError) - is SendEffect.NavigateToFee -> navController.navigate(SendRoute.FeeRate) - is SendEffect.NavigateToFeeCustom -> navController.navigate(SendRoute.FeeCustom) - } - } - } - - NavHost( - navController = navController, - startDestination = startDestination, - ) { - composableWithDefaultTransitions { - SendRecipientScreen( - onEvent = { appViewModel.setSendEvent(it) } - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - SendAddressScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onEvent = { appViewModel.setSendEvent(it) }, - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() - SendAmountScreen( - uiState = uiState, - walletUiState = walletUiState, - canGoBack = startDestination != SendRoute.Amount, - onBack = { - if (!navController.popBackStack()) { - appViewModel.hideSheet() - } - }, - onEvent = { appViewModel.setSendEvent(it) } - ) - } - composableWithDefaultTransitions { - QrScanningScreen( - navController = navController, - inSheet = true, - ) { qrCode -> - navController.popBackStack() - appViewModel.onScanResult(data = qrCode) - } - } - composableWithDefaultTransitions { - val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - SendCoinSelectionScreen( - requiredAmount = sendUiState.amount, - address = sendUiState.address, - onBack = { navController.popBackStack() }, - onContinue = { utxos -> appViewModel.setSendEvent(SendEvent.CoinSelectionContinue(utxos)) }, - ) - } - navigationWithDefaultTransitions( - startDestination = SendRoute.FeeRate, - ) { - composableWithDefaultTransitions { - val sendUiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } - SendFeeRateScreen( - sendUiState = sendUiState, - viewModel = hiltViewModel(parentEntry), - onBack = { navController.popBackStack() }, - onContinue = { navController.popBackStack() }, - onSelect = { speed -> appViewModel.onSelectSpeed(speed) }, - ) - } - composableWithDefaultTransitions { - val parentEntry = remember(it) { navController.getBackStackEntry(SendRoute.FeeNav) } - SendFeeCustomScreen( - viewModel = hiltViewModel(parentEntry), - onBack = { navController.popBackStack() }, - onContinue = { speed -> appViewModel.setTransactionSpeed(speed) }, - ) - } - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - val walletUiState by walletViewModel.uiState.collectAsStateWithLifecycle() - - SendConfirmScreen( - savedStateHandle = it.savedStateHandle, - uiState = uiState, - isNodeRunning = walletUiState.nodeLifecycleState.isRunning(), - canGoBack = startDestination != SendRoute.Confirm, - onBack = { - if (!navController.popBackStack()) { - appViewModel.hideSheet() - } - }, - onEvent = { e -> appViewModel.setSendEvent(e) }, - onClickAddTag = { navController.navigate(SendRoute.AddTag) }, - onClickTag = { tag -> appViewModel.removeTag(tag) }, - onNavigateToPin = { navController.navigate(SendRoute.PinCheck) }, - ) - } - composableWithDefaultTransitions { - val sendDetail by appViewModel.successSendUiState.collectAsStateWithLifecycle() - NewTransactionSheetView( - details = sendDetail, - onCloseClick = { appViewModel.hideSheet() }, - onDetailClick = { appViewModel.onClickSendDetail() }, - modifier = Modifier - .fillMaxSize() - .gradientBackground() - .navigationBarsPadding() - .testTag("SendSuccess") - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithdrawConfirmScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onConfirm = { appViewModel.onConfirmWithdraw() }, - ) - } - composableWithDefaultTransitions { - val uiState by appViewModel.sendUiState.collectAsStateWithLifecycle() - WithdrawErrorScreen( - uiState = uiState, - onBack = { navController.popBackStack() }, - onClickScan = { navController.navigate(SendRoute.QrScanner) }, - onClickSupport = { navController.navigate(SendRoute.Support) }, - ) - } - // TODO navigate to main support screen, not inside SEND sheet - composableWithDefaultTransitions { - SupportScreen(navController) - } - composableWithDefaultTransitions { - AddTagScreen( - onBack = { navController.popBackStack() }, - onTagSelected = { tag -> - appViewModel.addTagToSelected(tag) - navController.popBackStack() - }, - tqgInputTestTag = "TagInputSend", - addButtonTestTag = "SendTagsSubmit", - ) - } - composableWithDefaultTransitions { - SendPinCheckScreen( - onBack = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(PIN_CHECK_RESULT_KEY, false) - navController.popBackStack() - }, - onSuccess = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(PIN_CHECK_RESULT_KEY, true) - navController.popBackStack() - appViewModel.setSendEvent(SendEvent.PayConfirmed) - }, - ) - } - composableWithDefaultTransitions { - val quickPayData by appViewModel.quickPayData.collectAsStateWithLifecycle() - SendQuickPayScreen( - quickPayData = requireNotNull(quickPayData), - onPaymentComplete = { paymentHash, amountWithFee -> - appViewModel.handlePaymentSuccess( - NewTransactionSheetDetails( - type = NewTransactionSheetType.LIGHTNING, - direction = NewTransactionSheetDirection.SENT, - paymentHashOrTxId = paymentHash, - sats = amountWithFee, - ), - ) - }, - onShowError = { errorMessage -> - navController.navigate(SendRoute.Error(errorMessage)) - } - ) - } - composableWithDefaultTransitions { - val route = it.toRoute() - SendErrorScreen( - errorMessage = route.errorMessage, - onRetry = { - if (startDestination == SendRoute.Recipient) { - navController.navigate(SendRoute.Recipient) { - popUpTo { inclusive = true } - } - } else { - navController.navigate(SendRoute.Success) - } - }, - onClose = { - appViewModel.hideSheet() - } - ) - } - } - } -} - -sealed interface SendRoute { - @Serializable - data object Recipient : SendRoute - - @Serializable - data object Address : SendRoute - - @Serializable - data object Amount : SendRoute - - @Serializable - data object QrScanner : SendRoute - - @Serializable - data object WithdrawConfirm : SendRoute - - @Serializable - data object WithdrawError : SendRoute - - @Serializable - data object Support : SendRoute - - @Serializable - data object AddTag : SendRoute - - @Serializable - data object PinCheck : SendRoute - - @Serializable - data object CoinSelection : SendRoute - - @Serializable - data object QuickPay : SendRoute - - @Serializable - data object FeeNav : SendRoute - - @Serializable - data object FeeRate : SendRoute - - @Serializable - data object FeeCustom : SendRoute - - @Serializable - data object Confirm : SendRoute - - @Serializable - data object Success : SendRoute - - @Serializable - data class Error(val errorMessage: String) : SendRoute -} diff --git a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt index d7252c73e..9554979dc 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.theme -import androidx.compose.animation.core.AnimationConstants import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.navigationBars @@ -112,9 +111,6 @@ object AppSwitchDefaults { ) } -const val TRANSITION_SCREEN_MS = AnimationConstants.DefaultDurationMillis.toLong() // 300ms -const val TRANSITION_SHEET_MS = 650L - object Insets { val Top: Dp @Composable diff --git a/app/src/main/java/to/bitkit/ui/utils/Transitions.kt b/app/src/main/java/to/bitkit/ui/utils/Transitions.kt deleted file mode 100644 index d40cef1c1..000000000 --- a/app/src/main/java/to/bitkit/ui/utils/Transitions.kt +++ /dev/null @@ -1,119 +0,0 @@ -package to.bitkit.ui.utils - -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.slideOutVertically -import androidx.compose.runtime.Composable -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavDeepLink -import androidx.navigation.NavGraph -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavType -import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation -import to.bitkit.ui.utils.Transitions.defaultEnterTrans -import to.bitkit.ui.utils.Transitions.defaultExitTrans -import to.bitkit.ui.utils.Transitions.defaultPopEnterTrans -import to.bitkit.ui.utils.Transitions.defaultPopExitTrans -import kotlin.reflect.KType - -@Suppress("MagicNumber") -object Transitions { - val slideInHorizontally = slideInHorizontally(animationSpec = tween(), initialOffsetX = { it }) - val slideOutHorizontally = slideOutHorizontally(animationSpec = tween(), targetOffsetX = { it }) - val slideInVertically = slideInVertically(animationSpec = tween(), initialOffsetY = { it }) - val slideOutVertically = slideOutVertically(animationSpec = tween(), targetOffsetY = { it }) - - val defaultEnterTrans: (AnimatedContentTransitionScope.() -> EnterTransition?) = { - slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) - } - - val defaultExitTrans: (AnimatedContentTransitionScope.() -> ExitTransition?) = { - slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth / 3 }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) + fadeOut( - animationSpec = tween(300, easing = FastOutSlowInEasing), - targetAlpha = 0.8f - ) - } - - val defaultPopEnterTrans: (AnimatedContentTransitionScope.() -> EnterTransition?) = { - slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth / 3 }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) + fadeIn( - animationSpec = tween(300, easing = FastOutSlowInEasing), - initialAlpha = 0.8f - ) - } - - val defaultPopExitTrans: (AnimatedContentTransitionScope.() -> ExitTransition?) = { - slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, - animationSpec = tween(300, easing = FastOutSlowInEasing) - ) - } -} - -/** - * Construct a nested [NavGraph] with the default screen transitions. - */ -@Suppress("LongParameterList", "MaxLineLength") -inline fun NavGraphBuilder.navigationWithDefaultTransitions( - startDestination: Any, - typeMap: Map> = emptyMap(), - deepLinks: List = emptyList(), - noinline enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultEnterTrans, - noinline exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultExitTrans, - noinline popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultPopEnterTrans, - noinline popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultPopExitTrans, - noinline builder: NavGraphBuilder.() -> Unit, -) { - navigation( - startDestination = startDestination, - typeMap = typeMap, - deepLinks = deepLinks, - enterTransition = enterTransition, - exitTransition = exitTransition, - popEnterTransition = popEnterTransition, - popExitTransition = popExitTransition, - builder = builder, - ) -} - -/** - * Adds the [Composable] to the [NavGraphBuilder] with the default screen transitions. - */ -@Suppress("LongParameterList", "MaxLineLength") -inline fun NavGraphBuilder.composableWithDefaultTransitions( - typeMap: Map> = emptyMap(), - deepLinks: List = emptyList(), - noinline enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultEnterTrans, - noinline exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultExitTrans, - noinline popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = defaultPopEnterTrans, - noinline popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = defaultPopExitTrans, - noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, -) { - composable( - typeMap = typeMap, - deepLinks = deepLinks, - enterTransition = enterTransition, - exitTransition = exitTransition, - popEnterTransition = popEnterTransition, - popExitTransition = popExitTransition, - content = content, - ) -} diff --git a/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt b/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt index 12025db7b..04e324f6c 100644 --- a/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt +++ b/app/src/main/java/to/bitkit/usecases/WipeWalletUseCase.kt @@ -67,7 +67,7 @@ class WipeWalletUseCase @Inject constructor( } } - companion object Companion { + companion object { const val TAG = "WipeWalletUseCase" } } diff --git a/app/src/main/java/to/bitkit/utils/Bip21Utils.kt b/app/src/main/java/to/bitkit/utils/Bip21Utils.kt index bcb4efb69..cd7ea88f7 100644 --- a/app/src/main/java/to/bitkit/utils/Bip21Utils.kt +++ b/app/src/main/java/to/bitkit/utils/Bip21Utils.kt @@ -1,6 +1,7 @@ package to.bitkit.utils import to.bitkit.models.SATS_IN_BTC +import to.bitkit.ui.nav.UriScheme object Bip21Utils { @@ -11,7 +12,7 @@ object Bip21Utils { message: String? = "Bitkit", lightningInvoice: String? = null ): String { - val builder = StringBuilder("bitcoin:$bitcoinAddress") + val builder = StringBuilder("${UriScheme.BITCOIN.withColon}$bitcoinAddress") val queryParams = mutableListOf() diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 6e7cd229f..e0a5989e4 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -9,8 +9,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.navigation.NavOptions -import androidx.navigation.navOptions import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.ActivityFilter import com.synonym.bitkitcore.FeeRates @@ -70,7 +68,6 @@ import to.bitkit.ext.maxWithdrawableSat import to.bitkit.ext.minSendableSat import to.bitkit.ext.minWithdrawableSat import to.bitkit.ext.nowMillis -import to.bitkit.ext.rawId import to.bitkit.ext.removeSpaces import to.bitkit.ext.setClipboardText import to.bitkit.ext.toHex @@ -97,13 +94,12 @@ import to.bitkit.repositories.PreActivityMetadataRepo import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo import to.bitkit.services.AppUpdaterService -import to.bitkit.ui.Routes -import to.bitkit.ui.components.Sheet import to.bitkit.ui.components.TimedSheetType +import to.bitkit.ui.nav.DeepLinkPatterns +import to.bitkit.ui.nav.Routes +import to.bitkit.ui.nav.removeLightningSchemes import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.toast.ToastQueueManager -import to.bitkit.ui.sheets.SendRoute -import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.utils.Logger import to.bitkit.utils.jsonLogOf import java.math.BigDecimal @@ -474,15 +470,13 @@ class AppViewModel @Inject constructor( SendEvent.DismissAmountWarning -> onDismissAmountWarning() SendEvent.PayConfirmed -> onConfirmPay() SendEvent.ClearPayConfirmation -> _sendUiState.update { s -> s.copy(shouldConfirmPay = false) } - SendEvent.BackToAmount -> setSendEffect(SendEffect.PopBack(SendRoute.Amount)) + SendEvent.BackToAmount -> setSendEffect(SendEffect.PopBack(Routes.Send.Amount())) SendEvent.NavToAddress -> setSendEffect(SendEffect.NavigateToAddress) } } } } - private val isMainScanner get() = currentSheet.value !is Sheet.Send - private fun onEnterManuallyClick() { resetAddressInput() setSendEffect(SendEffect.NavigateToAddress) @@ -566,7 +560,7 @@ class AppViewModel @Inject constructor( ) } refreshOnchainSendIfNeeded() - setSendEffect(SendEffect.PopBack(SendRoute.Confirm)) + setSendEffect(SendEffect.PopBack(Routes.Send.Confirm)) } } @@ -744,11 +738,7 @@ class AppViewModel @Inject constructor( if (quickPayHandled) return refreshOnchainSendIfNeeded() - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Confirm)) - } else { - setSendEffect(SendEffect.NavigateToConfirm) - } + setSendEffect(SendEffect.NavigateToConfirm) return } @@ -760,11 +750,7 @@ class AppViewModel @Inject constructor( context = TAG, ) - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } private suspend fun onScanLightning(invoice: LightningInvoice, scanResult: String) { @@ -801,21 +787,11 @@ class AppViewModel @Inject constructor( if (invoice.amountSatoshis > 0uL) { Logger.info("Found amount in invoice, proceeding with payment", context = TAG) - - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Confirm)) - } else { - setSendEffect(SendEffect.NavigateToConfirm) - } + setSendEffect(SendEffect.NavigateToConfirm) return } Logger.info("No amount found in invoice, proceeding to enter amount", context = TAG) - - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } private suspend fun onScanLnurlPay(data: LnurlPayData) { @@ -850,20 +826,12 @@ class AppViewModel @Inject constructor( val quickPayHandled = handleQuickPayIfApplicable(amountSats = minSendable, lnurlPay = data) if (quickPayHandled) return - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Confirm)) - } else { - setSendEffect(SendEffect.NavigateToConfirm) - } + setSendEffect(SendEffect.NavigateToConfirm) return } Logger.info("No amount found in lnurlPay, proceeding to enter amount manually", context = TAG) - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } private fun onScanLnurlWithdraw(data: LnurlWithdrawData) { @@ -894,20 +862,16 @@ class AppViewModel @Inject constructor( return } - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.Amount)) - } else { - setSendEffect(SendEffect.NavigateToAmount) - } + setSendEffect(SendEffect.NavigateToAmount) } - private suspend fun onScanLnurlAuth(data: LnurlAuthData) { + private fun onScanLnurlAuth(data: LnurlAuthData) { Logger.debug("LNURL: $data", context = TAG) - if (!isMainScanner) { - hideSheet() - delay(TRANSITION_SCREEN_MS) - } - showSheet(Sheet.LnurlAuth(domain = data.domain, lnurl = data.uri, k1 = data.k1)) + mainScreenEffect( + MainScreenEffect.Navigate( + Routes.Sheet.LnurlAuth(domain = data.domain, lnurl = data.uri, k1 = data.k1) + ) + ) } fun requestLnurlAuth(callback: String, k1: String, domain: String) { @@ -944,7 +908,7 @@ class AppViewModel @Inject constructor( hideSheet() // hide scan sheet if opened mainScreenEffect( MainScreenEffect.Navigate( - Routes.LnurlChannel(uri = data.uri, callback = data.callback, k1 = data.k1) + Routes.Sheet.LnurlChannel(uri = data.uri, callback = data.callback, k1 = data.k1) ) ) } @@ -966,13 +930,13 @@ class AppViewModel @Inject constructor( // return // } hideSheet() // hide scan sheet if opened - val nextRoute = Routes.ExternalConnection(data.url) - mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) + mainScreenEffect(MainScreenEffect.Navigate(Routes.External.Connection(data.url))) } private fun onScanGift(code: String, amount: ULong) { - hideSheet() // hide scan sheet if opened - showSheet(Sheet.Gift(code = code, amount = amount)) + mainScreenEffect( + MainScreenEffect.Navigate(Routes.Gift.Loading(code = code, amount = amount)) + ) } private suspend fun handleQuickPayIfApplicable( @@ -1006,11 +970,7 @@ class AppViewModel @Inject constructor( Logger.debug("QuickPayData: $quickPayData", context = TAG) - if (isMainScanner) { - showSheet(Sheet.Send(SendRoute.QuickPay)) - } else { - setSendEffect(SendEffect.NavigateToQuickPay) - } + setSendEffect(SendEffect.NavigateToQuickPay) return true } @@ -1277,8 +1237,7 @@ class AppViewModel @Inject constructor( ).onSuccess { activity -> hideNewTransactionSheet() _transactionSheet.update { it.copy(isLoadingDetails = false) } - val nextRoute = Routes.ActivityDetail(activity.rawId()) - mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) + mainScreenEffect(MainScreenEffect.Navigate(Routes.Activity.Detail(activity))) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) toast(e) @@ -1301,8 +1260,7 @@ class AppViewModel @Inject constructor( ).onSuccess { activity -> hideSheet() _successSendUiState.update { it.copy(isLoadingDetails = false) } - val nextRoute = Routes.ActivityDetail(activity.rawId()) - mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) + mainScreenEffect(MainScreenEffect.Navigate(Routes.Activity.Detail(activity))) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) toast(e) @@ -1530,27 +1488,11 @@ class AppViewModel @Inject constructor( // endregion // region Sheets - private val _currentSheet: MutableStateFlow = MutableStateFlow(null) - val currentSheet = _currentSheet.asStateFlow() - - fun showSheet(sheetType: Sheet) { - viewModelScope.launch { - _currentSheet.value?.let { - _currentSheet.update { null } - delay(SCREEN_TRANSITION_DELAY_MS) - } - _currentSheet.update { sheetType } - } - } - fun hideSheet() { - if (currentSheet.value is Sheet.TimedSheet && currentTimedSheet != null) { + if (currentTimedSheet != null) { dismissTimedSheet() - } else { - _currentSheet.update { null } } } - // endregion // region Toasts @@ -1716,37 +1658,25 @@ class AppViewModel @Inject constructor( } private fun processDeeplink(uri: Uri) = viewModelScope.launch { - if (uri.toString().contains("recovery-mode")) { + // Step 1: Try Nav3 pattern matching first + val patternMatch = DeepLinkPatterns.findMatch(uri) + + // Handle recovery-mode via pattern match + if (patternMatch?.routeClass == Routes.Recovery.Mode::class) { lightningRepo.setRecoveryMode(enabled = true) delay(SCREEN_TRANSITION_DELAY_MS) - mainScreenEffect( - MainScreenEffect.Navigate( - route = Routes.RecoveryMode, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } - ) - ) + mainScreenEffect(MainScreenEffect.Navigate(Routes.Recovery.Mode)) return@launch } if (!walletRepo.walletExists()) return@launch + // Step 2: Fall back to Rust-based parsing for complex URIs val data = uri.toString() delay(SCREEN_TRANSITION_DELAY_MS) handleScan(data.removeLightningSchemes()) } - // TODO Temporary fix while these schemes can't be decoded - @Suppress("SpellCheckingInspection") - private fun String.removeLightningSchemes(): String { - return this - .replace("lnurl:", "") - .replace("lnurlw:", "") - .replace("lnurlc:", "") - .replace("lnurlp:", "") - } - fun checkTimedSheets() { if (backupRepo.isRestoring.value) return @@ -1776,7 +1706,7 @@ class AppViewModel @Inject constructor( ) timedSheetQueue = eligibleSheets currentTimedSheet = eligibleSheets.first() - showSheet(Sheet.TimedSheet(eligibleSheets.first())) + mainScreenEffect(MainScreenEffect.Navigate(eligibleSheets.first().toRoute())) } else { Logger.debug("No timed sheet eligible, skipping", context = "Timed sheet") } @@ -1838,7 +1768,7 @@ class AppViewModel @Inject constructor( if (nextIndex < currentQueue.size) { Logger.debug("Moving to next timed sheet in queue: ${currentQueue[nextIndex].name}") currentTimedSheet = currentQueue[nextIndex] - showSheet(Sheet.TimedSheet(currentQueue[nextIndex])) + mainScreenEffect(MainScreenEffect.Navigate(currentQueue[nextIndex].toRoute())) } else { Logger.debug("Timed sheet queue exhausted") clearTimedSheets() @@ -1898,14 +1828,7 @@ class AppViewModel @Inject constructor( if (androidReleaseInfo.buildNumber <= currentBuildNumber) return@withContext false if (androidReleaseInfo.isCritical) { - mainScreenEffect( - MainScreenEffect.Navigate( - route = Routes.CriticalUpdate, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } - ) - ) + mainScreenEffect(MainScreenEffect.Navigate(Routes.CriticalUpdate)) return@withContext false } @@ -2019,7 +1942,7 @@ sealed class SendFee(open val value: Long) { enum class SendMethod { ONCHAIN, LIGHTNING } sealed class SendEffect { - data class PopBack(val route: SendRoute) : SendEffect() + data class PopBack(val route: Routes) : SendEffect() data object NavigateToAddress : SendEffect() data object NavigateToAmount : SendEffect() data object NavigateToScan : SendEffect() @@ -2034,11 +1957,7 @@ sealed class SendEffect { } sealed class MainScreenEffect { - data class Navigate( - val route: Routes, - val navOptions: NavOptions? = null, - ) : MainScreenEffect() - + data class Navigate(val route: Routes) : MainScreenEffect() data object WipeWallet : MainScreenEffect() data class ProcessClipboardAutoRead(val data: String) : MainScreenEffect() } @@ -2083,3 +2002,11 @@ sealed interface QuickPayData { data class LnurlPay(override val sats: ULong, val callback: String) : QuickPayData } // endregion + +private fun TimedSheetType.toRoute(): Routes = when (this) { + TimedSheetType.APP_UPDATE -> Routes.Sheet.Update + TimedSheetType.BACKUP -> Routes.Sheet.Backup + TimedSheetType.NOTIFICATIONS -> Routes.Sheet.Notifications + TimedSheetType.QUICK_PAY -> Routes.Sheet.QuickPay + TimedSheetType.HIGH_BALANCE -> Routes.Sheet.HighBalance +} diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 1aa39e6a8..f487f509d 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.BtOrderState2 import com.synonym.bitkitcore.IBtOrder +import com.synonym.bitkitcore.regtestMine import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Job @@ -25,9 +26,11 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.lightningdevkit.ldknode.ChannelDetails +import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore +import to.bitkit.env.Env import to.bitkit.ext.amountOnClose import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed @@ -224,7 +227,19 @@ class TransferViewModel @Inject constructor( // Step 0: Starting settingsStore.update { it.copy(lightningSetupStep = LN_SETUP_STEP_0) } Logger.debug("LN setup step: $LN_SETUP_STEP_0", context = TAG) - delay(MIN_STEP_DELAY_MS) + delay(MS_DELAY_STEP) + + // Auto-mine on regtest: delay to let tx propagate, then mine + if (Env.network == Network.REGTEST) { + delay(MS_DELAY_REGTEST_MINE) + try { + Logger.debug("Auto-mining a block for order: '$orderId'", context = TAG) + regtestMine(1u) + Logger.debug("Successfully mined a block", context = TAG) + } catch (e: Throwable) { + Logger.warn("Failed to mine block", e, context = TAG) + } + } // Poll until payment is confirmed (order state becomes PAID or EXECUTED) val paidOrder = pollUntil(orderId) { order -> @@ -234,7 +249,7 @@ class TransferViewModel @Inject constructor( // Step 1: Payment confirmed settingsStore.update { it.copy(lightningSetupStep = LN_SETUP_STEP_1) } Logger.debug("LN setup step: $LN_SETUP_STEP_1", context = TAG) - delay(MIN_STEP_DELAY_MS) + delay(MS_DELAY_STEP) // Try to open channel (idempotent - safe to call multiple times) blocktankRepo.openChannel(paidOrder.id) @@ -242,7 +257,7 @@ class TransferViewModel @Inject constructor( // Step 2: Channel opening requested settingsStore.update { it.copy(lightningSetupStep = LN_SETUP_STEP_2) } Logger.debug("LN setup step: $LN_SETUP_STEP_2", context = TAG) - delay(MIN_STEP_DELAY_MS) + delay(MS_DELAY_STEP) // Poll until channel is ready (EXECUTED state or channel has state) pollUntil(orderId) { order -> @@ -278,7 +293,7 @@ class TransferViewModel @Inject constructor( if (condition(order)) { return order } - delay(POLL_INTERVAL_MS) + delay(MS_INTERVAL_POLL) } } @@ -492,8 +507,9 @@ class TransferViewModel @Inject constructor( companion object { private const val TAG = "TransferViewModel" - private const val MIN_STEP_DELAY_MS = 500L - private const val POLL_INTERVAL_MS = 2_500L + private const val MS_DELAY_STEP = 500L + private const val MS_INTERVAL_POLL = 2_500L + private const val MS_DELAY_REGTEST_MINE = 5_000L const val LN_SETUP_STEP_0 = 0 const val LN_SETUP_STEP_1 = 1 const val LN_SETUP_STEP_2 = 2 diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index ac15b99ef..698308324 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -29,6 +29,7 @@ import to.bitkit.repositories.RecoveryModeException import to.bitkit.repositories.SyncSource import to.bitkit.repositories.WalletRepo import to.bitkit.ui.onboarding.LOADING_MS +import to.bitkit.ui.screens.wallets.receive.CjitEntryDetails import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger import to.bitkit.utils.isTxSyncTimeout @@ -292,6 +293,23 @@ class WalletViewModel @Inject constructor( settingsStore.update { it.copy(hideBalance = true) } } } + + // region Receive Flow State + private val _pendingCjitEntry = MutableStateFlow(null) + val pendingCjitEntry = _pendingCjitEntry.asStateFlow() + + private val _pendingCjitInvoice = MutableStateFlow(null) + val pendingCjitInvoice = _pendingCjitInvoice.asStateFlow() + + fun setPendingCjitEntry(entry: CjitEntryDetails?) = _pendingCjitEntry.update { entry } + + fun setPendingCjitInvoice(invoice: String?) = _pendingCjitInvoice.update { invoice } + + fun resetReceiveFlowState() { + _pendingCjitEntry.update { null } + _pendingCjitInvoice.update { null } + } + // endregion } // TODO rename to walletUiState diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9342bafe0..1a8df3e7f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,10 +4,10 @@ camera = "1.5.2" detekt = "1.23.8" hilt = "2.57.2" hiltAndroidx = "1.3.0" -kotlin = "2.2.21" +kotlin = "2.3.0" ktor = "3.3.3" lifecycle = "2.10.0" -navCompose = "2.9.6" +navigation3 = "1.0.0" room = "2.8.4" slf4j = "2.0.17" haze = "1.7.1" @@ -15,17 +15,17 @@ haze = "1.7.1" [libraries] accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version = "0.36.0" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version = "0.37.3" } -activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.1" } +activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.2" } appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } -biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha04" } +biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.33" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" } # https://developer.android.com/develop/ui/compose/bom/bom-mapping -compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.00" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.01" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-ui = { group = "androidx.compose.ui", name = "ui" } @@ -39,8 +39,8 @@ core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" } core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.2.0" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } -detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.1" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.6.0" } +detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.2" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.7.0" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } @@ -65,8 +65,8 @@ lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "lifecycle" } material = { module = "com.google.android.material:material", version = "1.13.0" } -navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navCompose" } -navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "navCompose" } +navigation-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } +navigation-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" } room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }