@@ -28,10 +28,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
2828import dagger.hilt.android.qualifiers.ApplicationContext
2929import kotlinx.coroutines.CoroutineDispatcher
3030import kotlinx.coroutines.CoroutineScope
31- import kotlinx.coroutines.SupervisorJob
3231import kotlinx.coroutines.async
3332import kotlinx.coroutines.awaitAll
34- import kotlinx.coroutines.cancel
3533import kotlinx.coroutines.coroutineScope
3634import kotlinx.coroutines.delay
3735import kotlinx.coroutines.flow.MutableSharedFlow
@@ -50,7 +48,6 @@ import org.lightningdevkit.ldknode.Event
5048import org.lightningdevkit.ldknode.PaymentId
5149import org.lightningdevkit.ldknode.SpendableUtxo
5250import org.lightningdevkit.ldknode.Txid
53- import to.bitkit.BuildConfig
5451import to.bitkit.R
5552import to.bitkit.data.CacheStore
5653import to.bitkit.data.SettingsStore
@@ -69,7 +66,6 @@ import to.bitkit.ext.maxSendableSat
6966import to.bitkit.ext.maxWithdrawableSat
7067import to.bitkit.ext.minSendableSat
7168import to.bitkit.ext.minWithdrawableSat
72- import to.bitkit.ext.nowMillis
7369import to.bitkit.ext.rawId
7470import to.bitkit.ext.removeSpaces
7571import to.bitkit.ext.setClipboardText
@@ -97,20 +93,23 @@ import to.bitkit.repositories.LightningRepo
9793import to.bitkit.repositories.PreActivityMetadataRepo
9894import to.bitkit.repositories.TransferRepo
9995import to.bitkit.repositories.WalletRepo
100- import to.bitkit.services.AppUpdaterService
10196import to.bitkit.ui.Routes
10297import to.bitkit.ui.components.Sheet
103- import to.bitkit.ui.components.TimedSheetType
10498import to.bitkit.ui.shared.toast.ToastEventBus
10599import to.bitkit.ui.shared.toast.ToastQueueManager
106100import to.bitkit.ui.sheets.SendRoute
107101import to.bitkit.ui.theme.TRANSITION_SCREEN_MS
108102import to.bitkit.utils.Logger
109103import to.bitkit.utils.jsonLogOf
104+ import to.bitkit.utils.timedsheets.TimedSheetManager
105+ import to.bitkit.utils.timedsheets.sheets.AppUpdateTimedSheet
106+ import to.bitkit.utils.timedsheets.sheets.BackupTimedSheet
107+ import to.bitkit.utils.timedsheets.sheets.HighBalanceTimedSheet
108+ import to.bitkit.utils.timedsheets.sheets.NotificationsTimedSheet
109+ import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet
110110import java.math.BigDecimal
111111import javax.inject.Inject
112112import kotlin.coroutines.cancellation.CancellationException
113- import kotlin.time.Clock
114113import kotlin.time.ExperimentalTime
115114
116115@OptIn(ExperimentalTime ::class )
@@ -120,6 +119,7 @@ class AppViewModel @Inject constructor(
120119 connectivityRepo : ConnectivityRepo ,
121120 healthRepo : HealthRepo ,
122121 toastManagerProvider : @JvmSuppressWildcards (CoroutineScope ) -> ToastQueueManager ,
122+ timedSheetManagerProvider : @JvmSuppressWildcards (CoroutineScope ) -> TimedSheetManager ,
123123 @ApplicationContext private val context : Context ,
124124 @BgDispatcher private val bgDispatcher : CoroutineDispatcher ,
125125 private val keychain : Keychain ,
@@ -131,10 +131,14 @@ class AppViewModel @Inject constructor(
131131 private val activityRepo : ActivityRepo ,
132132 private val preActivityMetadataRepo : PreActivityMetadataRepo ,
133133 private val blocktankRepo : BlocktankRepo ,
134- private val appUpdaterService : AppUpdaterService ,
135134 private val notifyPaymentReceivedHandler : NotifyPaymentReceivedHandler ,
136135 private val cacheStore : CacheStore ,
137136 private val transferRepo : TransferRepo ,
137+ private val appUpdateSheet : AppUpdateTimedSheet ,
138+ private val backupSheet : BackupTimedSheet ,
139+ private val notificationsSheet : NotificationsTimedSheet ,
140+ private val quickPaySheet : QuickPayTimedSheet ,
141+ private val highBalanceSheet : HighBalanceTimedSheet ,
138142) : ViewModel() {
139143 val healthState = healthRepo.healthState
140144
@@ -172,9 +176,13 @@ class AppViewModel @Inject constructor(
172176
173177 private val processedPayments = mutableSetOf<String >()
174178
175- private var timedSheetsScope: CoroutineScope ? = null
176- private var timedSheetQueue: List <TimedSheetType > = emptyList()
177- private var currentTimedSheet: TimedSheetType ? = null
179+ private val timedSheetManager = timedSheetManagerProvider(viewModelScope).apply {
180+ registerSheet(appUpdateSheet)
181+ registerSheet(backupSheet)
182+ registerSheet(notificationsSheet)
183+ registerSheet(quickPaySheet)
184+ registerSheet(highBalanceSheet)
185+ }
178186
179187 fun setShowForgotPin (value : Boolean ) {
180188 _showForgotPinSheet .value = value
@@ -223,6 +231,13 @@ class AppViewModel @Inject constructor(
223231 viewModelScope.launch {
224232 lightningRepo.updateGeoBlockState()
225233 }
234+ viewModelScope.launch {
235+ timedSheetManager.currentSheet.collect { sheetType ->
236+ if (sheetType != null ) {
237+ _currentSheet .update { Sheet .TimedSheet (sheetType) }
238+ }
239+ }
240+ }
226241
227242 observeLdkNodeEvents()
228243 observeSendEvents()
@@ -1583,7 +1598,7 @@ class AppViewModel @Inject constructor(
15831598 }
15841599
15851600 fun hideSheet () {
1586- if (currentSheet.value is Sheet .TimedSheet && currentTimedSheet != null ) {
1601+ if (currentSheet.value is Sheet .TimedSheet && timedSheetManager.currentSheet.value != null ) {
15871602 dismissTimedSheet()
15881603 } else {
15891604 _currentSheet .update { null }
@@ -1786,213 +1801,12 @@ class AppViewModel @Inject constructor(
17861801 .replace(" lnurlp:" , " " )
17871802 }
17881803
1789- fun checkTimedSheets () {
1790- if (backupRepo.isRestoring.value) return
1791-
1792- if (currentTimedSheet != null || timedSheetQueue.isNotEmpty()) {
1793- Logger .debug(" Timed sheet already active, skipping check" )
1794- return
1795- }
1796-
1797- timedSheetsScope?.cancel()
1798- timedSheetsScope = CoroutineScope (bgDispatcher + SupervisorJob ())
1799- timedSheetsScope?.launch {
1800- delay(CHECK_DELAY_MILLIS )
1801-
1802- if (currentTimedSheet != null || timedSheetQueue.isNotEmpty()) {
1803- Logger .debug(" Timed sheet became active during delay, skipping" )
1804- return @launch
1805- }
1806-
1807- val eligibleSheets = TimedSheetType .entries
1808- .filter { shouldDisplaySheet(it) }
1809- .sortedByDescending { it.priority }
1810-
1811- if (eligibleSheets.isNotEmpty()) {
1812- Logger .debug(
1813- " Building timed sheet queue: ${eligibleSheets.joinToString { it.name }} " ,
1814- context = " Timed sheet"
1815- )
1816- timedSheetQueue = eligibleSheets
1817- currentTimedSheet = eligibleSheets.first()
1818- showSheet(Sheet .TimedSheet (eligibleSheets.first()))
1819- } else {
1820- Logger .debug(" No timed sheet eligible, skipping" , context = " Timed sheet" )
1821- }
1822- }
1823- }
1824-
1825- fun onLeftHome () {
1826- Logger .debug(" Left home, skipping timed sheet check" )
1827- timedSheetsScope?.cancel()
1828- timedSheetsScope = null
1829- }
1830-
1831- fun dismissTimedSheet (skipQueue : Boolean = false) {
1832- Logger .debug(" dismissTimedSheet called" , context = " Timed sheet" )
1833-
1834- val currentQueue = timedSheetQueue
1835- val currentSheet = currentTimedSheet
1836-
1837- if (currentQueue.isEmpty() || currentSheet == null ) {
1838- clearTimedSheets()
1839- return
1840- }
1841-
1842- viewModelScope.launch {
1843- val currentTime = nowMillis()
1844-
1845- when (currentSheet) {
1846- TimedSheetType .HIGH_BALANCE -> settingsStore.update {
1847- it.copy(
1848- balanceWarningTimes = it.balanceWarningTimes + 1 ,
1849- balanceWarningIgnoredMillis = currentTime,
1850- )
1851- }
1852-
1853- TimedSheetType .NOTIFICATIONS -> settingsStore.update {
1854- it.copy(notificationsIgnoredMillis = currentTime)
1855- }
1856-
1857- TimedSheetType .BACKUP -> settingsStore.update {
1858- it.copy(backupWarningIgnoredMillis = currentTime)
1859- }
1860-
1861- TimedSheetType .QUICK_PAY -> settingsStore.update {
1862- it.copy(quickPayIntroSeen = true )
1863- }
1864-
1865- TimedSheetType .APP_UPDATE -> Unit
1866- }
1867- }
1868-
1869- if (skipQueue) {
1870- clearTimedSheets()
1871- return
1872- }
1873-
1874- val currentIndex = currentQueue.indexOf(currentSheet)
1875- val nextIndex = currentIndex + 1
1876-
1877- if (nextIndex < currentQueue.size) {
1878- Logger .debug(" Moving to next timed sheet in queue: ${currentQueue[nextIndex].name} " )
1879- currentTimedSheet = currentQueue[nextIndex]
1880- showSheet(Sheet .TimedSheet (currentQueue[nextIndex]))
1881- } else {
1882- Logger .debug(" Timed sheet queue exhausted" )
1883- clearTimedSheets()
1884- }
1885- }
1886-
1887- private fun clearTimedSheets () {
1888- currentTimedSheet = null
1889- timedSheetQueue = emptyList()
1890- hideSheet()
1891- }
1892-
1893- private suspend fun shouldDisplaySheet (sheet : TimedSheetType ): Boolean = when (sheet) {
1894- TimedSheetType .APP_UPDATE -> checkAppUpdate()
1895- TimedSheetType .BACKUP -> checkBackupSheet()
1896- TimedSheetType .NOTIFICATIONS -> checkNotificationSheet()
1897- TimedSheetType .QUICK_PAY -> checkQuickPaySheet()
1898- TimedSheetType .HIGH_BALANCE -> checkHighBalance()
1899- }
1900-
1901- private suspend fun checkQuickPaySheet (): Boolean {
1902- val settings = settingsStore.data.first()
1903- if (settings.quickPayIntroSeen || settings.isQuickPayEnabled) return false
1904- val shouldShow = walletRepo.balanceState.value.totalLightningSats > 0U
1905- return shouldShow
1906- }
1907-
1908- private suspend fun checkNotificationSheet (): Boolean {
1909- val settings = settingsStore.data.first()
1910- if (settings.notificationsGranted) return false
1911- if (walletRepo.balanceState.value.totalLightningSats == 0UL ) return false
1912-
1913- return checkTimeout(
1914- lastIgnoredMillis = settings.notificationsIgnoredMillis,
1915- intervalMillis = ONE_WEEK_ASK_INTERVAL_MILLIS
1916- )
1917- }
1918-
1919- private suspend fun checkBackupSheet (): Boolean {
1920- val settings = settingsStore.data.first()
1921- if (settings.backupVerified) return false
1922-
1923- val hasBalance = walletRepo.balanceState.value.totalSats > 0U
1924- if (! hasBalance) return false
1925-
1926- return checkTimeout(
1927- lastIgnoredMillis = settings.backupWarningIgnoredMillis,
1928- intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS
1929- )
1930- }
1931-
1932- private suspend fun checkAppUpdate (): Boolean = withContext(bgDispatcher) {
1933- try {
1934- val androidReleaseInfo = appUpdaterService.getReleaseInfo().platforms.android
1935- val currentBuildNumber = BuildConfig .VERSION_CODE
1804+ fun checkTimedSheets () = timedSheetManager.onHomeScreenEntered()
19361805
1937- if (androidReleaseInfo.buildNumber <= currentBuildNumber) return @withContext false
1806+ fun onLeftHome () = timedSheetManager.onHomeScreenExited()
19381807
1939- if (androidReleaseInfo.isCritical) {
1940- mainScreenEffect(
1941- MainScreenEffect .Navigate (
1942- route = Routes .CriticalUpdate ,
1943- navOptions = navOptions {
1944- popUpTo(0 ) { inclusive = true }
1945- }
1946- )
1947- )
1948- return @withContext false
1949- }
1950-
1951- return @withContext true
1952- } catch (e: Exception ) {
1953- Logger .warn(" Failure fetching new releases" , e = e)
1954- return @withContext false
1955- }
1956- }
1957-
1958- private suspend fun checkHighBalance (): Boolean {
1959- val settings = settingsStore.data.first()
1960-
1961- val totalOnChainSats = walletRepo.balanceState.value.totalSats
1962- val balanceUsd = satsToUsd(totalOnChainSats) ? : return false
1963- val thresholdReached = balanceUsd > BigDecimal (BALANCE_THRESHOLD_USD )
1964-
1965- if (! thresholdReached) {
1966- settingsStore.update { it.copy(balanceWarningTimes = 0 ) }
1967- return false
1968- }
1969-
1970- val belowMaxWarnings = settings.balanceWarningTimes < MAX_WARNINGS
1971-
1972- return checkTimeout(
1973- lastIgnoredMillis = settings.balanceWarningIgnoredMillis,
1974- intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS ,
1975- additionalCondition = belowMaxWarnings
1976- )
1977- }
1978-
1979- private fun checkTimeout (
1980- lastIgnoredMillis : Long ,
1981- intervalMillis : Long ,
1982- additionalCondition : Boolean = true,
1983- ): Boolean {
1984- if (! additionalCondition) return false
1985-
1986- val currentTime = Clock .System .now().toEpochMilliseconds()
1987- val isTimeOutOver = lastIgnoredMillis == 0L ||
1988- (currentTime - lastIgnoredMillis > intervalMillis)
1989- return isTimeOutOver
1990- }
1991-
1992- private fun satsToUsd (sats : ULong ): BigDecimal ? {
1993- val converted = currencyRepo.convertSatsToFiat(sats = sats.toLong(), currency = " USD" ).getOrNull()
1994- return converted?.value
1995- }
1808+ fun dismissTimedSheet (skipQueue : Boolean = false) =
1809+ timedSheetManager.dismissCurrentSheet(skipQueue)
19961810
19971811 companion object {
19981812 private const val TAG = " AppViewModel"
@@ -2001,19 +1815,6 @@ class AppViewModel @Inject constructor(
20011815 private const val MAX_BALANCE_FRACTION = 0.5
20021816 private const val MAX_FEE_AMOUNT_RATIO = 0.5
20031817 private const val SCREEN_TRANSITION_DELAY_MS = 300L
2004-
2005- /* *How high the balance must be to show this warning to the user (in USD)*/
2006- private const val BALANCE_THRESHOLD_USD = 500L
2007- private const val MAX_WARNINGS = 3
2008-
2009- /* * how long this prompt will be hidden if user taps Later*/
2010- private const val ONE_DAY_ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24L
2011-
2012- /* * how long this prompt will be hidden if user taps Later*/
2013- private const val ONE_WEEK_ASK_INTERVAL_MILLIS = ONE_DAY_ASK_INTERVAL_MILLIS * 7L
2014-
2015- /* *How long user needs to stay on the home screen before he see this prompt*/
2016- private const val CHECK_DELAY_MILLIS = 2000L
20171818 }
20181819}
20191820
0 commit comments