Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/src/main/java/to/bitkit/di/TimedSheetModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package to.bitkit.di

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import to.bitkit.utils.timedsheets.TimedSheetManager

@Module
@InstallIn(SingletonComponent::class)
object TimedSheetModule {

@Provides
fun provideTimedSheetManagerProvider(): (CoroutineScope) -> TimedSheetManager {
return ::TimedSheetManager
}
}
6 changes: 3 additions & 3 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ fun ContentView(
TimedSheetType.NOTIFICATIONS -> {
BackgroundPaymentsIntroSheet(
onContinue = {
appViewModel.dismissTimedSheet(skipQueue = true)
appViewModel.dismissTimedSheet()
navController.navigate(Routes.BackgroundPaymentsSettings)
settingsViewModel.setBgPaymentsIntroSeen(true)
},
Expand All @@ -419,7 +419,7 @@ fun ContentView(
TimedSheetType.QUICK_PAY -> {
QuickPayIntroSheet(
onContinue = {
appViewModel.dismissTimedSheet(skipQueue = true)
appViewModel.dismissTimedSheet()
navController.navigate(Routes.QuickPaySettings)
},
)
Expand All @@ -432,7 +432,7 @@ fun ContentView(
val intent =
Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri())
context.startActivity(intent)
appViewModel.dismissTimedSheet(skipQueue = true)
appViewModel.dismissTimedSheet()
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,6 @@ private fun TermsText(
}
}


@Preview(showSystemUi = true)
@Composable
private fun TermsPreview() {
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/to/bitkit/utils/timedsheets/TimedSheetItem.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package to.bitkit.utils.timedsheets

import to.bitkit.ui.components.TimedSheetType

interface TimedSheetItem {
val type: TimedSheetType
val priority: Int

suspend fun shouldShow(): Boolean
suspend fun onShown()
suspend fun onDismissed()
}
82 changes: 82 additions & 0 deletions app/src/main/java/to/bitkit/utils/timedsheets/TimedSheetManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package to.bitkit.utils.timedsheets

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import to.bitkit.ui.components.TimedSheetType
import to.bitkit.utils.Logger

class TimedSheetManager(private val scope: CoroutineScope) {
private val _currentSheet = MutableStateFlow<TimedSheetType?>(null)
val currentSheet: StateFlow<TimedSheetType?> = _currentSheet.asStateFlow()

private val registeredSheets = mutableListOf<TimedSheetItem>()
private var currentTimedSheet: TimedSheetItem? = null
private var checkJob: Job? = null

fun registerSheet(sheet: TimedSheetItem) {
registeredSheets.add(sheet)
registeredSheets.sortByDescending { it.priority }
Logger.debug(
"Registered timed sheet: ${sheet.type.name} with priority: ${sheet.priority}",
context = TAG
)
}

fun onHomeScreenEntered() {
Logger.debug("User entered home screen, starting timer", context = TAG)
checkJob?.cancel()
checkJob = scope.launch {
delay(CHECK_DELAY_MILLIS)
checkAndShowNextSheet()
}
}

fun onHomeScreenExited() {
Logger.debug("User exited home screen, cancelling timer", context = TAG)
checkJob?.cancel()
checkJob = null
}

fun dismissCurrentSheet() {
if (currentTimedSheet == null) return

scope.launch {
currentTimedSheet?.onDismissed()
_currentSheet.value = null
currentTimedSheet = null

Logger.debug("Clearing timed sheet queue", context = TAG)
}
}

private suspend fun checkAndShowNextSheet() {
Logger.debug("Registered sheets: ${registeredSheets.map { it.type.name }}")
for (sheet in registeredSheets.toList()) {
if (sheet.shouldShow()) {
Logger.debug(
"Showing timed sheet: ${sheet.type.name} with priority: ${sheet.priority}",
context = TAG
)
currentTimedSheet = sheet
_currentSheet.value = sheet.type
sheet.onShown()
registeredSheets.remove(sheet)
return
}
}

Logger.debug("No timed sheets need to be shown", context = TAG)
_currentSheet.value = null
currentTimedSheet = null
}

companion object {
private const val TAG = "TimedSheetManager"
private const val CHECK_DELAY_MILLIS = 2000L
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/to/bitkit/utils/timedsheets/TimedSheetUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package to.bitkit.utils.timedsheets

import kotlin.time.Clock
import kotlin.time.ExperimentalTime

@OptIn(ExperimentalTime::class)
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
}

const val ONE_DAY_ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24L
const val ONE_WEEK_ASK_INTERVAL_MILLIS = ONE_DAY_ASK_INTERVAL_MILLIS * 7L
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package to.bitkit.utils.timedsheets.sheets

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import to.bitkit.BuildConfig
import to.bitkit.di.BgDispatcher
import to.bitkit.services.AppUpdaterService
import to.bitkit.ui.components.TimedSheetType
import to.bitkit.utils.Logger
import to.bitkit.utils.timedsheets.TimedSheetItem
import javax.inject.Inject

class AppUpdateTimedSheet @Inject constructor(
private val appUpdaterService: AppUpdaterService,
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
) : TimedSheetItem {
override val type = TimedSheetType.APP_UPDATE
override val priority = 5

override suspend fun shouldShow(): 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) {
return@withContext false
}

return@withContext true
} catch (e: Exception) {
Logger.warn("Failure fetching new releases", e = e, context = TAG)
return@withContext false
}
}

override suspend fun onShown() {
Logger.debug("App update sheet shown", context = TAG)
}

override suspend fun onDismissed() {
Logger.debug("App update sheet dismissed", context = TAG)
}

companion object {
private const val TAG = "AppUpdateTimedSheet"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package to.bitkit.utils.timedsheets.sheets

import kotlinx.coroutines.flow.first
import to.bitkit.data.SettingsStore
import to.bitkit.ext.nowMillis
import to.bitkit.repositories.WalletRepo
import to.bitkit.ui.components.TimedSheetType
import to.bitkit.utils.Logger
import to.bitkit.utils.timedsheets.ONE_DAY_ASK_INTERVAL_MILLIS
import to.bitkit.utils.timedsheets.TimedSheetItem
import to.bitkit.utils.timedsheets.checkTimeout
import javax.inject.Inject
import kotlin.time.ExperimentalTime

class BackupTimedSheet @Inject constructor(
private val settingsStore: SettingsStore,
private val walletRepo: WalletRepo,
) : TimedSheetItem {
override val type = TimedSheetType.BACKUP
override val priority = 4

override suspend fun shouldShow(): 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
)
}

override suspend fun onShown() {
Logger.debug("Backup sheet shown", context = TAG)
}

@OptIn(ExperimentalTime::class)
override suspend fun onDismissed() {
val currentTime = nowMillis()
settingsStore.update {
it.copy(backupWarningIgnoredMillis = currentTime)
}
Logger.debug("Backup sheet dismissed", context = TAG)
}

companion object {
private const val TAG = "BackupTimedSheet"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package to.bitkit.utils.timedsheets.sheets

import kotlinx.coroutines.flow.first
import to.bitkit.data.SettingsStore
import to.bitkit.ext.nowMillis
import to.bitkit.repositories.CurrencyRepo
import to.bitkit.repositories.WalletRepo
import to.bitkit.ui.components.TimedSheetType
import to.bitkit.utils.Logger
import to.bitkit.utils.timedsheets.ONE_DAY_ASK_INTERVAL_MILLIS
import to.bitkit.utils.timedsheets.TimedSheetItem
import to.bitkit.utils.timedsheets.checkTimeout
import java.math.BigDecimal
import javax.inject.Inject
import kotlin.time.ExperimentalTime

class HighBalanceTimedSheet @Inject constructor(
private val settingsStore: SettingsStore,
private val walletRepo: WalletRepo,
private val currencyRepo: CurrencyRepo,
) : TimedSheetItem {
override val type = TimedSheetType.HIGH_BALANCE
override val priority = 1

override suspend fun shouldShow(): 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
)
}

override suspend fun onShown() {
Logger.debug("High balance sheet shown", context = TAG)
}

@OptIn(ExperimentalTime::class)
override suspend fun onDismissed() {
val currentTime = nowMillis()
settingsStore.update {
it.copy(
balanceWarningTimes = it.balanceWarningTimes + 1,
balanceWarningIgnoredMillis = currentTime,
)
}
Logger.debug("High balance sheet dismissed", context = TAG)
}

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 = "HighBalanceTimedSheet"
private const val BALANCE_THRESHOLD_USD = 500L
private const val MAX_WARNINGS = 3
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package to.bitkit.utils.timedsheets.sheets

import kotlinx.coroutines.flow.first
import to.bitkit.data.SettingsStore
import to.bitkit.ext.nowMillis
import to.bitkit.repositories.WalletRepo
import to.bitkit.ui.components.TimedSheetType
import to.bitkit.utils.Logger
import to.bitkit.utils.timedsheets.ONE_WEEK_ASK_INTERVAL_MILLIS
import to.bitkit.utils.timedsheets.TimedSheetItem
import to.bitkit.utils.timedsheets.checkTimeout
import javax.inject.Inject
import kotlin.time.ExperimentalTime

class NotificationsTimedSheet @Inject constructor(
private val settingsStore: SettingsStore,
private val walletRepo: WalletRepo,
) : TimedSheetItem {
override val type = TimedSheetType.NOTIFICATIONS
override val priority = 3

override suspend fun shouldShow(): 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
)
}

override suspend fun onShown() {
Logger.debug("Notifications sheet shown", context = TAG)
}

@OptIn(ExperimentalTime::class)
override suspend fun onDismissed() {
val currentTime = nowMillis()
settingsStore.update {
it.copy(notificationsIgnoredMillis = currentTime)
}
Logger.debug("Notifications sheet dismissed", context = TAG)
}

companion object {
private const val TAG = "NotificationsTimedSheet"
}
}
Loading
Loading