Skip to content

Commit 2964646

Browse files
committed
refactor: replace timed sheet logic with manager
1 parent ec46cfb commit 2964646

File tree

1 file changed

+31
-230
lines changed

1 file changed

+31
-230
lines changed

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 31 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel
2828
import dagger.hilt.android.qualifiers.ApplicationContext
2929
import kotlinx.coroutines.CoroutineDispatcher
3030
import kotlinx.coroutines.CoroutineScope
31-
import kotlinx.coroutines.SupervisorJob
3231
import kotlinx.coroutines.async
3332
import kotlinx.coroutines.awaitAll
34-
import kotlinx.coroutines.cancel
3533
import kotlinx.coroutines.coroutineScope
3634
import kotlinx.coroutines.delay
3735
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -50,7 +48,6 @@ import org.lightningdevkit.ldknode.Event
5048
import org.lightningdevkit.ldknode.PaymentId
5149
import org.lightningdevkit.ldknode.SpendableUtxo
5250
import org.lightningdevkit.ldknode.Txid
53-
import to.bitkit.BuildConfig
5451
import to.bitkit.R
5552
import to.bitkit.data.CacheStore
5653
import to.bitkit.data.SettingsStore
@@ -69,7 +66,6 @@ import to.bitkit.ext.maxSendableSat
6966
import to.bitkit.ext.maxWithdrawableSat
7067
import to.bitkit.ext.minSendableSat
7168
import to.bitkit.ext.minWithdrawableSat
72-
import to.bitkit.ext.nowMillis
7369
import to.bitkit.ext.rawId
7470
import to.bitkit.ext.removeSpaces
7571
import to.bitkit.ext.setClipboardText
@@ -97,20 +93,23 @@ import to.bitkit.repositories.LightningRepo
9793
import to.bitkit.repositories.PreActivityMetadataRepo
9894
import to.bitkit.repositories.TransferRepo
9995
import to.bitkit.repositories.WalletRepo
100-
import to.bitkit.services.AppUpdaterService
10196
import to.bitkit.ui.Routes
10297
import to.bitkit.ui.components.Sheet
103-
import to.bitkit.ui.components.TimedSheetType
10498
import to.bitkit.ui.shared.toast.ToastEventBus
10599
import to.bitkit.ui.shared.toast.ToastQueueManager
106100
import to.bitkit.ui.sheets.SendRoute
107101
import to.bitkit.ui.theme.TRANSITION_SCREEN_MS
108102
import to.bitkit.utils.Logger
109103
import 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
110110
import java.math.BigDecimal
111111
import javax.inject.Inject
112112
import kotlin.coroutines.cancellation.CancellationException
113-
import kotlin.time.Clock
114113
import 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

Comments
 (0)