Skip to content

Commit

Permalink
Show Welcome Tutorial on first launch (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
SaintPatrck authored Apr 15, 2024
1 parent b6a165a commit 12e5314
Show file tree
Hide file tree
Showing 21 changed files with 766 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ interface SettingsDiskSource {
*/
val isIconLoadingDisabledFlow: Flow<Boolean?>

/**
* Tracks whether user has seen the Welcome tutorial.
*/
var hasSeenWelcomeTutorial: Boolean

/**
* Emits update that track [hasSeenWelcomeTutorial]
*/
val hasSeenWelcomeTutorialFlow: Flow<Boolean>

/**
* Stores the threshold at which users are alerted that an items validity period is nearing
* expiration.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ private const val SCREEN_CAPTURE_ALLOW_KEY = "$BASE_KEY:screenCaptureAllowed"
private const val ACCOUNT_BIOMETRIC_INTEGRITY_VALID_KEY = "$BASE_KEY:accountBiometricIntegrityValid"
private const val ALERT_THRESHOLD_SECONDS_KEY = "$BASE_KEY:alertThresholdSeconds"
private const val DISABLE_ICON_LOADING_KEY = "$BASE_KEY:disableFavicon"
private const val FIRST_LAUNCH_KEY = "$BASE_KEY:hasSeenWelcomeTutorial"

/**
* Primary implementation of [SettingsDiskSource].
Expand Down Expand Up @@ -47,6 +48,9 @@ class SettingsDiskSourceImpl(
)
}

private val mutableFirstLaunchFlow =
bufferedMutableSharedFlow<Boolean>()

override var appTheme: AppTheme
get() = getString(key = APP_THEME_KEY)
?.let { storedValue ->
Expand Down Expand Up @@ -76,6 +80,16 @@ class SettingsDiskSourceImpl(
get() = mutableIsIconLoadingDisabledFlow
.onSubscription { emit(getBoolean(DISABLE_ICON_LOADING_KEY)) }

override var hasSeenWelcomeTutorial: Boolean
get() = getBoolean(key = FIRST_LAUNCH_KEY) ?: false
set(value) {
putBoolean(key = FIRST_LAUNCH_KEY, value)
mutableFirstLaunchFlow.tryEmit(hasSeenWelcomeTutorial)
}

override val hasSeenWelcomeTutorialFlow: Flow<Boolean>
get() = mutableFirstLaunchFlow.onSubscription { emit(hasSeenWelcomeTutorial) }

override fun storeAlertThresholdSeconds(thresholdSeconds: Int) {
putInt(
ALERT_THRESHOLD_SECONDS_KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,14 @@ interface SettingsRepository {
* Emits updates that track the [isIconLoadingDisabled] value.
*/
val isIconLoadingDisabledFlow: Flow<Boolean>

/**
* Whether the user has seen the Welcome tutorial.
*/
var hasSeenWelcomeTutorial: Boolean

/**
* Tracks whether the user has seen the Welcome tutorial.
*/
val hasSeenWelcomeTutorialFlow: StateFlow<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,18 @@ class SettingsRepositoryImpl(
.isIconLoadingDisabled
?: false,
)
override var hasSeenWelcomeTutorial: Boolean
get() = settingsDiskSource.hasSeenWelcomeTutorial
set(value) {
settingsDiskSource.hasSeenWelcomeTutorial = value
}

override val hasSeenWelcomeTutorialFlow: StateFlow<Boolean>
get() = settingsDiskSource
.hasSeenWelcomeTutorialFlow
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = hasSeenWelcomeTutorial,
)
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.authenticator

import android.widget.Toast
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.navigation
import com.x8bit.bitwarden.authenticator.R
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.edititem.editItemDestination
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.edititem.navigateToEditItem
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingDestination
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.itemListingGraph
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry.manualCodeEntryDestination
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.manualcodeentry.navigateToManualCodeEntryScreen
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.navbar.AUTHENTICATOR_NAV_BAR_ROUTE
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.navbar.authenticatorNavBarDestination
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.navigateToQrCodeScanScreen
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.qrCodeScanDestination
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.search.navigateToSearch
import com.x8bit.bitwarden.authenticator.ui.platform.feature.tutorial.navigateToTutorial

const val AUTHENTICATOR_GRAPH_ROUTE = "authenticator_graph"

Expand All @@ -42,6 +37,7 @@ fun NavGraphBuilder.authenticatorGraph(
onNavigateToQrCodeScanner = { navController.navigateToQrCodeScanScreen() },
onNavigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() },
onNavigateToEditItem = { navController.navigateToEditItem(itemId = it) },
onNavigateToTutorial = { navController.navigateToTutorial() },
)
itemListingGraph(
navController = navController,
Expand All @@ -56,7 +52,8 @@ fun NavGraphBuilder.authenticatorGraph(
},
navigateToEditItem = {
navController.navigateToEditItem(itemId = it)
}
},
navigateToTutorial = { navController.navigateToTutorial() },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.manualcodeentr
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.navigateToQrCodeScanScreen
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.qrcodescan.qrCodeScanDestination
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.search.itemSearchDestination
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.search.navigateToSearch
import com.x8bit.bitwarden.authenticator.ui.platform.feature.settings.settingsGraph

const val ITEM_LISTING_GRAPH_ROUTE = "item_listing_graph"
Expand All @@ -24,6 +23,7 @@ fun NavGraphBuilder.itemListingGraph(
navigateToQrCodeScanner: () -> Unit,
navigateToManualKeyEntry: () -> Unit,
navigateToEditItem: (String) -> Unit,
navigateToTutorial: () -> Unit,
) {
navigation(
route = ITEM_LISTING_GRAPH_ROUTE,
Expand Down Expand Up @@ -60,7 +60,10 @@ fun NavGraphBuilder.itemListingGraph(
navController.navigateToQrCodeScanScreen()
}
)
settingsGraph(navController)
settingsGraph(
navController = navController,
onNavigateToTutorial = navigateToTutorial
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToQrCodeScanner: () -> Unit,
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToEditItem: (itemId: String) -> Unit,
onNavigateToTutorial: () -> Unit,
) {
composableWithStayTransitions(
route = AUTHENTICATOR_NAV_BAR_ROUTE,
Expand All @@ -22,6 +23,7 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToManualKeyEntry = onNavigateToManualKeyEntry,
onNavigateToEditItem = onNavigateToEditItem,
onNavigateToSearch = onNavigateToSearch,
onNavigateToTutorial = onNavigateToTutorial,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fun AuthenticatorNavBarScreen(
onNavigateToQrCodeScanner: () -> Unit,
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToEditItem: (itemId: String) -> Unit,
onNavigateToTutorial: () -> Unit,
) {
EventsEffect(viewModel = viewModel) { event ->
navController.apply {
Expand Down Expand Up @@ -108,6 +109,7 @@ fun AuthenticatorNavBarScreen(
navigateToQrCodeScanner = onNavigateToQrCodeScanner,
navigateToManualKeyEntry = onNavigateToManualKeyEntry,
navigateToEditItem = onNavigateToEditItem,
navigateToTutorial = onNavigateToTutorial,
)
}

Expand All @@ -121,6 +123,7 @@ private fun AuthenticatorNavBarScaffold(
navigateToQrCodeScanner: () -> Unit,
navigateToManualKeyEntry: () -> Unit,
navigateToEditItem: (itemId: String) -> Unit,
navigateToTutorial: () -> Unit,
) {
BitwardenScaffold(
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.statusBars),
Expand Down Expand Up @@ -166,6 +169,7 @@ private fun AuthenticatorNavBarScaffold(
navigateToQrCodeScanner = navigateToQrCodeScanner,
navigateToManualKeyEntry = navigateToManualKeyEntry,
navigateToEditItem = navigateToEditItem,
navigateToTutorial = navigateToTutorial,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.authenticator.
import com.x8bit.bitwarden.authenticator.ui.platform.feature.splash.SPLASH_ROUTE
import com.x8bit.bitwarden.authenticator.ui.platform.feature.splash.navigateToSplash
import com.x8bit.bitwarden.authenticator.ui.platform.feature.splash.splashDestination
import com.x8bit.bitwarden.authenticator.ui.platform.feature.tutorial.TUTORIAL_ROUTE
import com.x8bit.bitwarden.authenticator.ui.platform.feature.tutorial.navigateToTutorial
import com.x8bit.bitwarden.authenticator.ui.platform.feature.tutorial.tutorialDestination
import com.x8bit.bitwarden.authenticator.ui.platform.theme.NonNullEnterTransitionProvider
import com.x8bit.bitwarden.authenticator.ui.platform.theme.NonNullExitTransitionProvider
import com.x8bit.bitwarden.authenticator.ui.platform.theme.RootTransitionProviders
Expand Down Expand Up @@ -62,12 +65,16 @@ fun RootNavScreen(
popExitTransition = { toExitTransition()(this) },
) {
splashDestination()
tutorialDestination(
onTutorialFinished = { navController.navigateToAuthenticatorGraph() }
)
authenticatorGraph(navController)
}

val targetRoute = when (state) {
RootNavState.ItemListing -> AUTHENTICATOR_GRAPH_ROUTE
RootNavState.Splash -> SPLASH_ROUTE
RootNavState.Tutorial -> TUTORIAL_ROUTE
}

val currentRoute = navController.currentDestination?.rootLevelRoute()
Expand All @@ -94,8 +101,9 @@ fun RootNavScreen(

LaunchedEffect(state) {
when (state) {
RootNavState.ItemListing -> navController.navigateToAuthenticatorGraph(rootNavOptions)
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
RootNavState.Tutorial -> navController.navigateToTutorial(rootNavOptions)
RootNavState.ItemListing -> navController.navigateToAuthenticatorGraph(rootNavOptions)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package com.x8bit.bitwarden.authenticator.ui.platform.feature.rootnav
import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.authenticator.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.authenticator.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
Expand All @@ -14,30 +17,42 @@ import javax.inject.Inject
@HiltViewModel
class RootNavViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val settingsRepository: SettingsRepository,
) : BaseViewModel<RootNavState, Unit, RootNavAction>(
initialState = RootNavState.Splash
) {

init {
viewModelScope.launch {
delay(250)
trySendAction(RootNavAction.Internal.StateUpdate)
settingsRepository.hasSeenWelcomeTutorialFlow
.map { RootNavAction.Internal.HasSeenWelcomeTutorialChange(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
}

override fun handleAction(action: RootNavAction) {
when (action) {
RootNavAction.BackStackUpdate -> handleBackStackUpdate()
RootNavAction.Internal.StateUpdate -> handleStateUpdate()
RootNavAction.BackStackUpdate -> {
handleBackStackUpdate()
}

is RootNavAction.Internal.HasSeenWelcomeTutorialChange -> {
handleHasSeenWelcomeTutorialChange(action.hasSeenWelcomeGuide)
}
}
}

private fun handleBackStackUpdate() {
authRepository.updateLastActiveTime()
}

private fun handleStateUpdate() {
mutableStateFlow.update { RootNavState.ItemListing }
private fun handleHasSeenWelcomeTutorialChange(hasSeenWelcomeGuide: Boolean) {
if (hasSeenWelcomeGuide) {
mutableStateFlow.update { RootNavState.ItemListing }
} else {
mutableStateFlow.update { RootNavState.Tutorial }
}
}
}

Expand All @@ -52,6 +67,12 @@ sealed class RootNavState : Parcelable {
@Parcelize
data object Splash : RootNavState()

/**
* App should display the Tutorial nav graph.
*/
@Parcelize
data object Tutorial : RootNavState()

/**
* App should display the Account List nav graph.
*/
Expand All @@ -68,7 +89,14 @@ sealed class RootNavAction {
*/
data object BackStackUpdate : RootNavAction()

/**
* Models actions the [RootNavViewModel] itself may send.
*/
sealed class Internal : RootNavAction() {
data object StateUpdate : Internal()

/**
* Indicates an update in the welcome guide being seen has been received.
*/
data class HasSeenWelcomeTutorialChange(val hasSeenWelcomeGuide: Boolean) : Internal()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ private const val SETTINGS_ROUTE = "settings"
*/
fun NavGraphBuilder.settingsGraph(
navController: NavController,
onNavigateToTutorial: () -> Unit,
) {
navigation(
startDestination = SETTINGS_ROUTE,
route = SETTINGS_GRAPH_ROUTE
) {
composableWithRootPushTransitions(
route = SETTINGS_ROUTE
) {
SettingsScreen()
}
}
navigation(
startDestination = SETTINGS_ROUTE,
route = SETTINGS_GRAPH_ROUTE
) {
composableWithRootPushTransitions(
route = SETTINGS_ROUTE
) {
SettingsScreen(
onNavigateToTutorial = onNavigateToTutorial,
)
}
}
}

/**
Expand Down
Loading

0 comments on commit 12e5314

Please sign in to comment.