Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
77c3848
fix: remove check on balance change
jvsena42 Dec 23, 2025
84a9085
chore: log context tag
jvsena42 Dec 23, 2025
9c558a2
refactor: move timed sheet logic to specific classes
jvsena42 Dec 24, 2025
0ed3260
test: timed sheet tests
jvsena42 Dec 24, 2025
b7879fd
refactor: replace timed sheet logic with manager
jvsena42 Dec 24, 2025
6e99ee1
fix: reimplement critical update logic
jvsena42 Dec 24, 2025
4e13983
fix: add two seconds delay
jvsena42 Dec 24, 2025
a72fc22
refactor: Migrate navigation to nav3
ovitrif Dec 23, 2025
06d5091
chore: Update deps
ovitrif Dec 24, 2025
4fe4ecc
Merge branch 'feat/nav3' into fix/uniffy-timed-sheet-behavior
jvsena42 Dec 24, 2025
a110582
Merge remote-tracking branch 'origin/fix/uniffy-timed-sheet-behavior'…
jvsena42 Dec 24, 2025
920aa1d
refactor: Migrate navigation to nav3
ovitrif Dec 23, 2025
5b5fe6e
chore: Update deps
ovitrif Dec 24, 2025
6059092
Merge remote-tracking branch 'origin/feat/nav3' into feat/nav3
jvsena42 Dec 24, 2025
eabe2c0
Merge branch 'feat/nav3' into fix/uniffy-timed-sheet-behavior
jvsena42 Dec 24, 2025
697bf90
refactor: Migrate navigation to nav3
ovitrif Dec 23, 2025
e73fbef
chore: Update deps
ovitrif Dec 24, 2025
a66ffe6
Merge remote-tracking branch 'origin/feat/nav3' into feat/nav3
jvsena42 Dec 24, 2025
b25be02
Merge branch 'feat/nav3' into fix/uniffy-timed-sheet-behavior
jvsena42 Dec 24, 2025
63897cf
fix: sheet navigation
jvsena42 Dec 24, 2025
989602d
fix: detect when the sheet is dismissed by swipe down
jvsena42 Dec 24, 2025
9bd5cc6
fix: return early if sheet is null
jvsena42 Dec 24, 2025
b37efac
fix: text log
jvsena42 Dec 24, 2025
2f1b45d
chore: lint
jvsena42 Dec 24, 2025
89dd6e7
refactor: replace try catch with runCatching
jvsena42 Dec 26, 2025
2f0b00c
fix: add transition delay
jvsena42 Dec 26, 2025
ba10eb3
fix: clear backstack on critical update navigation
jvsena42 Dec 26, 2025
ad97ef0
chore: remove redundant drop
jvsena42 Dec 26, 2025
8fe29b9
Merge branch 'feat/nav3' into fix/uniffy-timed-sheet-behavior
jvsena42 Dec 29, 2025
fe8b2d9
fix: detect dismiss by swipe down
jvsena42 Dec 29, 2025
b2e8b70
fix: clear backstack on critical update navigation
jvsena42 Dec 29, 2025
e72d4ce
Merge branch 'feat/nav3' into fix/uniffy-timed-sheet-behavior
jvsena42 Dec 29, 2025
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
}
}
1 change: 1 addition & 0 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fun ContentView(
appViewModel.mainScreenEffect.collect {
when (it) {
is MainScreenEffect.Navigate -> navigator.navigate(it.route)
is MainScreenEffect.NavigateAndClearBackstack -> navigator.navigateAndClearBackstack(it.route)
is MainScreenEffect.ProcessClipboardAutoRead -> {
if (!navigator.isAtHome()) {
navigator.navigateToHome()
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/to/bitkit/ui/nav/Navigator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ class Navigator(@PublishedApi internal val backStack: NavBackStack<NavKey>) {

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

fun navigateAndClearBackstack(route: Routes) {
backStack.clear()
backStack.add(route)
}

fun shouldShowTabBar(): Boolean = when (backStack.lastOrNull()) {
is Routes.Home, is Routes.Savings, is Routes.Spending, is Routes.Activity.All -> true
else -> false
Expand Down
31 changes: 19 additions & 12 deletions app/src/main/java/to/bitkit/ui/nav/entries/SheetEntries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package to.bitkit.ui.nav.entries
import android.content.Intent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand Down Expand Up @@ -799,30 +800,33 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
entry<Routes.Sheet.Update>(
metadata = SheetSceneStrategy.sheet()
) {
DisposableEffect(Unit) {
onDispose { appViewModel.dismissTimedSheet() }
}
UpdateSheet(
onCancel = {
appViewModel.dismissTimedSheet()
navigator.goBack()
},
onCancel = { navigator.goBack() },
)
}

entry<Routes.Sheet.Backup>(
metadata = SheetSceneStrategy.sheet()
) {
DisposableEffect(Unit) {
onDispose { appViewModel.dismissTimedSheet() }
}
BackupIntroScreen(
hasFunds = LocalBalances.current.totalSats > 0u,
onClose = {
appViewModel.dismissTimedSheet()
navigator.goBack()
},
onClose = { navigator.goBack() },
onConfirm = { navigator.navigate(Routes.Backup.ShowMnemonic) },
)
}

entry<Routes.Sheet.Notifications>(
metadata = SheetSceneStrategy.sheet()
) {
DisposableEffect(Unit) {
onDispose { appViewModel.dismissTimedSheet() }
}
BackgroundPaymentsIntroSheet(
onContinue = {
appViewModel.dismissTimedSheet(skipQueue = true)
Expand All @@ -834,6 +838,9 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
entry<Routes.Sheet.QuickPay>(
metadata = SheetSceneStrategy.sheet()
) {
DisposableEffect(Unit) {
onDispose { appViewModel.dismissTimedSheet() }
}
QuickPayIntroSheet(
onContinue = {
appViewModel.dismissTimedSheet(skipQueue = true)
Expand All @@ -845,12 +852,12 @@ private fun EntryProviderScope<NavKey>.sheetFlowEntries(
entry<Routes.Sheet.HighBalance>(
metadata = SheetSceneStrategy.sheet()
) {
DisposableEffect(Unit) {
onDispose { appViewModel.dismissTimedSheet() }
}
val context = LocalContext.current
HighBalanceWarningSheet(
understoodClick = {
appViewModel.dismissTimedSheet()
navigator.goBack()
},
understoodClick = { navigator.goBack() },
learnMoreClick = {
val intent = Intent(Intent.ACTION_VIEW, Env.STORING_BITCOINS_URL.toUri())
context.startActivity(intent)
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()
}
87 changes: 87 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,87 @@
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(skipQueue: Boolean = false) {
if (currentTimedSheet == null) return

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

if (skipQueue) {
Logger.debug("Clearing timed sheet queue", context = TAG)
} else {
delay(CHECK_DELAY_MILLIS)
checkAndShowNextSheet()
}
}
}

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,47 @@
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) {
return@withContext runCatching {
val androidReleaseInfo = appUpdaterService.getReleaseInfo().platforms.android
val currentBuildNumber = BuildConfig.VERSION_CODE

if (androidReleaseInfo.buildNumber <= currentBuildNumber) return@withContext false

if (androidReleaseInfo.isCritical) {
return@runCatching false
}
return@runCatching true
}.onFailure { e ->
Logger.warn("Failure fetching new releases", e = e, context = TAG)
}.getOrDefault(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"
}
}
Loading
Loading