Skip to content

Commit cbc152e

Browse files
authored
Merge pull request #556 from synonymdev/fix/uniffy-timed-sheet-behavior
fix: unify timed sheets behavior
2 parents e6a9de6 + e72d4ce commit cbc152e

19 files changed

+1396
-226
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package to.bitkit.di
2+
3+
import dagger.Module
4+
import dagger.Provides
5+
import dagger.hilt.InstallIn
6+
import dagger.hilt.components.SingletonComponent
7+
import kotlinx.coroutines.CoroutineScope
8+
import to.bitkit.utils.timedsheets.TimedSheetManager
9+
10+
@Module
11+
@InstallIn(SingletonComponent::class)
12+
object TimedSheetModule {
13+
14+
@Provides
15+
fun provideTimedSheetManagerProvider(): (CoroutineScope) -> TimedSheetManager {
16+
return ::TimedSheetManager
17+
}
18+
}

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ fun ContentView(
127127
appViewModel.mainScreenEffect.collect {
128128
when (it) {
129129
is MainScreenEffect.Navigate -> navigator.navigate(it.route)
130+
is MainScreenEffect.NavigateAndClearBackstack -> navigator.navigateAndClearBackstack(it.route)
130131
is MainScreenEffect.ProcessClipboardAutoRead -> {
131132
if (!navigator.isAtHome()) {
132133
navigator.navigateToHome()

app/src/main/java/to/bitkit/ui/nav/Navigator.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ class Navigator(@PublishedApi internal val backStack: NavBackStack<NavKey>) {
5050

5151
fun isAtHome(): Boolean = backStack.lastOrNull() is Routes.Home
5252

53+
fun navigateAndClearBackstack(route: Routes) {
54+
backStack.clear()
55+
backStack.add(route)
56+
}
57+
5358
fun shouldShowTabBar(): Boolean = when (backStack.lastOrNull()) {
5459
is Routes.Home, is Routes.Savings, is Routes.Spending, is Routes.Activity.All -> true
5560
else -> false

app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package to.bitkit.ui.nav.entries
33
import android.content.Intent
44
import androidx.compose.foundation.layout.fillMaxSize
55
import androidx.compose.foundation.layout.navigationBarsPadding
6+
import androidx.compose.runtime.DisposableEffect
67
import androidx.compose.runtime.LaunchedEffect
78
import androidx.compose.runtime.getValue
89
import androidx.compose.runtime.remember
@@ -799,30 +800,33 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
799800
entry<Routes.Sheet.Update>(
800801
metadata = SheetSceneStrategy.sheet()
801802
) {
803+
DisposableEffect(Unit) {
804+
onDispose { appViewModel.dismissTimedSheet() }
805+
}
802806
UpdateSheet(
803-
onCancel = {
804-
appViewModel.dismissTimedSheet()
805-
navigator.goBack()
806-
},
807+
onCancel = { navigator.goBack() },
807808
)
808809
}
809810

810811
entry<Routes.Sheet.Backup>(
811812
metadata = SheetSceneStrategy.sheet()
812813
) {
814+
DisposableEffect(Unit) {
815+
onDispose { appViewModel.dismissTimedSheet() }
816+
}
813817
BackupIntroScreen(
814818
hasFunds = LocalBalances.current.totalSats > 0u,
815-
onClose = {
816-
appViewModel.dismissTimedSheet()
817-
navigator.goBack()
818-
},
819+
onClose = { navigator.goBack() },
819820
onConfirm = { navigator.navigate(Routes.Backup.ShowMnemonic) },
820821
)
821822
}
822823

823824
entry<Routes.Sheet.Notifications>(
824825
metadata = SheetSceneStrategy.sheet()
825826
) {
827+
DisposableEffect(Unit) {
828+
onDispose { appViewModel.dismissTimedSheet() }
829+
}
826830
BackgroundPaymentsIntroSheet(
827831
onContinue = {
828832
appViewModel.dismissTimedSheet(skipQueue = true)
@@ -834,6 +838,9 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
834838
entry<Routes.Sheet.QuickPay>(
835839
metadata = SheetSceneStrategy.sheet()
836840
) {
841+
DisposableEffect(Unit) {
842+
onDispose { appViewModel.dismissTimedSheet() }
843+
}
837844
QuickPayIntroSheet(
838845
onContinue = {
839846
appViewModel.dismissTimedSheet(skipQueue = true)
@@ -845,12 +852,12 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
845852
entry<Routes.Sheet.HighBalance>(
846853
metadata = SheetSceneStrategy.sheet()
847854
) {
855+
DisposableEffect(Unit) {
856+
onDispose { appViewModel.dismissTimedSheet() }
857+
}
848858
val context = LocalContext.current
849859
HighBalanceWarningSheet(
850-
understoodClick = {
851-
appViewModel.dismissTimedSheet()
852-
navigator.goBack()
853-
},
860+
understoodClick = { navigator.goBack() },
854861
learnMoreClick = {
855862
val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri())
856863
context.startActivity(intent)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package to.bitkit.utils.timedsheets
2+
3+
import to.bitkit.ui.components.TimedSheetType
4+
5+
interface TimedSheetItem {
6+
val type: TimedSheetType
7+
val priority: Int
8+
9+
suspend fun shouldShow(): Boolean
10+
suspend fun onShown()
11+
suspend fun onDismissed()
12+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package to.bitkit.utils.timedsheets
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Job
5+
import kotlinx.coroutines.delay
6+
import kotlinx.coroutines.flow.MutableStateFlow
7+
import kotlinx.coroutines.flow.StateFlow
8+
import kotlinx.coroutines.flow.asStateFlow
9+
import kotlinx.coroutines.launch
10+
import to.bitkit.ui.components.TimedSheetType
11+
import to.bitkit.utils.Logger
12+
13+
class TimedSheetManager(private val scope: CoroutineScope) {
14+
private val _currentSheet = MutableStateFlow<TimedSheetType?>(null)
15+
val currentSheet: StateFlow<TimedSheetType?> = _currentSheet.asStateFlow()
16+
17+
private val registeredSheets = mutableListOf<TimedSheetItem>()
18+
private var currentTimedSheet: TimedSheetItem? = null
19+
private var checkJob: Job? = null
20+
21+
fun registerSheet(sheet: TimedSheetItem) {
22+
registeredSheets.add(sheet)
23+
registeredSheets.sortByDescending { it.priority }
24+
Logger.debug(
25+
"Registered timed sheet: ${sheet.type.name} with priority: ${sheet.priority}",
26+
context = TAG
27+
)
28+
}
29+
30+
fun onHomeScreenEntered() {
31+
Logger.debug("User entered home screen, starting timer", context = TAG)
32+
checkJob?.cancel()
33+
checkJob = scope.launch {
34+
delay(CHECK_DELAY_MILLIS)
35+
checkAndShowNextSheet()
36+
}
37+
}
38+
39+
fun onHomeScreenExited() {
40+
Logger.debug("User exited home screen, cancelling timer", context = TAG)
41+
checkJob?.cancel()
42+
checkJob = null
43+
}
44+
45+
fun dismissCurrentSheet(skipQueue: Boolean = false) {
46+
if (currentTimedSheet == null) return
47+
48+
scope.launch {
49+
currentTimedSheet?.onDismissed()
50+
_currentSheet.value = null
51+
currentTimedSheet = null
52+
53+
if (skipQueue) {
54+
Logger.debug("Clearing timed sheet queue", context = TAG)
55+
} else {
56+
delay(CHECK_DELAY_MILLIS)
57+
checkAndShowNextSheet()
58+
}
59+
}
60+
}
61+
62+
private suspend fun checkAndShowNextSheet() {
63+
Logger.debug("Registered sheets: ${registeredSheets.map { it.type.name }}")
64+
for (sheet in registeredSheets.toList()) {
65+
if (sheet.shouldShow()) {
66+
Logger.debug(
67+
"Showing timed sheet: ${sheet.type.name} with priority: ${sheet.priority}",
68+
context = TAG
69+
)
70+
currentTimedSheet = sheet
71+
_currentSheet.value = sheet.type
72+
sheet.onShown()
73+
registeredSheets.remove(sheet)
74+
return
75+
}
76+
}
77+
78+
Logger.debug("No timed sheets need to be shown", context = TAG)
79+
_currentSheet.value = null
80+
currentTimedSheet = null
81+
}
82+
83+
companion object {
84+
private const val TAG = "TimedSheetManager"
85+
private const val CHECK_DELAY_MILLIS = 2000L
86+
}
87+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package to.bitkit.utils.timedsheets
2+
3+
import kotlin.time.Clock
4+
import kotlin.time.ExperimentalTime
5+
6+
@OptIn(ExperimentalTime::class)
7+
fun checkTimeout(
8+
lastIgnoredMillis: Long,
9+
intervalMillis: Long,
10+
additionalCondition: Boolean = true,
11+
): Boolean {
12+
if (!additionalCondition) return false
13+
14+
val currentTime = Clock.System.now().toEpochMilliseconds()
15+
val isTimeOutOver = lastIgnoredMillis == 0L ||
16+
(currentTime - lastIgnoredMillis > intervalMillis)
17+
return isTimeOutOver
18+
}
19+
20+
const val ONE_DAY_ASK_INTERVAL_MILLIS = 1000 * 60 * 60 * 24L
21+
const val ONE_WEEK_ASK_INTERVAL_MILLIS = ONE_DAY_ASK_INTERVAL_MILLIS * 7L
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package to.bitkit.utils.timedsheets.sheets
2+
3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.withContext
5+
import to.bitkit.BuildConfig
6+
import to.bitkit.di.BgDispatcher
7+
import to.bitkit.services.AppUpdaterService
8+
import to.bitkit.ui.components.TimedSheetType
9+
import to.bitkit.utils.Logger
10+
import to.bitkit.utils.timedsheets.TimedSheetItem
11+
import javax.inject.Inject
12+
13+
class AppUpdateTimedSheet @Inject constructor(
14+
private val appUpdaterService: AppUpdaterService,
15+
@BgDispatcher private val bgDispatcher: CoroutineDispatcher,
16+
) : TimedSheetItem {
17+
override val type = TimedSheetType.APP_UPDATE
18+
override val priority = 5
19+
20+
override suspend fun shouldShow(): Boolean = withContext(bgDispatcher) {
21+
return@withContext runCatching {
22+
val androidReleaseInfo = appUpdaterService.getReleaseInfo().platforms.android
23+
val currentBuildNumber = BuildConfig.VERSION_CODE
24+
25+
if (androidReleaseInfo.buildNumber <= currentBuildNumber) return@withContext false
26+
27+
if (androidReleaseInfo.isCritical) {
28+
return@runCatching false
29+
}
30+
return@runCatching true
31+
}.onFailure { e ->
32+
Logger.warn("Failure fetching new releases", e = e, context = TAG)
33+
}.getOrDefault(false)
34+
}
35+
36+
override suspend fun onShown() {
37+
Logger.debug("App update sheet shown", context = TAG)
38+
}
39+
40+
override suspend fun onDismissed() {
41+
Logger.debug("App update sheet dismissed", context = TAG)
42+
}
43+
44+
companion object {
45+
private const val TAG = "AppUpdateTimedSheet"
46+
}
47+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package to.bitkit.utils.timedsheets.sheets
2+
3+
import kotlinx.coroutines.flow.first
4+
import to.bitkit.data.SettingsStore
5+
import to.bitkit.ext.nowMillis
6+
import to.bitkit.repositories.WalletRepo
7+
import to.bitkit.ui.components.TimedSheetType
8+
import to.bitkit.utils.Logger
9+
import to.bitkit.utils.timedsheets.ONE_DAY_ASK_INTERVAL_MILLIS
10+
import to.bitkit.utils.timedsheets.TimedSheetItem
11+
import to.bitkit.utils.timedsheets.checkTimeout
12+
import javax.inject.Inject
13+
import kotlin.time.ExperimentalTime
14+
15+
class BackupTimedSheet @Inject constructor(
16+
private val settingsStore: SettingsStore,
17+
private val walletRepo: WalletRepo,
18+
) : TimedSheetItem {
19+
override val type = TimedSheetType.BACKUP
20+
override val priority = 4
21+
22+
override suspend fun shouldShow(): Boolean {
23+
val settings = settingsStore.data.first()
24+
if (settings.backupVerified) return false
25+
26+
val hasBalance = walletRepo.balanceState.value.totalSats > 0U
27+
if (!hasBalance) return false
28+
29+
return checkTimeout(
30+
lastIgnoredMillis = settings.backupWarningIgnoredMillis,
31+
intervalMillis = ONE_DAY_ASK_INTERVAL_MILLIS
32+
)
33+
}
34+
35+
override suspend fun onShown() {
36+
Logger.debug("Backup sheet shown", context = TAG)
37+
}
38+
39+
@OptIn(ExperimentalTime::class)
40+
override suspend fun onDismissed() {
41+
val currentTime = nowMillis()
42+
settingsStore.update {
43+
it.copy(backupWarningIgnoredMillis = currentTime)
44+
}
45+
Logger.debug("Backup sheet dismissed", context = TAG)
46+
}
47+
48+
companion object {
49+
private const val TAG = "BackupTimedSheet"
50+
}
51+
}

0 commit comments

Comments
 (0)