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
112 changes: 112 additions & 0 deletions app/src/androidTest/java/to/bitkit/ui/RootDestinationTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package to.bitkit.ui

import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import org.junit.Rule
import org.junit.Test
import to.bitkit.test.annotations.ComposeUi

@ComposeUi
class RootDestinationTest {

@get:Rule
val composeTestRule = createComposeRule()

private fun setContent(
isCriticalUpdateRequired: Boolean = false,
isShowingMigrationLoading: Boolean = false,
walletExists: Boolean = true,
isRecoveryMode: Boolean = false,
) {
composeTestRule.setContent {
RootDestination(
isCriticalUpdateRequired = isCriticalUpdateRequired,
isShowingMigrationLoading = isShowingMigrationLoading,
walletExists = walletExists,
isRecoveryMode = isRecoveryMode,
criticalUpdate = { Text(text = "critical", modifier = Modifier.testTag(TAG_CRITICAL)) },
migrationLoading = { Text(text = "migration", modifier = Modifier.testTag(TAG_MIGRATION)) },
onboarding = { Text(text = "onboarding", modifier = Modifier.testTag(TAG_ONBOARDING)) },
wallet = { Text(text = "wallet", modifier = Modifier.testTag(TAG_WALLET)) },
)
}
}

private fun assertOnly(tag: String) {
composeTestRule.onNodeWithTag(tag).assertExists()
listOf(TAG_CRITICAL, TAG_MIGRATION, TAG_ONBOARDING, TAG_WALLET)
.filterNot { it == tag }
.forEach { composeTestRule.onNodeWithTag(it).assertDoesNotExist() }
}

@Test
fun whenCriticalUpdateRequiredDuringMigration_shouldShowCriticalUpdateOnly() {
setContent(isCriticalUpdateRequired = true, isShowingMigrationLoading = true)

assertOnly(TAG_CRITICAL)
}

@Test
fun whenCriticalUpdateRequiredWithoutWallet_shouldShowCriticalUpdateInsteadOfOnboarding() {
setContent(isCriticalUpdateRequired = true, walletExists = false)

assertOnly(TAG_CRITICAL)
}

@Test
fun whenCriticalUpdateRequiredInRecoveryMode_shouldShowCriticalUpdateInsteadOfWallet() {
setContent(isCriticalUpdateRequired = true, walletExists = false, isRecoveryMode = true)

assertOnly(TAG_CRITICAL)
}

@Test
fun whenCriticalUpdateRequiredWithWallet_shouldShowCriticalUpdateInsteadOfWallet() {
setContent(isCriticalUpdateRequired = true)

assertOnly(TAG_CRITICAL)
}

@Test
fun whenMigrationLoadingWithoutCriticalUpdate_shouldShowMigrationOnly() {
setContent(isShowingMigrationLoading = true, walletExists = false)

assertOnly(TAG_MIGRATION)
}

@Test
fun whenMigrationLoadingInRecoveryMode_shouldShowWallet() {
setContent(isShowingMigrationLoading = true, isRecoveryMode = true)

assertOnly(TAG_WALLET)
}

@Test
fun whenNoWalletAndNoCriticalUpdate_shouldShowOnboarding() {
setContent(walletExists = false)

assertOnly(TAG_ONBOARDING)
}

@Test
fun whenNoWalletInRecoveryMode_shouldShowWallet() {
setContent(walletExists = false, isRecoveryMode = true)

assertOnly(TAG_WALLET)
}

@Test
fun whenWalletExistsAndNothingBlocks_shouldShowWallet() {
setContent()

assertOnly(TAG_WALLET)
}
}

private const val TAG_CRITICAL = "TestCriticalUpdate"
private const val TAG_MIGRATION = "TestMigrationLoading"
private const val TAG_ONBOARDING = "TestOnboarding"
private const val TAG_WALLET = "TestWallet"
11 changes: 0 additions & 11 deletions app/src/main/java/to/bitkit/ui/ContentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ import to.bitkit.ui.components.TimedSheetType
import to.bitkit.ui.onboarding.InitializingWalletView
import to.bitkit.ui.onboarding.WalletRestoreErrorView
import to.bitkit.ui.onboarding.WalletRestoreSuccessView
import to.bitkit.ui.screens.CriticalUpdateScreen
import to.bitkit.ui.screens.common.ComingSoonScreen
import to.bitkit.ui.screens.contacts.AddContactScreen
import to.bitkit.ui.screens.contacts.AddContactViewModel
Expand Down Expand Up @@ -901,7 +900,6 @@ private fun RootNavHost(
appViewModel = appViewModel,
onNavigateHomeWidgets = onNavigateHomeWidgets,
)
update()
recoveryMode(navController, appViewModel)

// TODO extract transferNavigation
Expand Down Expand Up @@ -1918,12 +1916,6 @@ private fun NavGraphBuilder.suggestions(
}
}

private fun NavGraphBuilder.update() {
composableWithDefaultTransitions<Routes.CriticalUpdate> {
CriticalUpdateScreen()
}
}

private fun NavGraphBuilder.recoveryMode(
navController: NavHostController,
appViewModel: AppViewModel,
Expand Down Expand Up @@ -2469,9 +2461,6 @@ sealed interface Routes {
@Serializable
data object AppStatus : Routes.DeepLinkable

@Serializable
data object CriticalUpdate : Routes.InternalOnly

@Serializable
data object RecoveryMode : Routes.InternalOnly

Expand Down
148 changes: 98 additions & 50 deletions app/src/main/java/to/bitkit/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand All @@ -23,6 +24,9 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.core.content.IntentCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
Expand Down Expand Up @@ -53,6 +57,7 @@ import to.bitkit.ui.onboarding.OnboardingSlidesScreen
import to.bitkit.ui.onboarding.RestoreWalletScreen
import to.bitkit.ui.onboarding.TermsOfUseScreen
import to.bitkit.ui.onboarding.WarningMultipleDevicesScreen
import to.bitkit.ui.screens.CriticalUpdateScreen
import to.bitkit.ui.screens.MigrationLoadingScreen
import to.bitkit.ui.screens.SplashScreen
import to.bitkit.ui.sheets.ForgotPinSheet
Expand Down Expand Up @@ -126,6 +131,7 @@ class MainActivity : FragmentActivity() {
val walletExists = walletViewModel.walletExists
val isShowingMigrationLoading by walletViewModel.isShowingMigrationLoading.collectAsStateWithLifecycle()
val restoreState by walletViewModel.restoreState.collectAsStateWithLifecycle()
val isCriticalUpdateRequired by appViewModel.isCriticalUpdateRequired.collectAsStateWithLifecycle()
val hazeState = rememberHazeState(blurEnabled = true)
val bottomSheetOverlayState = remember { BottomSheetOverlayState() }
val authSheetOverlayState = remember { BottomSheetOverlayState() }
Expand All @@ -145,67 +151,90 @@ class MainActivity : FragmentActivity() {
}
}

if (isShowingMigrationLoading && !isRecoveryMode) {
MigrationLoadingScreen(isVisible = true)
} else if (!walletViewModel.walletExists && !isRecoveryMode) {
OnboardingNav(
startupNavController = rememberNavController(),
scope = scope,
appViewModel = appViewModel,
walletViewModel = walletViewModel,
)
} else {
val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle()

IsOnlineTracker(appViewModel)
ContentView(
appViewModel = appViewModel,
walletViewModel = walletViewModel,
blocktankViewModel = blocktankViewModel,
currencyViewModel = currencyViewModel,
activityListViewModel = activityListViewModel,
transferViewModel = transferViewModel,
settingsViewModel = settingsViewModel,
backupsViewModel = backupsViewModel,
hazeState = hazeState,
bottomSheetOverlayState = bottomSheetOverlayState,
modifier = Modifier.hazeSource(hazeState, zIndex = 0f),
)
RootDestination(
isCriticalUpdateRequired = isCriticalUpdateRequired,
isShowingMigrationLoading = isShowingMigrationLoading,
walletExists = walletExists,
isRecoveryMode = isRecoveryMode,
criticalUpdate = {
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle, walletExists, isRecoveryMode, notificationsGranted, keepActive) {
val observer = LifecycleEventObserver { _, event ->
if (event != Lifecycle.Event.ON_STOP) return@LifecycleEventObserver
val keptAliveByService = notificationsGranted &&
keepActive &&
appViewModel.isForegroundServiceRunning()
if (walletExists && !isRecoveryMode && !keptAliveByService) {
walletViewModel.stop()
}
}
lifecycle.addObserver(observer)
onDispose { lifecycle.removeObserver(observer) }
}
CriticalUpdateScreen()
},
migrationLoading = { MigrationLoadingScreen(isVisible = true) },
onboarding = {
OnboardingNav(
startupNavController = rememberNavController(),
scope = scope,
appViewModel = appViewModel,
walletViewModel = walletViewModel,
)
},
wallet = {
val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle()

AnimatedVisibility(
visible = !isAuthenticated,
enter = fadeIn(),
exit = fadeOut(),
) {
AuthCheckView(
showLogoOnPin = true,
IsOnlineTracker(appViewModel)
ContentView(
appViewModel = appViewModel,
walletViewModel = walletViewModel,
blocktankViewModel = blocktankViewModel,
currencyViewModel = currencyViewModel,
activityListViewModel = activityListViewModel,
transferViewModel = transferViewModel,
settingsViewModel = settingsViewModel,
onSuccess = { appViewModel.setIsAuthenticated(true) },
backupsViewModel = backupsViewModel,
hazeState = hazeState,
bottomSheetOverlayState = bottomSheetOverlayState,
modifier = Modifier.hazeSource(hazeState, zIndex = 0f),
)
}

val showForgotPinSheet by appViewModel.showForgotPinSheet.collectAsStateWithLifecycle()
if (showForgotPinSheet) {
CompositionLocalProvider(LocalBottomSheetOverlayState provides authSheetOverlayState) {
ForgotPinSheet(
onDismiss = { appViewModel.setShowForgotPin(false) },
onResetClick = { walletViewModel.wipeWallet() },
AnimatedVisibility(
visible = !isAuthenticated,
enter = fadeIn(),
exit = fadeOut(),
) {
AuthCheckView(
showLogoOnPin = true,
appViewModel = appViewModel,
settingsViewModel = settingsViewModel,
onSuccess = { appViewModel.setIsAuthenticated(true) },
)
}
}

BottomSheetOverlayHost(state = authSheetOverlayState)
val showForgotPinSheet by appViewModel.showForgotPinSheet.collectAsStateWithLifecycle()
if (showForgotPinSheet) {
CompositionLocalProvider(LocalBottomSheetOverlayState provides authSheetOverlayState) {
ForgotPinSheet(
onDismiss = { appViewModel.setShowForgotPin(false) },
onResetClick = { walletViewModel.wipeWallet() },
)
}
}

BottomSheetOverlayHost(state = authSheetOverlayState)

LaunchedEffect(appViewModel) {
appViewModel.mainScreenEffect.collect {
when (it) {
MainScreenEffect.WipeWallet -> walletViewModel.wipeWallet()
else -> Unit
LaunchedEffect(appViewModel) {
appViewModel.mainScreenEffect.collect {
when (it) {
MainScreenEffect.WipeWallet -> walletViewModel.wipeWallet()
else -> Unit
}
}
}
}
}
},
)

val transactionSheetDetails by appViewModel.transactionSheet.collectAsStateWithLifecycle()
if (transactionSheetDetails != NewTransactionSheetDetails.EMPTY) {
Expand Down Expand Up @@ -331,6 +360,25 @@ class MainActivity : FragmentActivity() {
}
}

@Composable
fun RootDestination(
isCriticalUpdateRequired: Boolean,
isShowingMigrationLoading: Boolean,
walletExists: Boolean,
isRecoveryMode: Boolean,
criticalUpdate: @Composable () -> Unit,
migrationLoading: @Composable () -> Unit,
onboarding: @Composable () -> Unit,
wallet: @Composable () -> Unit,
) {
when {
isCriticalUpdateRequired -> criticalUpdate()
isShowingMigrationLoading && !isRecoveryMode -> migrationLoading()
!walletExists && !isRecoveryMode -> onboarding()
else -> wallet()
}
}

internal fun Intent?.launchKey(): String? {
this ?: return null
return when (action) {
Expand Down
Loading
Loading