diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index e5f8077d0..c4979c18d 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -1,5 +1,6 @@ package to.bitkit.ui +import android.content.Intent import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -12,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver @@ -29,13 +31,16 @@ import androidx.navigation.toRoute import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable +import to.bitkit.env.Env import to.bitkit.models.NewTransactionSheetDetails 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.Sheet import to.bitkit.ui.components.SheetHost +import to.bitkit.ui.components.TimedSheetType import to.bitkit.ui.onboarding.InitializingWalletView import to.bitkit.ui.onboarding.WalletRestoreErrorView import to.bitkit.ui.onboarding.WalletRestoreSuccessView @@ -138,11 +143,16 @@ 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.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.Transitions @@ -321,7 +331,7 @@ fun ContentView( ) { AutoReadClipboardHandler() - val currentSheet by appViewModel.currentSheet + val currentSheet by appViewModel.currentSheet.collectAsStateWithLifecycle() SheetHost( shouldExpand = currentSheet != null, onDismiss = { appViewModel.hideSheet() }, @@ -341,7 +351,7 @@ fun ContentView( ReceiveSheet( walletState = walletUiState, navigateToExternalConnection = { - navController.navigate(Routes.ExternalConnection()) + navController.navigate(ExternalConnection()) appViewModel.hideSheet() } ) @@ -353,6 +363,50 @@ fun ContentView( is Sheet.Backup -> BackupSheet(sheet, onDismiss = { appViewModel.hideSheet() }) is Sheet.LnurlAuth -> LnurlAuthSheet(sheet, appViewModel) Sheet.ForceTransfer -> ForceTransferSheet(appViewModel, transferViewModel) + 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) + } + ) + } + } + } } } ) { 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 cfabd5714..c0ffe470b 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -39,6 +39,17 @@ sealed interface Sheet { data object ActivityTagSelector : Sheet data class LnurlAuth(val domain: String, val lnurl: String, val k1: String) : Sheet data object ForceTransfer : Sheet + + data class TimedSheet(val type: TimedSheetType) : Sheet +} + +/**@param priority Priority levels for timed sheets (higher number = higher priority)*/ +enum class TimedSheetType(val priority: Int) { + APP_UPDATE(priority = 5), + BACKUP(priority = 4), + NOTIFICATIONS(priority = 3), + QUICK_PAY(priority = 2), + HIGH_BALANCE(priority = 1) } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeNav.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeNav.kt index e4dd9e601..6dc64fabd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeNav.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeNav.kt @@ -50,9 +50,11 @@ fun HomeNav( val hasSeenShopIntro: Boolean by settingsViewModel.hasSeenShopIntro.collectAsStateWithLifecycle() val hazeState = rememberHazeState() - RequestNotificationPermissions { granted -> - settingsViewModel.setNotificationPreference(granted) - } + RequestNotificationPermissions( + onPermissionChange = { granted -> + settingsViewModel.setNotificationPreference(granted) + } + ) Box( modifier = Modifier.fillMaxSize() 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 827dd3621..e8c249df3 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 @@ -1,6 +1,5 @@ package to.bitkit.ui.screens.wallets -import android.content.Intent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.gestures.snapping.SnapPosition @@ -55,13 +54,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex -import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController -import androidx.navigation.navOptions import com.synonym.bitkitcore.Activity import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect @@ -79,12 +76,9 @@ import to.bitkit.ui.LocalBalances import to.bitkit.ui.Routes import to.bitkit.ui.components.AppStatus import to.bitkit.ui.components.BalanceHeaderView -import to.bitkit.ui.components.BottomSheet import to.bitkit.ui.components.EmptyStateView import to.bitkit.ui.components.HorizontalSpacer import to.bitkit.ui.components.Sheet -import to.bitkit.ui.components.Sheet.Backup -import to.bitkit.ui.components.SheetHost import to.bitkit.ui.components.StatusBarSpacer import to.bitkit.ui.components.SuggestionCard import to.bitkit.ui.components.TabBar @@ -111,13 +105,8 @@ import to.bitkit.ui.screens.widgets.price.PriceCard import to.bitkit.ui.screens.widgets.weather.WeatherCard import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.shared.util.shareText -import to.bitkit.ui.sheets.BackgroundPaymentsIntroSheet import to.bitkit.ui.sheets.BackupRoute -import to.bitkit.ui.sheets.BackupSheet -import to.bitkit.ui.sheets.HighBalanceWarningSheet import to.bitkit.ui.sheets.PinRoute -import to.bitkit.ui.sheets.QuickPayIntroSheet -import to.bitkit.ui.sheets.UpdateSheet import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -127,7 +116,6 @@ import to.bitkit.viewmodels.MainUiState import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.WalletViewModel -@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( mainUiState: MainUiState, @@ -140,7 +128,6 @@ fun HomeScreen( activityListViewModel: ActivityListViewModel, homeViewModel: HomeViewModel = hiltViewModel(), ) { - val context = LocalContext.current val hasSeenTransferIntro by settingsViewModel.hasSeenTransferIntro.collectAsStateWithLifecycle() val hasSeenShopIntro by settingsViewModel.hasSeenShopIntro.collectAsStateWithLifecycle() val hasSeenProfileIntro by settingsViewModel.hasSeenProfileIntro.collectAsStateWithLifecycle() @@ -150,28 +137,15 @@ fun HomeScreen( val homeUiState by homeViewModel.uiState.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - homeViewModel.checkTimedSheets() - } + val context = LocalContext.current LaunchedEffect(Unit) { - homeViewModel.homeEffect.collect { effect -> - when (effect) { - HomeEffects.NavigateCriticalUpdate -> { - rootNavController.navigate( - route = Routes.CriticalUpdate, - navOptions = navOptions { - popUpTo(0) { inclusive = true } - } - ) - } - } - } + appViewModel.checkTimedSheets() } DisposableEffect(Unit) { onDispose { - homeViewModel.onLeftHome() + appViewModel.onLeftHome() } } @@ -179,177 +153,121 @@ fun HomeScreen( DeleteWidgetAlert(type, homeViewModel) } - SheetHost( - shouldExpand = homeUiState.timedSheet != null, - onDismiss = { homeViewModel.dismissTimedSheet() }, - sheets = { - when (homeUiState.timedSheet) { - TimedSheets.APP_UPDATE -> { - UpdateSheet(onDismiss = { homeViewModel.dismissTimedSheet() }) + Content( + mainUiState = mainUiState, + homeUiState = homeUiState, + rootNavController = rootNavController, + walletNavController = walletNavController, + drawerState = drawerState, + latestActivities = latestActivities, + onRefresh = { + activityListViewModel.fetchLatestActivities() + walletViewModel.onPullToRefresh() + homeViewModel.refreshWidgets() + activityListViewModel.syncLdkNodePayments() + }, + onClickProfile = { + if (!hasSeenProfileIntro) { + rootNavController.navigate(Routes.ProfileIntro) + } else { + rootNavController.navigate(Routes.CreateProfile) + } + }, + onRemoveSuggestion = { suggestion -> + homeViewModel.removeSuggestion(suggestion) + }, + onClickSuggestion = { suggestion -> + when (suggestion) { + Suggestion.BUY -> { + rootNavController.navigate(Routes.BuyIntro) } - TimedSheets.BACKUP -> { - BottomSheet(onDismissRequest = { homeViewModel.dismissTimedSheet() }) { - BackupSheet( - sheet = Backup(BackupRoute.Intro), - onDismiss = { homeViewModel.dismissTimedSheet() } - ) + Suggestion.LIGHTNING -> { + if (!hasSeenTransferIntro) { + rootNavController.navigateToTransferIntro() + } else { + rootNavController.navigateToTransferFunding() } } - TimedSheets.NOTIFICATIONS -> { - BackgroundPaymentsIntroSheet( - onContinue = { - homeViewModel.dismissTimedSheet() - rootNavController.navigate(Routes.BackgroundPaymentsSettings) - settingsViewModel.setBgPaymentsIntroSeen(true) - }, - onDismiss = { homeViewModel.dismissTimedSheet() } - ) + Suggestion.BACK_UP -> { + appViewModel.showSheet(Sheet.Backup(BackupRoute.Intro)) } - TimedSheets.QUICK_PAY -> { - QuickPayIntroSheet( - onContinue = { - homeViewModel.dismissTimedSheet() - rootNavController.navigate(Routes.QuickPaySettings) - }, - onDismiss = { homeViewModel.dismissTimedSheet() } - ) + Suggestion.SECURE -> { + appViewModel.showSheet(Sheet.Pin(PinRoute.Prompt(showLaterButton = true))) } - TimedSheets.HIGH_BALANCE -> { - HighBalanceWarningSheet( - onDismiss = { homeViewModel.dismissTimedSheet() }, - understoodClick = { homeViewModel.dismissTimedSheet() }, - learnMoreClick = { - val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri()) - context.startActivity(intent) - homeViewModel.dismissTimedSheet() - } - ) + Suggestion.SUPPORT -> { + rootNavController.navigate(Routes.Support) } - null -> {} - } - } - ) { - Content( - mainUiState = mainUiState, - homeUiState = homeUiState, - rootNavController = rootNavController, - walletNavController = walletNavController, - drawerState = drawerState, - latestActivities = latestActivities, - onRefresh = { - activityListViewModel.fetchLatestActivities() - walletViewModel.onPullToRefresh() - homeViewModel.refreshWidgets() - activityListViewModel.syncLdkNodePayments() - }, - onClickProfile = { - if (!hasSeenProfileIntro) { - rootNavController.navigate(Routes.ProfileIntro) - } else { - rootNavController.navigate(Routes.CreateProfile) + Suggestion.INVITE -> { + shareText( + context, + context.getString(R.string.settings__about__shareText) + .replace("{appStoreUrl}", Env.APP_STORE_URL) + .replace("{playStoreUrl}", Env.PLAY_STORE_URL) + ) } - }, - onRemoveSuggestion = { suggestion -> - homeViewModel.removeSuggestion(suggestion) - }, - onClickSuggestion = { suggestion -> - when (suggestion) { - Suggestion.BUY -> { - rootNavController.navigate(Routes.BuyIntro) - } - - Suggestion.LIGHTNING -> { - if (!hasSeenTransferIntro) { - rootNavController.navigateToTransferIntro() - } else { - rootNavController.navigateToTransferFunding() - } - } - - Suggestion.BACK_UP -> { - appViewModel.showSheet(Sheet.Backup(BackupRoute.Intro)) - } - Suggestion.SECURE -> { - appViewModel.showSheet(Sheet.Pin(PinRoute.Prompt(showLaterButton = true))) - } - - Suggestion.SUPPORT -> { - rootNavController.navigate(Routes.Support) - } - - Suggestion.INVITE -> { - shareText( - context, - context.getString(R.string.settings__about__shareText) - .replace("{appStoreUrl}", Env.APP_STORE_URL) - .replace("{playStoreUrl}", Env.PLAY_STORE_URL) - ) - } - - Suggestion.PROFILE -> { - if (!hasSeenProfileIntro) { - rootNavController.navigate(Routes.ProfileIntro) - } else { - rootNavController.navigate(Routes.CreateProfile) - } + Suggestion.PROFILE -> { + if (!hasSeenProfileIntro) { + rootNavController.navigate(Routes.ProfileIntro) + } else { + rootNavController.navigate(Routes.CreateProfile) } + } - Suggestion.SHOP -> { - if (!hasSeenShopIntro) { - rootNavController.navigate(Routes.ShopIntro) - } else { - rootNavController.navigate(Routes.ShopDiscover) - } + Suggestion.SHOP -> { + if (!hasSeenShopIntro) { + rootNavController.navigate(Routes.ShopIntro) + } else { + rootNavController.navigate(Routes.ShopDiscover) } + } - Suggestion.QUICK_PAY -> { - if (!quickPayIntroSeen) { - rootNavController.navigate(Routes.QuickPayIntro) - } else { - rootNavController.navigate(Routes.QuickPaySettings) - } + Suggestion.QUICK_PAY -> { + if (!quickPayIntroSeen) { + rootNavController.navigate(Routes.QuickPayIntro) + } else { + rootNavController.navigate(Routes.QuickPaySettings) } - - Suggestion.TRANSFER_PENDING -> Unit - Suggestion.TRANSFER_CLOSING_CHANNEL -> Unit - Suggestion.LIGHTNING_SETTING_UP -> rootNavController.navigate(Routes.SettingUp) - Suggestion.LIGHTNING_READY -> Unit - } - }, - onClickAddWidget = { - if (!hasSeenWidgetsIntro) { - rootNavController.navigate(Routes.WidgetsIntro) - } else { - rootNavController.navigate(Routes.AddWidget) } - }, - onClickEditWidgetList = homeViewModel::onClickEditWidgetList, - onClickEditWidget = { widgetType -> - 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) - } - }, - onClickDeleteWidget = { widgetType -> - homeViewModel.displayAlertDeleteWidget(widgetType) - }, - onMoveWidget = { fromIndex, toIndex -> - homeViewModel.moveWidget(fromIndex, toIndex) - }, - onDismissEmptyState = homeViewModel::dismissEmptyState, - onClickEmptyActivityRow = { appViewModel.showSheet(Sheet.Receive) }, - ) - } + + Suggestion.TRANSFER_PENDING -> Unit + Suggestion.TRANSFER_CLOSING_CHANNEL -> Unit + Suggestion.LIGHTNING_SETTING_UP -> rootNavController.navigate(Routes.SettingUp) + Suggestion.LIGHTNING_READY -> Unit + } + }, + onClickAddWidget = { + if (!hasSeenWidgetsIntro) { + rootNavController.navigate(Routes.WidgetsIntro) + } else { + rootNavController.navigate(Routes.AddWidget) + } + }, + onClickEditWidgetList = homeViewModel::onClickEditWidgetList, + onClickEditWidget = { widgetType -> + 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) + } + }, + onClickDeleteWidget = { widgetType -> + homeViewModel.displayAlertDeleteWidget(widgetType) + }, + onMoveWidget = { fromIndex, toIndex -> + homeViewModel.moveWidget(fromIndex, toIndex) + }, + onDismissEmptyState = homeViewModel::dismissEmptyState, + onClickEmptyActivityRow = { appViewModel.showSheet(Sheet.Receive) }, + ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt index f73f5f568..23298a21f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeUiState.kt @@ -34,15 +34,4 @@ data class HomeUiState( val isEditingWidgets: Boolean = false, val deleteWidgetAlert: WidgetType? = null, val showEmptyState: Boolean = false, - val timedSheet: TimedSheets? = null, - val timedSheetQueue: List = emptyList(), ) - -/**@param priority Priority levels for timed sheets (higher number = higher priority)*/ -enum class TimedSheets(val priority: Int) { - APP_UPDATE(2), - BACKUP(3), - NOTIFICATIONS(2), - QUICK_PAY(1), - HIGH_BALANCE(2) -} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt index 158b43be0..f6fc16cc5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt @@ -3,26 +3,15 @@ package to.bitkit.ui.screens.wallets import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.datetime.Clock -import to.bitkit.BuildConfig import to.bitkit.data.SettingsStore -import to.bitkit.di.BgDispatcher import to.bitkit.models.Suggestion import to.bitkit.models.TransferType import to.bitkit.models.WidgetType @@ -30,14 +19,10 @@ import to.bitkit.models.toSuggestionOrNull import to.bitkit.models.widget.ArticleModel import to.bitkit.models.widget.toArticleModel import to.bitkit.models.widget.toBlockModel -import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo import to.bitkit.repositories.WidgetsRepo -import to.bitkit.services.AppUpdaterService import to.bitkit.ui.screens.widgets.blocks.toWeatherModel -import to.bitkit.utils.Logger -import java.math.BigDecimal import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -46,24 +31,15 @@ class HomeViewModel @Inject constructor( private val walletRepo: WalletRepo, private val widgetsRepo: WidgetsRepo, private val settingsStore: SettingsStore, - private val currencyRepo: CurrencyRepo, private val transferRepo: TransferRepo, - private val appUpdaterService: AppUpdaterService, - @BgDispatcher private val bgDispatcher: CoroutineDispatcher, ) : ViewModel() { - private var timedSheetsScope: CoroutineScope? = null - private val _uiState = MutableStateFlow(HomeUiState()) val uiState: StateFlow = _uiState.asStateFlow() private val _currentArticle = MutableStateFlow(null) private val _currentFact = MutableStateFlow(null) - private val _homeEffect = MutableSharedFlow(extraBufferCapacity = 1) - val homeEffect = _homeEffect.asSharedFlow() - private fun setHomeEffect(effect: HomeEffects) = viewModelScope.launch { _homeEffect.emit(effect) } - init { setupStateObservation() setupArticleRotation() @@ -109,7 +85,6 @@ class HomeViewModel @Inject constructor( showEmptyState = settings.showEmptyBalanceView && balanceState.totalSats == 0uL ) }.collect { newState -> - checkTimedSheets() _uiState.update { newState } } } @@ -165,195 +140,6 @@ class HomeViewModel @Inject constructor( _currentFact.value = null } - fun checkTimedSheets() { - if (_uiState.value.timedSheet != null || _uiState.value.timedSheetQueue.isNotEmpty()) { - Logger.debug("Timed sheet already active, skipping check") - return - } - - timedSheetsScope?.cancel() - timedSheetsScope = CoroutineScope(bgDispatcher + SupervisorJob()) - timedSheetsScope?.launch { - delay(CHECK_DELAY_MILLIS) - - if (_uiState.value.timedSheet != null || _uiState.value.timedSheetQueue.isNotEmpty()) { - Logger.debug("Timed sheet became active during delay, skipping") - return@launch - } - - val eligibleSheets = TimedSheets.entries - .filter { shouldDisplaySheet(it) } - .sortedByDescending { it.priority } - - if (eligibleSheets.isNotEmpty()) { - Logger.debug("Building timed sheet queue: ${eligibleSheets.joinToString { it.name }}") - _uiState.update { - it.copy( - timedSheet = eligibleSheets.first(), - timedSheetQueue = eligibleSheets - ) - } - } else { - Logger.debug("No timed sheet eligible, skipping") - } - } - } - - private suspend fun shouldDisplaySheet(sheet: TimedSheets): Boolean = when (sheet) { - TimedSheets.APP_UPDATE -> checkAppUpdate() - TimedSheets.BACKUP -> checkBackupSheet() - TimedSheets.NOTIFICATIONS -> checkNotificationSheet() - TimedSheets.QUICK_PAY -> checkQuickPaySheet() - TimedSheets.HIGH_BALANCE -> checkHighBalance() - } - - fun onLeftHome() { - timedSheetsScope?.cancel() - timedSheetsScope = null - } - - fun dismissTimedSheet() { - val currentQueue = _uiState.value.timedSheetQueue - val currentSheet = _uiState.value.timedSheet - - if (currentQueue.isEmpty() || currentSheet == null) { - _uiState.update { it.copy(timedSheet = null, timedSheetQueue = emptyList()) } - return - } - - viewModelScope.launch { - val currentTime = Clock.System.now().toEpochMilliseconds() - - when (currentSheet) { - TimedSheets.BACKUP -> { - settingsStore.update { it.copy(backupWarningIgnoredMillis = currentTime) } - } - - TimedSheets.HIGH_BALANCE -> { - settingsStore.update { - it.copy( - balanceWarningTimes = it.balanceWarningTimes + 1, - balanceWarningIgnoredMillis = currentTime - ) - } - } - - TimedSheets.APP_UPDATE -> Unit - TimedSheets.NOTIFICATIONS -> { - settingsStore.update { it.copy(notificationsIgnoredMillis = currentTime) } - } - - TimedSheets.QUICK_PAY -> { - settingsStore.update { it.copy(quickPayIntroSeen = true) } - } - } - } - - val currentIndex = currentQueue.indexOf(currentSheet) - val nextIndex = currentIndex + 1 - - if (nextIndex < currentQueue.size) { - Logger.debug("Moving to next timed sheet in queue: ${currentQueue[nextIndex].name}") - _uiState.update { - it.copy(timedSheet = currentQueue[nextIndex]) - } - } else { - Logger.debug("Timed sheet queue exhausted") - _uiState.update { - it.copy(timedSheet = null, timedSheetQueue = emptyList()) - } - } - } - - private suspend fun checkQuickPaySheet(): Boolean { - val settings = settingsStore.data.first() - if (settings.quickPayIntroSeen || settings.isQuickPayEnabled) return false - val shouldShow = walletRepo.balanceState.value.totalLightningSats > 0U - return shouldShow - } - - private suspend fun checkNotificationSheet(): Boolean { - val settings = settingsStore.data.first() - if (settings.notificationsGranted) return false - if (walletRepo.balanceState.value.totalLightningSats == 0UL) return false - - return checkTimeout( - lastIgnoredMillis = settings.notificationsIgnoredMillis, - intervalMillis = ONE_WEEK_ASK_INTERVAL_MILLIS - ) - } - - private suspend fun checkBackupSheet(): Boolean { - val settings = settingsStore.data.first() - if (settings.backupVerified) return false - - val hasBalance = walletRepo.balanceState.value.totalSats > 0U - if (!hasBalance) return false - - return checkTimeout( - lastIgnoredMillis = settings.backupWarningIgnoredMillis, - intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS - ) - } - - private suspend fun checkAppUpdate(): Boolean = withContext(bgDispatcher) { - try { - val androidReleaseInfo = appUpdaterService.getReleaseInfo().platforms.android - val currentBuildNumber = BuildConfig.VERSION_CODE - - if (androidReleaseInfo.buildNumber <= currentBuildNumber) return@withContext false - - if (androidReleaseInfo.isCritical) { - setHomeEffect(HomeEffects.NavigateCriticalUpdate) - return@withContext false - } - - return@withContext true - } catch (e: Exception) { - Logger.warn("Failure fetching new releases", e = e) - return@withContext false - } - } - - private suspend fun checkHighBalance(): Boolean { - val settings = settingsStore.data.first() - - val totalOnChainSats = walletRepo.balanceState.value.totalSats - val balanceUsd = satsToUsd(totalOnChainSats) ?: return false - val thresholdReached = balanceUsd > BigDecimal(BALANCE_THRESHOLD_USD) - - if (!thresholdReached) { - settingsStore.update { it.copy(balanceWarningTimes = 0) } - return false - } - - val belowMaxWarnings = settings.balanceWarningTimes < MAX_WARNINGS - - return checkTimeout( - lastIgnoredMillis = settings.balanceWarningIgnoredMillis, - intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS, - additionalCondition = belowMaxWarnings - ) - } - - private suspend inline fun checkTimeout( - lastIgnoredMillis: Long, - intervalMillis: Long, - additionalCondition: Boolean = true, - ): Boolean { - if (!additionalCondition) return false - - val currentTime = Clock.System.now().toEpochMilliseconds() - val isTimeOutOver = lastIgnoredMillis == 0L || - (currentTime - lastIgnoredMillis > intervalMillis) - return isTimeOutOver - } - - private fun satsToUsd(sats: ULong): BigDecimal? { - val converted = currencyRepo.convertSatsToFiat(sats = sats.toLong(), currency = "USD").getOrNull() - return converted?.value - } - fun dismissEmptyState() { viewModelScope.launch { settingsStore.update { it.copy(showEmptyBalanceView = false) } @@ -435,7 +221,6 @@ class HomeViewModel @Inject constructor( balanceState.totalLightningSats > 0uL -> { // With Lightning listOfNotNull( Suggestion.BACK_UP.takeIf { !settings.backupVerified }, - // The previous list has LIGHTNING_SETTING_UP and the current don't Suggestion.LIGHTNING_READY.takeIf { Suggestion.LIGHTNING_SETTING_UP in _uiState.value.suggestions && transfers.all { it.type != TransferType.TO_SPENDING } @@ -484,27 +269,7 @@ class HomeViewModel @Inject constructor( ) } } - // TODO REMOVE PROFILE CARD IF THE USER ALREADY HAS one val dismissedList = settings.dismissedSuggestions.mapNotNull { it.toSuggestionOrNull() } baseSuggestions.filterNot { it in dismissedList } } - - companion object { - /**How high the balance must be to show this warning to the user (in USD)*/ - private const val BALANCE_THRESHOLD_USD = 500L - private const val MAX_WARNINGS = 3 - - /** how long this prompt will be hidden if user taps Later*/ - private const val ONE_DAY_ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24L - - /** how long this prompt will be hidden if user taps Later*/ - private const val ONE_WEEK_ASK_INTERVAL_MILLIS = ONE_DAY_ASK_INTERVAL_MILLIS * 7L - - /**How long user needs to stay on the home screen before he see this prompt*/ - private const val CHECK_DELAY_MILLIS = 2000L - } -} - -sealed interface HomeEffects { - data object NavigateCriticalUpdate : HomeEffects } diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt index 4ae2a4609..1fcea8362 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsIntroScreen.kt @@ -30,10 +30,11 @@ fun BackgroundPaymentsIntroScreen( onBack: () -> Unit, onClose: () -> Unit, onContinue: () -> Unit, + modifier: Modifier = Modifier, settingsViewModel: SettingsViewModel = hiltViewModel(), ) { Column( - modifier = Modifier.screen() + modifier = modifier.screen() ) { AppTopBar( titleText = "Background Payments", // Todo Transifex @@ -50,9 +51,12 @@ fun BackgroundPaymentsIntroScreen( } @Composable -fun BackgroundPaymentsIntroContent(onContinue: () -> Unit) { +fun BackgroundPaymentsIntroContent( + onContinue: () -> Unit, + modifier: Modifier = Modifier, +) { Column( - modifier = Modifier.padding(horizontal = 32.dp) + modifier = modifier.padding(horizontal = 32.dp) ) { Image( painter = painterResource(R.drawable.bell), 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 b70261d52..fa5b82d10 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,9 +43,12 @@ fun BackgroundPaymentsSettings( val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() val showNotificationDetails by settingsViewModel.showNotificationDetails.collectAsStateWithLifecycle() - RequestNotificationPermissions(showPermissionDialog = false) { granted -> - settingsViewModel.setNotificationPreference(granted) - } + RequestNotificationPermissions( + onPermissionChange = { granted -> + settingsViewModel.setNotificationPreference(granted) + }, + showPermissionDialog = false + ) Content( onBack = onBack, @@ -90,13 +93,11 @@ private fun Content( onClick = onSystemSettingsClick ) - AnimatedVisibility( - visible = hasPermission, - modifier = Modifier.padding(vertical = 16.dp), - ) { + if (hasPermission) { BodyM( text = "Background payments are enabled. You can receive funds even when the app is closed (if your device is connected to the internet).", color = Colors.White64, + modifier = Modifier.padding(vertical = 16.dp), ) } @@ -180,4 +181,3 @@ private fun Preview2() { ) } } - 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 ebf6ed18c..dda09b073 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 @@ -50,6 +50,7 @@ fun GeneralSettingsScreen( val lastUsedTags by settings.lastUsedTags.collectAsStateWithLifecycle() val quickPayIntroSeen by settings.quickPayIntroSeen.collectAsStateWithLifecycle() val bgPaymentsIntroSeen by settings.bgPaymentsIntroSeen.collectAsStateWithLifecycle() + val notificationsGranted by settings.notificationsGranted.collectAsStateWithLifecycle() val languageUiState by languageViewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { languageViewModel.fetchLanguageInfo() } @@ -69,13 +70,14 @@ fun GeneralSettingsScreen( onTagsClick = { navController.navigateToTagsSettings() }, onLanguageSettingsClick = { navController.navigateToLanguageSettings() }, onBgPaymentsClick = { - if (bgPaymentsIntroSeen) { + if (bgPaymentsIntroSeen || notificationsGranted) { navController.navigate(Routes.BackgroundPaymentsSettings) } else { navController.navigate(Routes.BackgroundPaymentsIntro) } }, - selectedLanguage = languageUiState.selectedLanguage.displayName + selectedLanguage = languageUiState.selectedLanguage.displayName, + notificationsGranted = notificationsGranted ) } @@ -86,6 +88,7 @@ private fun GeneralSettingsContent( defaultTransactionSpeed: TransactionSpeed, selectedLanguage: String, showTagsButton: Boolean = false, + notificationsGranted: Boolean, onBackClick: () -> Unit = {}, onCloseClick: () -> Unit = {}, onLocalCurrencyClick: () -> Unit = {}, @@ -157,6 +160,7 @@ private fun GeneralSettingsContent( SettingsButtonRow( title = "Background Payments", // TODO Transifex onClick = onBgPaymentsClick, + value = SettingsButtonValue.StringValue(if (notificationsGranted) "On" else "Off"), modifier = Modifier.testTag("BackgroundPaymentSettings") ) } @@ -172,7 +176,8 @@ private fun Preview() { primaryDisplay = PrimaryDisplay.BITCOIN, defaultTransactionSpeed = TransactionSpeed.Medium, showTagsButton = true, - selectedLanguage = Language.SYSTEM_DEFAULT.displayName + selectedLanguage = Language.SYSTEM_DEFAULT.displayName, + notificationsGranted = true ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPayIntroScreen.kt b/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPayIntroScreen.kt index 9ff2820ec..cb090561b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPayIntroScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/quickPay/QuickPayIntroScreen.kt @@ -2,9 +2,7 @@ package to.bitkit.ui.settings.quickPay import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -20,7 +18,7 @@ import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon -import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.util.screen import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -30,8 +28,11 @@ fun QuickPayIntroScreen( onBack: () -> Unit, onClose: () -> Unit, onContinue: () -> Unit, + modifier: Modifier = Modifier, ) { - ScreenColumn { + Column( + modifier = modifier.screen() + ) { AppTopBar( titleText = stringResource(R.string.settings__quickpay__nav_title), onBackClick = onBack, @@ -43,9 +44,12 @@ fun QuickPayIntroScreen( } @Composable -fun QuickPayIntroContent(onContinue: () -> Unit) { +fun QuickPayIntroContent( + onContinue: () -> Unit, + modifier: Modifier = Modifier, +) { Column( - modifier = Modifier.padding(horizontal = 32.dp) + modifier = modifier.padding(horizontal = 32.dp) ) { Image( painter = painterResource(R.drawable.fast_forward), diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackgroundPaymentsIntroSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackgroundPaymentsIntroSheet.kt index 064e1d1f3..feae729ea 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BackgroundPaymentsIntroSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BackgroundPaymentsIntroSheet.kt @@ -3,12 +3,10 @@ package to.bitkit.ui.sheets import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview -import to.bitkit.ui.components.BottomSheet import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.settings.backgroundPayments.BackgroundPaymentsIntroContent @@ -16,21 +14,13 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface -@OptIn(ExperimentalMaterial3Api::class) @Composable fun BackgroundPaymentsIntroSheet( onContinue: () -> Unit, - onDismiss: () -> Unit, + modifier: Modifier = Modifier, ) { - BottomSheet(onDismissRequest = onDismiss) { - BackgroundPaymentsIntroSheetContent(onContinue) - } -} - -@Composable -private fun BackgroundPaymentsIntroSheetContent(onContinue: () -> Unit) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .sheetHeight(isModal = true) .gradientBackground() @@ -40,7 +30,7 @@ private fun BackgroundPaymentsIntroSheetContent(onContinue: () -> Unit) { SheetTopBar( titleText = "Background Payments", // Todo Transifex ) - BackgroundPaymentsIntroContent(onContinue) + BackgroundPaymentsIntroContent(onContinue = onContinue) } } @@ -50,7 +40,7 @@ private fun Preview() { AppThemeSurface { Column { BottomSheetPreview { - BackgroundPaymentsIntroSheetContent( + BackgroundPaymentsIntroSheet( onContinue = {}, ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/HighBalanceWarningSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/HighBalanceWarningSheet.kt index f453d0e9e..3d413cc63 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/HighBalanceWarningSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/HighBalanceWarningSheet.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -18,7 +17,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.BottomSheet import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -32,28 +30,14 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -@OptIn(ExperimentalMaterial3Api::class) @Composable fun HighBalanceWarningSheet( - onDismiss: () -> Unit, - understoodClick: () -> Unit, - learnMoreClick: () -> Unit, -) { - BottomSheet(onDismissRequest = onDismiss) { - HighBalanceWarningContent( - understoodClick = understoodClick, - learnMoreClick = learnMoreClick, - ) - } -} - -@Composable -fun HighBalanceWarningContent( understoodClick: () -> Unit, learnMoreClick: () -> Unit, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .sheetHeight(isModal = true) .gradientBackground() @@ -84,8 +68,7 @@ fun HighBalanceWarningContent( ) VerticalSpacer(8.dp) BodyM( - text = - stringResource(R.string.other__high_balance__text).withAccent( + text = stringResource(R.string.other__high_balance__text).withAccent( defaultColor = Colors.White64, accentStyle = AppTextStyles.Subtitle.merge(color = Colors.White).toSpanStyle() ), @@ -129,7 +112,7 @@ private fun Preview() { AppThemeSurface { Column { BottomSheetPreview { - HighBalanceWarningContent( + HighBalanceWarningSheet( understoodClick = {}, learnMoreClick = {}, ) diff --git a/app/src/main/java/to/bitkit/ui/sheets/QuickPayIntroSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/QuickPayIntroSheet.kt index 42c1097cf..88c375e79 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/QuickPayIntroSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/QuickPayIntroSheet.kt @@ -3,14 +3,12 @@ package to.bitkit.ui.sheets import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import to.bitkit.R -import to.bitkit.ui.components.BottomSheet import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.settings.quickPay.QuickPayIntroContent @@ -18,21 +16,13 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface -@OptIn(ExperimentalMaterial3Api::class) @Composable fun QuickPayIntroSheet( onContinue: () -> Unit, - onDismiss: () -> Unit, + modifier: Modifier = Modifier, ) { - BottomSheet(onDismissRequest = onDismiss) { - QuickPayIntroSheetContent(onContinue) - } -} - -@Composable -private fun QuickPayIntroSheetContent(onContinue: () -> Unit) { Column( - modifier = Modifier + modifier = modifier .fillMaxWidth() .sheetHeight(isModal = true) .gradientBackground() @@ -40,7 +30,7 @@ private fun QuickPayIntroSheetContent(onContinue: () -> Unit) { .testTag("quick_pay_intro_sheet") ) { SheetTopBar(stringResource(R.string.settings__quickpay__nav_title)) - QuickPayIntroContent(onContinue) + QuickPayIntroContent(onContinue = onContinue) } } @@ -50,7 +40,7 @@ private fun Preview() { AppThemeSurface { Column { BottomSheetPreview { - QuickPayIntroSheetContent( + QuickPayIntroSheet( onContinue = {}, ) } diff --git a/app/src/main/java/to/bitkit/ui/sheets/UpdateSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/UpdateSheet.kt index 64499dda2..7e892dbca 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/UpdateSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/UpdateSheet.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -22,7 +21,6 @@ import androidx.core.net.toUri import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.BottomSheet import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -36,18 +34,8 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -@OptIn(ExperimentalMaterial3Api::class) @Composable fun UpdateSheet( - onDismiss: () -> Unit, -) { - BottomSheet(onDismissRequest = onDismiss) { - UpdateSheetContent(onDismiss) - } -} - -@Composable -fun UpdateSheetContent( onCancel: () -> Unit, modifier: Modifier = Modifier, ) { @@ -118,7 +106,7 @@ fun UpdateSheetContent( private fun Preview() { AppThemeSurface { BottomSheetPreview { - UpdateSheetContent( + UpdateSheet( onCancel = {}, ) } diff --git a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt index 7a7b2e231..21c815ec4 100644 --- a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt +++ b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt @@ -17,8 +17,8 @@ import androidx.lifecycle.LifecycleEventObserver @Composable fun RequestNotificationPermissions( - showPermissionDialog: Boolean = true, onPermissionChange: (Boolean) -> Unit, + showPermissionDialog: Boolean = true, ) { val context = LocalContext.current val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 7937f984f..d31234905 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -26,8 +26,11 @@ import com.synonym.bitkitcore.validateBitcoinAddress import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -41,10 +44,12 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.datetime.Clock import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId import org.lightningdevkit.ldknode.SpendableUtxo import org.lightningdevkit.ldknode.Txid +import to.bitkit.BuildConfig import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain @@ -81,9 +86,11 @@ import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.HealthRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo +import to.bitkit.services.AppUpdaterService import to.bitkit.services.LdkNodeEventBus import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet +import to.bitkit.ui.components.TimedSheetType import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.TRANSITION_SCREEN_MS @@ -106,6 +113,7 @@ class AppViewModel @Inject constructor( private val blocktankRepo: BlocktankRepo, private val connectivityRepo: ConnectivityRepo, private val healthRepo: HealthRepo, + private val appUpdaterService: AppUpdaterService, ) : ViewModel() { val healthState = healthRepo.healthState @@ -143,6 +151,10 @@ class AppViewModel @Inject constructor( private val processedPayments = mutableSetOf() + private var timedSheetsScope: CoroutineScope? = null + private var timedSheetQueue: List = emptyList() + private var currentTimedSheet: TimedSheetType? = null + fun setShowForgotPin(value: Boolean) { _showForgotPinSheet.value = value } @@ -193,6 +205,12 @@ class AppViewModel @Inject constructor( observeLdkNodeEvents() observeSendEvents() + + viewModelScope.launch { + walletRepo.balanceState.collect { + checkTimedSheets() + } + } } private fun observeLdkNodeEvents() { @@ -1348,16 +1366,27 @@ class AppViewModel @Inject constructor( // endregion // region Sheets - var currentSheet = mutableStateOf(null) - private set + private val _currentSheet: MutableStateFlow = MutableStateFlow(null) + val currentSheet = _currentSheet.asStateFlow() fun showSheet(sheetType: Sheet) { - currentSheet.value = sheetType + viewModelScope.launch { + _currentSheet.value?.let { + _currentSheet.update { null } + delay(SCREEN_TRANSITION_DELAY_MS) + } + _currentSheet.update { sheetType } + } } fun hideSheet() { - currentSheet.value = null + if (currentSheet.value is Sheet.TimedSheet && currentTimedSheet != null) { + dismissTimedSheet() + } else { + _currentSheet.update { null } + } } + // endregion // region Toasts @@ -1543,6 +1572,214 @@ class AppViewModel @Inject constructor( .replace("lnurlp:", "") } + fun checkTimedSheets() { + if (currentTimedSheet != null || timedSheetQueue.isNotEmpty()) { + Logger.debug("Timed sheet already active, skipping check") + return + } + + timedSheetsScope?.cancel() + timedSheetsScope = CoroutineScope(bgDispatcher + SupervisorJob()) + timedSheetsScope?.launch { + delay(CHECK_DELAY_MILLIS) + + if (currentTimedSheet != null || timedSheetQueue.isNotEmpty()) { + Logger.debug("Timed sheet became active during delay, skipping") + return@launch + } + + val eligibleSheets = TimedSheetType.entries + .filter { shouldDisplaySheet(it) } + .sortedByDescending { it.priority } + + if (eligibleSheets.isNotEmpty()) { + Logger.debug( + "Building timed sheet queue: ${eligibleSheets.joinToString { it.name }}", + context = "Timed sheet" + ) + timedSheetQueue = eligibleSheets + currentTimedSheet = eligibleSheets.first() + showSheet(Sheet.TimedSheet(eligibleSheets.first())) + } else { + Logger.debug("No timed sheet eligible, skipping", context = "Timed sheet") + } + } + } + + fun onLeftHome() { + Logger.debug("Left home, skipping timed sheet check") + timedSheetsScope?.cancel() + timedSheetsScope = null + } + + fun dismissTimedSheet(skipQueue: Boolean = false) { + Logger.debug("dismissTimedSheet called", context = "Timed sheet") + + val currentQueue = timedSheetQueue + val currentSheet = currentTimedSheet + + if (currentQueue.isEmpty() || currentSheet == null) { + clearTimedSheets() + return + } + + viewModelScope.launch { + val currentTime = Clock.System.now().toEpochMilliseconds() + + when (currentSheet) { + TimedSheetType.BACKUP -> { + settingsStore.update { it.copy(backupWarningIgnoredMillis = currentTime) } + } + + TimedSheetType.HIGH_BALANCE -> { + settingsStore.update { + it.copy( + balanceWarningTimes = it.balanceWarningTimes + 1, + balanceWarningIgnoredMillis = currentTime + ) + } + } + + TimedSheetType.APP_UPDATE -> Unit + + TimedSheetType.NOTIFICATIONS -> { + settingsStore.update { it.copy(notificationsIgnoredMillis = currentTime) } + } + + TimedSheetType.QUICK_PAY -> { + settingsStore.update { it.copy(quickPayIntroSeen = true) } + } + } + } + + if (skipQueue) { + clearTimedSheets() + return + } + + val currentIndex = currentQueue.indexOf(currentSheet) + val nextIndex = currentIndex + 1 + + if (nextIndex < currentQueue.size) { + Logger.debug("Moving to next timed sheet in queue: ${currentQueue[nextIndex].name}") + currentTimedSheet = currentQueue[nextIndex] + showSheet(Sheet.TimedSheet(currentQueue[nextIndex])) + } else { + Logger.debug("Timed sheet queue exhausted") + clearTimedSheets() + } + } + + private fun clearTimedSheets() { + currentTimedSheet = null + timedSheetQueue = emptyList() + hideSheet() + } + + private suspend fun shouldDisplaySheet(sheet: TimedSheetType): Boolean = when (sheet) { + TimedSheetType.APP_UPDATE -> checkAppUpdate() + TimedSheetType.BACKUP -> checkBackupSheet() + TimedSheetType.NOTIFICATIONS -> checkNotificationSheet() + TimedSheetType.QUICK_PAY -> checkQuickPaySheet() + TimedSheetType.HIGH_BALANCE -> checkHighBalance() + } + + private suspend fun checkQuickPaySheet(): Boolean { + val settings = settingsStore.data.first() + if (settings.quickPayIntroSeen || settings.isQuickPayEnabled) return false + val shouldShow = walletRepo.balanceState.value.totalLightningSats > 0U + return shouldShow + } + + private suspend fun checkNotificationSheet(): Boolean { + val settings = settingsStore.data.first() + if (settings.notificationsGranted) return false + if (walletRepo.balanceState.value.totalLightningSats == 0UL) return false + + return checkTimeout( + lastIgnoredMillis = settings.notificationsIgnoredMillis, + intervalMillis = ONE_WEEK_ASK_INTERVAL_MILLIS + ) + } + + private suspend fun checkBackupSheet(): Boolean { + val settings = settingsStore.data.first() + if (settings.backupVerified) return false + + val hasBalance = walletRepo.balanceState.value.totalSats > 0U + if (!hasBalance) return false + + return checkTimeout( + lastIgnoredMillis = settings.backupWarningIgnoredMillis, + intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS + ) + } + + private suspend fun checkAppUpdate(): Boolean = withContext(bgDispatcher) { + try { + val androidReleaseInfo = appUpdaterService.getReleaseInfo().platforms.android + val currentBuildNumber = BuildConfig.VERSION_CODE + + if (androidReleaseInfo.buildNumber <= currentBuildNumber) return@withContext false + + if (androidReleaseInfo.isCritical) { + mainScreenEffect( + MainScreenEffect.Navigate( + route = Routes.CriticalUpdate, + navOptions = navOptions { + popUpTo(0) { inclusive = true } + } + ) + ) + return@withContext false + } + + return@withContext true + } catch (e: Exception) { + Logger.warn("Failure fetching new releases", e = e) + return@withContext false + } + } + + private suspend fun checkHighBalance(): Boolean { + val settings = settingsStore.data.first() + + val totalOnChainSats = walletRepo.balanceState.value.totalSats + val balanceUsd = satsToUsd(totalOnChainSats) ?: return false + val thresholdReached = balanceUsd > BigDecimal(BALANCE_THRESHOLD_USD) + + if (!thresholdReached) { + settingsStore.update { it.copy(balanceWarningTimes = 0) } + return false + } + + val belowMaxWarnings = settings.balanceWarningTimes < MAX_WARNINGS + + return checkTimeout( + lastIgnoredMillis = settings.balanceWarningIgnoredMillis, + intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS, + additionalCondition = belowMaxWarnings + ) + } + + private fun checkTimeout( + lastIgnoredMillis: Long, + intervalMillis: Long, + additionalCondition: Boolean = true, + ): Boolean { + if (!additionalCondition) return false + + val currentTime = Clock.System.now().toEpochMilliseconds() + val isTimeOutOver = lastIgnoredMillis == 0L || + (currentTime - lastIgnoredMillis > intervalMillis) + return isTimeOutOver + } + + private fun satsToUsd(sats: ULong): BigDecimal? { + val converted = currencyRepo.convertSatsToFiat(sats = sats.toLong(), currency = "USD").getOrNull() + return converted?.value + } + companion object { private const val TAG = "AppViewModel" private const val SEND_AMOUNT_WARNING_THRESHOLD = 100.0 @@ -1550,6 +1787,19 @@ class AppViewModel @Inject constructor( private const val MAX_BALANCE_FRACTION = 0.5 private const val MAX_FEE_AMOUNT_RATIO = 0.5 private const val SCREEN_TRANSITION_DELAY_MS = 300L + + /**How high the balance must be to show this warning to the user (in USD)*/ + private const val BALANCE_THRESHOLD_USD = 500L + private const val MAX_WARNINGS = 3 + + /** how long this prompt will be hidden if user taps Later*/ + private const val ONE_DAY_ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24L + + /** how long this prompt will be hidden if user taps Later*/ + private const val ONE_WEEK_ASK_INTERVAL_MILLIS = ONE_DAY_ASK_INTERVAL_MILLIS * 7L + + /**How long user needs to stay on the home screen before he see this prompt*/ + private const val CHECK_DELAY_MILLIS = 2000L } }