Show Welcome Tutorial on first launch (#27)

This commit is contained in:
Patrick Honkonen 2024-04-15 10:12:58 -04:00 committed by GitHub
parent b6a165aef5
commit 12e5314c61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 766 additions and 33 deletions

View File

@ -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.

View File

@ -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].
@ -47,6 +48,9 @@ class SettingsDiskSourceImpl(
)
}
private val mutableFirstLaunchFlow =
bufferedMutableSharedFlow<Boolean>()
override var appTheme: AppTheme
get() = getString(key = APP_THEME_KEY)
?.let { storedValue ->
@ -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,

View File

@ -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>
}

View File

@ -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,
)
}

View File

@ -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"
@ -42,6 +37,7 @@ fun NavGraphBuilder.authenticatorGraph(
onNavigateToQrCodeScanner = { navController.navigateToQrCodeScanScreen() },
onNavigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() },
onNavigateToEditItem = { navController.navigateToEditItem(itemId = it) },
onNavigateToTutorial = { navController.navigateToTutorial() },
)
itemListingGraph(
navController = navController,
@ -56,7 +52,8 @@ fun NavGraphBuilder.authenticatorGraph(
},
navigateToEditItem = {
navController.navigateToEditItem(itemId = it)
}
},
navigateToTutorial = { navController.navigateToTutorial() },
)
}
}

View File

@ -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"
@ -24,6 +23,7 @@ fun NavGraphBuilder.itemListingGraph(
navigateToQrCodeScanner: () -> Unit,
navigateToManualKeyEntry: () -> Unit,
navigateToEditItem: (String) -> Unit,
navigateToTutorial: () -> Unit,
) {
navigation(
route = ITEM_LISTING_GRAPH_ROUTE,
@ -60,7 +60,10 @@ fun NavGraphBuilder.itemListingGraph(
navController.navigateToQrCodeScanScreen()
}
)
settingsGraph(navController)
settingsGraph(
navController = navController,
onNavigateToTutorial = navigateToTutorial
)
}
}

View File

@ -13,6 +13,7 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToQrCodeScanner: () -> Unit,
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToEditItem: (itemId: String) -> Unit,
onNavigateToTutorial: () -> Unit,
) {
composableWithStayTransitions(
route = AUTHENTICATOR_NAV_BAR_ROUTE,
@ -22,6 +23,7 @@ fun NavGraphBuilder.authenticatorNavBarDestination(
onNavigateToManualKeyEntry = onNavigateToManualKeyEntry,
onNavigateToEditItem = onNavigateToEditItem,
onNavigateToSearch = onNavigateToSearch,
onNavigateToTutorial = onNavigateToTutorial,
)
}
}

View File

@ -71,6 +71,7 @@ fun AuthenticatorNavBarScreen(
onNavigateToQrCodeScanner: () -> Unit,
onNavigateToManualKeyEntry: () -> Unit,
onNavigateToEditItem: (itemId: String) -> Unit,
onNavigateToTutorial: () -> Unit,
) {
EventsEffect(viewModel = viewModel) { event ->
navController.apply {
@ -108,6 +109,7 @@ fun AuthenticatorNavBarScreen(
navigateToQrCodeScanner = onNavigateToQrCodeScanner,
navigateToManualKeyEntry = onNavigateToManualKeyEntry,
navigateToEditItem = onNavigateToEditItem,
navigateToTutorial = onNavigateToTutorial,
)
}
@ -121,6 +123,7 @@ private fun AuthenticatorNavBarScaffold(
navigateToQrCodeScanner: () -> Unit,
navigateToManualKeyEntry: () -> Unit,
navigateToEditItem: (itemId: String) -> Unit,
navigateToTutorial: () -> Unit,
) {
BitwardenScaffold(
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.statusBars),
@ -166,6 +169,7 @@ private fun AuthenticatorNavBarScaffold(
navigateToQrCodeScanner = navigateToQrCodeScanner,
navigateToManualKeyEntry = navigateToManualKeyEntry,
navigateToEditItem = navigateToEditItem,
navigateToTutorial = navigateToTutorial,
)
}
}

View File

@ -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
@ -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()
@ -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)
}
}
}

View File

@ -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
@ -14,21 +17,29 @@ 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)
}
}
}
@ -36,8 +47,12 @@ class RootNavViewModel @Inject constructor(
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 }
}
}
}
@ -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.
*/
@ -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()
}
}

View File

@ -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,
)
}
}
}
/**

View File

@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.authenticator.R
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
import com.x8bit.bitwarden.authenticator.ui.platform.components.appbar.BitwardenMediumTopAppBar
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
@ -47,11 +48,18 @@ import com.x8bit.bitwarden.authenticator.ui.platform.util.displayLabel
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel(),
onNavigateToTutorial: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
EventsEffect(viewModel = viewModel) { event ->
when (event) {
SettingsEvent.NavigateToTutorial -> onNavigateToTutorial()
}
}
BitwardenScaffold(
topBar = {
BitwardenMediumTopAppBar(
@ -75,22 +83,32 @@ fun SettingsScreen(
}
},
onThemeSelection = remember(viewModel) {
{ viewModel.trySendAction(SettingsAction.AppearanceChange.ThemeChange(it)) }
{
viewModel.trySendAction(SettingsAction.AppearanceChange.ThemeChange(it))
}
},
onShowWebsiteIconsChange = remember(viewModel) {
{
viewModel.trySendAction(
SettingsAction.AppearanceChange.ShowWebsiteIconsChange(
it
)
SettingsAction.AppearanceChange.ShowWebsiteIconsChange(it)
)
}
},
)
HelpSettings(
onTutorialClick = remember(viewModel) {
{
viewModel.trySendAction(SettingsAction.HelpClick.ShowTutorialClick)
}
}
)
}
}
}
//region Appearance settings
@Composable
private fun AppearanceSettings(
state: SettingsState,
@ -222,3 +240,25 @@ private fun ThemeSelectionRow(
}
}
}
//endregion Appearance settings
//region Help settings
@Composable
private fun HelpSettings(
modifier: Modifier = Modifier,
onTutorialClick: () -> Unit,
) {
BitwardenListHeaderText(
modifier = Modifier.padding(horizontal = 16.dp),
label = stringResource(id = R.string.help)
)
BitwardenTextRow(
text = stringResource(id = R.string.tutorial),
onClick = onTutorialClick,
modifier = modifier,
)
}
//region Help settings

View File

@ -35,6 +35,7 @@ class SettingsViewModel @Inject constructor(
override fun handleAction(action: SettingsAction) {
when (action) {
is SettingsAction.AppearanceChange -> handleAppearanceChange(action)
is SettingsAction.HelpClick -> handleHelpClick(action)
}
}
@ -85,6 +86,16 @@ class SettingsViewModel @Inject constructor(
}
settingsRepository.appTheme = theme
}
private fun handleHelpClick(action: SettingsAction.HelpClick) {
when (action) {
SettingsAction.HelpClick.ShowTutorialClick -> handleShowTutorialCLick()
}
}
private fun handleShowTutorialCLick() {
sendEvent(SettingsEvent.NavigateToTutorial)
}
}
/**
@ -108,13 +119,19 @@ data class SettingsState(
/**
* Models events for the settings screen.
*/
sealed class SettingsEvent
sealed class SettingsEvent {
data object NavigateToTutorial : SettingsEvent()
}
/**
* Models actions for the settings screen.
*/
sealed class SettingsAction {
sealed class HelpClick : SettingsAction() {
data object ShowTutorialClick : HelpClick()
}
sealed class AppearanceChange : SettingsAction() {
/**
* Indicates the user changed the language.

View File

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.authenticator.ui.platform.feature.tutorial
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
const val TUTORIAL_ROUTE = "tutorial"
fun NavGraphBuilder.tutorialDestination(onTutorialFinished: () -> Unit) {
composable(TUTORIAL_ROUTE) {
TutorialScreen(
onTutorialFinished = onTutorialFinished,
)
}
}
fun NavController.navigateToTutorial(navOptions: NavOptions? = null) {
navigate(route = TUTORIAL_ROUTE, navOptions = navOptions)
}

View File

@ -0,0 +1,241 @@
package com.x8bit.bitwarden.authenticator.ui.platform.feature.tutorial
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.authenticator.R
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
import com.x8bit.bitwarden.authenticator.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
private const val INTRO_PAGE = 0
private const val QR_SCANNER_PAGE = 1
private const val UNIQUE_CODES_PAGE = 2
private const val PAGE_COUNT = 3
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun TutorialScreen(
viewModel: TutorialViewModel = hiltViewModel(),
onTutorialFinished: () -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val pagerState = rememberPagerState(pageCount = { PAGE_COUNT })
EventsEffect(viewModel = viewModel) { event ->
when (event) {
TutorialEvent.NavigateToAuthenticator -> {
onTutorialFinished()
}
TutorialEvent.NavigateToQrScannerSlide -> {
pagerState.animateScrollToPage(page = QR_SCANNER_PAGE)
}
TutorialEvent.NavigateToUniqueCodesSlide -> {
pagerState.animateScrollToPage(page = UNIQUE_CODES_PAGE)
}
}
}
BitwardenScaffold(
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding),
verticalArrangement = Arrangement.Center
) {
HorizontalPager(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.weight(1f),
state = pagerState,
userScrollEnabled = true,
) { page ->
viewModel.trySendAction(
TutorialAction.TutorialPageChange(pagerState.targetPage)
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
when (page) {
INTRO_PAGE -> VerificationCodesContent()
QR_SCANNER_PAGE -> TutorialQrScannerScreen()
UNIQUE_CODES_PAGE -> UniqueCodesContent()
}
}
}
LazyColumn(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.Bottom,
) {
item {
Row(
modifier = Modifier
.height(50.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
repeat(PAGE_COUNT) {
val color = if (pagerState.currentPage == it) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondary
}
Box(
modifier = Modifier
.padding(8.dp)
.background(color, CircleShape)
.size(10.dp)
)
}
}
}
item {
BitwardenFilledTonalButton(
modifier = Modifier.fillMaxWidth(),
label = state.continueButtonText(),
onClick = remember(viewModel) {
{
viewModel.trySendAction(
TutorialAction.ContinueClick
)
}
},
)
}
item {
val alpha = remember(state) {
if (state.isLastPage) {
0f
} else {
1f
}
}
BitwardenTextButton(
modifier = Modifier
.fillMaxWidth()
.alpha(alpha),
isEnabled = !state.isLastPage,
label = stringResource(id = R.string.skip),
onClick = remember(viewModel) {
{
viewModel.trySendAction(TutorialAction.SkipClick)
}
},
)
}
}
}
}
}
@Composable
private fun VerificationCodesContent() {
Image(
painter = painterResource(R.drawable.ic_tutorial_verification_codes),
contentDescription = stringResource(
id = R.string.secure_your_accounts_with_bitwarden_authenticator,
),
)
Text(
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
text = stringResource(R.string.secure_your_accounts_with_bitwarden_authenticator),
)
Text(
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
text = stringResource(R.string.get_verification_codes_for_all_your_accounts),
)
}
@Composable
private fun TutorialQrScannerScreen() {
Image(
painter = painterResource(id = R.drawable.ic_tutorial_qr_scanner),
contentDescription = stringResource(id = R.string.scan_qr_code),
)
Text(
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
text = stringResource(
R.string.use_your_device_camera_to_scan_codes
),
)
Text(
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
text = stringResource(
R.string.scan_the_qr_code_in_your_2_step_verification_settings_for_any_account
),
)
}
@Composable
private fun UniqueCodesContent() {
Image(
painter = painterResource(id = R.drawable.ic_tutorial_2fa),
contentDescription = stringResource(id = R.string.unique_codes)
)
Text(
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
text = stringResource(R.string.sign_in_using_unique_codes),
)
Text(
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
text = stringResource(
R.string.when_using_2_step_verification_youll_enter_your_username_and_password_and_a_code_generated_in_this_app
),
)
}
@Preview
@Composable
fun TutorialScreenPreview() {
Box {
TutorialScreen(
onTutorialFinished = {}
)
}
}

View File

@ -0,0 +1,145 @@
package com.x8bit.bitwarden.authenticator.ui.platform.feature.tutorial
import android.os.Parcelable
import com.x8bit.bitwarden.authenticator.R
import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.authenticator.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/**
* View model for the [TutorialScreen].
*/
@HiltViewModel
class TutorialViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
) :
BaseViewModel<TutorialState, TutorialEvent, TutorialAction>(
initialState = TutorialState.IntroSlide
) {
override fun handleAction(action: TutorialAction) {
when (action) {
TutorialAction.ContinueClick -> {
handleContinueClick()
}
TutorialAction.SkipClick -> {
handleSkipClick()
}
is TutorialAction.TutorialPageChange -> {
handleTutorialPageChange(action.targetPage)
}
}
}
private fun handleTutorialPageChange(page: Int) {
when (page) {
0 -> mutableStateFlow.update { TutorialState.IntroSlide }
1 -> mutableStateFlow.update { TutorialState.QrScannerSlide }
2 -> mutableStateFlow.update { TutorialState.UniqueCodesSlide }
}
}
private fun handleContinueClick() {
val currentPage = mutableStateFlow.value
val event = when (currentPage) {
TutorialState.IntroSlide -> TutorialEvent.NavigateToQrScannerSlide
TutorialState.QrScannerSlide -> TutorialEvent.NavigateToUniqueCodesSlide
TutorialState.UniqueCodesSlide -> {
settingsRepository.hasSeenWelcomeTutorial = true
TutorialEvent.NavigateToAuthenticator
}
}
sendEvent(event)
}
private fun handleSkipClick() {
settingsRepository.hasSeenWelcomeTutorial = true
sendEvent(TutorialEvent.NavigateToAuthenticator)
}
}
/**
* Models state for the Tutorial screen.
*/
@Parcelize
sealed class TutorialState(
val continueButtonText: Text,
val isLastPage: Boolean,
) : Parcelable {
/**
* Tutorial should display the introduction slide.
*/
@Parcelize
data object IntroSlide : TutorialState(
continueButtonText = R.string.continue_button.asText(),
isLastPage = false,
)
/**
* Tutorial should display the QR code scanner description slide.
*/
@Parcelize
data object QrScannerSlide : TutorialState(
continueButtonText = R.string.continue_button.asText(),
isLastPage = false
)
/**
* Tutorial should display the 2FA code description slide.
*/
@Parcelize
data object UniqueCodesSlide : TutorialState(
continueButtonText = R.string.get_started.asText(),
isLastPage = true
)
}
/**
* Represents a set of events related to the tutorial screen.
*/
sealed class TutorialEvent {
/**
* Navigate to the authenticator tutorial slide.
*/
data object NavigateToAuthenticator : TutorialEvent()
/**
* Navigate to the QR Code scanner tutorial slide.
*/
data object NavigateToQrScannerSlide : TutorialEvent()
/**
* Navigate to the unique codes tutorial slide.
*/
data object NavigateToUniqueCodesSlide : TutorialEvent()
}
/**
* Models actions that can be taken on the tutorial screen.
*/
sealed class TutorialAction {
/**
* The user has manually changed the tutorial page by swiping.
*/
data class TutorialPageChange(
val targetPage: Int,
) : TutorialAction()
/**
* The user clicked the continue button on the introduction slide.
*/
data object ContinueClick : TutorialAction()
/**
* The user clicked the skip button on one of the tutorial slides.
*/
data object SkipClick : TutorialAction()
}

View File

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="214dp"
android:height="72dp"
android:viewportHeight="72"
android:viewportWidth="214">
<path
android:fillColor="#B2C5FF"
android:pathData="M12.51,25.73L9.77,28.85C9.61,29.03 9.52,29.27 9.52,29.51V65.67C9.52,65.98 9.66,66.26 9.9,66.45L13.94,69.68C14.11,69.82 14.33,69.9 14.56,69.9H58.46C58.77,69.9 59.07,69.75 59.26,69.5L62.98,64.54C63.11,64.37 63.18,64.16 63.18,63.94V29.58C63.18,29.3 63.06,29.03 62.84,28.84L59.24,25.64C59.06,25.48 58.82,25.39 58.58,25.39H13.26C12.97,25.39 12.7,25.51 12.51,25.73Z" />
<group>
<clip-path android:pathData="M0.74,0.5h71v71h-71z" />
<path
android:fillColor="#0055D4"
android:pathData="M39.84,47.26C39.85,46.61 39.68,45.97 39.36,45.41C39.04,44.84 38.57,44.38 38.01,44.06C37.46,43.74 36.82,43.58 36.18,43.59C35.54,43.6 34.91,43.78 34.36,44.12C33.82,44.46 33.37,44.94 33.07,45.52C32.76,46.09 32.62,46.74 32.65,47.39C32.68,48.04 32.87,48.66 33.22,49.21C33.57,49.76 34.06,50.2 34.63,50.49V56.47C34.62,56.69 34.65,56.91 34.73,57.12C34.81,57.33 34.92,57.52 35.07,57.68C35.22,57.84 35.41,57.97 35.61,58.05C35.81,58.14 36.03,58.19 36.25,58.19C36.46,58.19 36.68,58.14 36.88,58.05C37.08,57.97 37.27,57.84 37.42,57.68C37.57,57.51 37.69,57.32 37.76,57.12C37.84,56.91 37.87,56.69 37.86,56.47V50.48C38.45,50.18 38.95,49.72 39.3,49.15C39.65,48.58 39.83,47.93 39.84,47.26ZM58.75,23.16H56.76C56.6,23.16 56.45,23.12 56.3,23.06C56.16,23 56.03,22.91 55.92,22.8C55.81,22.69 55.72,22.55 55.66,22.41C55.6,22.26 55.57,22.1 55.57,21.94V21.06C55.66,16.08 53.94,11.24 50.73,7.46C47.52,3.68 43.05,1.24 38.17,0.59C35.48,0.33 32.75,0.64 30.19,1.51C27.62,2.37 25.26,3.77 23.25,5.62C21.25,7.47 19.65,9.72 18.56,12.23C17.47,14.74 16.91,17.45 16.91,20.2V21.63C16.91,21.66 16.81,23.15 15.53,23.17H13.74C12.15,23.17 10.63,23.82 9.5,24.96C8.38,26.1 7.75,27.65 7.75,29.26V65.39C7.75,67.01 8.38,68.55 9.5,69.7C10.63,70.84 12.15,71.49 13.74,71.5H58.75C60.34,71.49 61.86,70.84 62.98,69.7C64.1,68.56 64.73,67.01 64.73,65.4V29.26C64.73,28.46 64.58,27.67 64.28,26.93C63.98,26.19 63.54,25.52 62.99,24.95C62.43,24.39 61.77,23.94 61.04,23.63C60.32,23.32 59.54,23.16 58.75,23.16L58.75,23.16ZM21.3,20.2C21.29,17.93 21.78,15.7 22.74,13.65C23.69,11.61 25.09,9.8 26.83,8.38C28.56,6.95 30.59,5.93 32.76,5.4C34.93,4.86 37.19,4.83 39.38,5.29C42.78,6.1 45.81,8.06 47.96,10.85C50.11,13.65 51.25,17.1 51.19,20.63V21.94C51.19,22.1 51.16,22.26 51.1,22.41C51.04,22.55 50.95,22.69 50.84,22.8C50.73,22.91 50.6,23 50.45,23.06C50.31,23.12 50.15,23.16 49.99,23.16H23.13C22.91,23.18 22.68,23.15 22.47,23.08C22.26,23.01 22.06,22.9 21.89,22.76C21.72,22.61 21.58,22.43 21.47,22.23C21.37,22.03 21.31,21.81 21.29,21.58V20.2H21.3ZM60.35,65.39C60.35,65.83 60.18,66.24 59.88,66.55C59.58,66.86 59.17,67.03 58.75,67.03H13.74C13.53,67.03 13.32,66.99 13.13,66.9C12.93,66.82 12.75,66.7 12.6,66.55C12.45,66.4 12.34,66.22 12.26,66.02C12.18,65.82 12.14,65.61 12.14,65.39V29.26C12.13,29.04 12.18,28.83 12.26,28.63C12.33,28.44 12.45,28.26 12.6,28.1C12.75,27.95 12.93,27.83 13.12,27.75C13.32,27.67 13.52,27.62 13.73,27.62H58.75C59.17,27.63 59.58,27.8 59.88,28.11C60.18,28.41 60.35,28.82 60.35,29.26V65.39L60.35,65.39Z" />
</group>
<path
android:fillColor="#ffffff"
android:pathData="M87.48,13L210.26,13A1,1 0,0 1,211.26 14L211.26,62A1,1 0,0 1,210.26 63L87.48,63A1,1 0,0 1,86.48 62L86.48,14A1,1 0,0 1,87.48 13z"
android:strokeColor="#B2C5FF"
android:strokeWidth="5" />
<path
android:fillColor="#0055D4"
android:pathData="M174.17,48.66C173.85,49.09 173.23,49.18 172.79,48.87L172.18,48.43C171.75,48.12 171.63,47.52 171.93,47.07L176.37,40.29C176.71,39.77 176.5,39.07 175.93,38.82L169.45,36.02C168.97,35.82 168.73,35.27 168.9,34.78L169.1,34.19C169.28,33.68 169.84,33.4 170.36,33.57L176.86,35.71C177.49,35.92 178.14,35.47 178.17,34.81L178.53,27.25C178.56,26.71 179,26.3 179.53,26.3H180.12C180.65,26.3 181.09,26.71 181.12,27.25L181.48,34.81C181.51,35.47 182.16,35.92 182.79,35.71L189.29,33.57C189.81,33.4 190.37,33.68 190.55,34.19L190.75,34.78C190.92,35.27 190.68,35.82 190.2,36.02L183.72,38.82C183.15,39.07 182.94,39.77 183.28,40.29L187.72,47.07C188.02,47.52 187.9,48.12 187.47,48.43L186.86,48.87C186.42,49.18 185.8,49.09 185.48,48.66L180.62,42.22C180.22,41.69 179.43,41.69 179.03,42.22L174.17,48.66Z" />
<path
android:fillColor="#0055D4"
android:pathData="M142.99,48.66C142.67,49.09 142.05,49.18 141.61,48.87L141,48.43C140.57,48.12 140.45,47.52 140.75,47.07L145.19,40.29C145.53,39.77 145.32,39.07 144.75,38.82L138.27,36.02C137.79,35.82 137.55,35.27 137.72,34.78L137.92,34.19C138.1,33.68 138.66,33.4 139.18,33.57L145.68,35.71C146.31,35.92 146.96,35.47 146.99,34.81L147.35,27.25C147.38,26.71 147.82,26.3 148.35,26.3H148.94C149.47,26.3 149.91,26.71 149.94,27.25L150.3,34.81C150.33,35.47 150.98,35.92 151.61,35.71L158.11,33.57C158.63,33.4 159.19,33.68 159.37,34.19L159.57,34.78C159.74,35.27 159.51,35.82 159.02,36.02L152.54,38.82C151.98,39.07 151.76,39.77 152.1,40.29L156.54,47.07C156.84,47.52 156.73,48.12 156.29,48.43L155.68,48.87C155.24,49.18 154.63,49.09 154.3,48.66L149.44,42.22C149.04,41.69 148.25,41.69 147.85,42.22L142.99,48.66Z" />
<path
android:fillColor="#0055D4"
android:pathData="M111.81,48.66C111.49,49.09 110.88,49.18 110.43,48.87L109.82,48.43C109.39,48.12 109.27,47.52 109.57,47.07L114.01,40.29C114.35,39.77 114.14,39.07 113.57,38.82L107.09,36.02C106.61,35.82 106.37,35.27 106.54,34.78L106.74,34.19C106.92,33.68 107.48,33.4 108,33.57L114.5,35.71C115.13,35.92 115.78,35.47 115.81,34.81L116.17,27.25C116.2,26.71 116.64,26.3 117.17,26.3H117.76C118.29,26.3 118.73,26.71 118.76,27.25L119.12,34.81C119.15,35.47 119.8,35.92 120.43,35.71L126.93,33.57C127.45,33.4 128.01,33.68 128.19,34.19L128.39,34.78C128.57,35.27 128.33,35.82 127.85,36.02L121.36,38.82C120.79,39.07 120.58,39.77 120.93,40.29L125.36,47.07C125.66,47.52 125.54,48.12 125.11,48.43L124.5,48.87C124.06,49.18 123.45,49.09 123.12,48.66L118.26,42.22C117.86,41.69 117.07,41.69 116.67,42.22L111.81,48.66Z" />
</vector>

View File

@ -0,0 +1,93 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="146dp"
android:height="147dp"
android:viewportWidth="146"
android:viewportHeight="147">
<path
android:pathData="M3,119.64V142.19C3,142.74 3.45,143.19 4,143.19H26.55"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#B2C5FF"
android:strokeLineCap="round"/>
<path
android:pathData="M143,26.55V4C143,3.45 142.55,3 142,3L119.45,3"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#B2C5FF"
android:strokeLineCap="round"/>
<path
android:pathData="M119.45,143.19H142C142.55,143.19 143,142.74 143,142.19V119.64"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#B2C5FF"
android:strokeLineCap="round"/>
<path
android:pathData="M26.55,3L4,3C3.45,3 3,3.45 3,4L3,26.55"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#B2C5FF"
android:strokeLineCap="round"/>
<path
android:pathData="M20.22,23.16L57.2,23.16A1,1 0,0 1,58.2 24.16L58.2,61.14A1,1 0,0 1,57.2 62.14L20.22,62.14A1,1 0,0 1,19.22 61.14L19.22,24.16A1,1 0,0 1,20.22 23.16z"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"/>
<path
android:pathData="M29.96,32.91L47.45,32.91A1,1 0,0 1,48.45 33.91L48.45,51.4A1,1 0,0 1,47.45 52.4L29.96,52.4A1,1 0,0 1,28.96 51.4L28.96,33.91A1,1 0,0 1,29.96 32.91z"
android:fillColor="#0055D4"/>
<path
android:pathData="M84.18,23.16H122.15C122.71,23.16 123.15,23.61 123.15,24.16V62.14"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"
android:strokeLineCap="round"/>
<path
android:pathData="M94.92,32.91L112.41,32.91A1,1 0,0 1,113.41 33.91L113.41,51.4A1,1 0,0 1,112.41 52.4L94.92,52.4A1,1 0,0 1,93.92 51.4L93.92,33.91A1,1 0,0 1,94.92 32.91z"
android:fillColor="#0055D4"/>
<path
android:pathData="M20.22,88.12L57.2,88.12A1,1 0,0 1,58.2 89.12L58.2,126.1A1,1 0,0 1,57.2 127.1L20.22,127.1A1,1 0,0 1,19.22 126.1L19.22,89.12A1,1 0,0 1,20.22 88.12z"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"/>
<path
android:pathData="M29.96,97.87L47.45,97.87A1,1 0,0 1,48.45 98.87L48.45,116.36A1,1 0,0 1,47.45 117.36L29.96,117.36A1,1 0,0 1,28.96 116.36L28.96,98.87A1,1 0,0 1,29.96 97.87z"
android:fillColor="#0055D4"/>
<path
android:pathData="M85.18,88.12L122.15,88.12A1,1 0,0 1,123.15 89.12L123.15,126.1A1,1 0,0 1,122.15 127.1L85.18,127.1A1,1 0,0 1,84.18 126.1L84.18,89.12A1,1 0,0 1,85.18 88.12z"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"/>
<path
android:pathData="M94.92,97.87L112.41,97.87A1,1 0,0 1,113.41 98.87L113.41,116.36A1,1 0,0 1,112.41 117.36L94.92,117.36A1,1 0,0 1,93.92 116.36L93.92,98.87A1,1 0,0 1,94.92 97.87z"
android:fillColor="#0055D4"/>
<path
android:pathData="M71.21,23.76V59.87"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"
android:strokeLineCap="round"/>
<path
android:pathData="M71.21,86.79V127.77"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"
android:strokeLineCap="round"/>
<path
android:pathData="M18.84,74.95H52.33"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"
android:strokeLineCap="round"/>
<path
android:pathData="M70.81,74.95H94.55"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"
android:strokeLineCap="round"/>
<path
android:pathData="M108.16,74.95H123.78"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#0055D4"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="214dp"
android:height="140dp"
android:viewportHeight="140"
android:viewportWidth="214">
<path
android:fillColor="#B2C5FF"
android:pathData="M12.51,59.73L9.77,62.85C9.61,63.03 9.52,63.27 9.52,63.51V99.67C9.52,99.98 9.66,100.26 9.9,100.45L13.94,103.68C14.11,103.82 14.33,103.9 14.56,103.9H58.46C58.77,103.9 59.07,103.75 59.26,103.5L62.98,98.54C63.11,98.37 63.18,98.16 63.18,97.94V63.58C63.18,63.3 63.06,63.03 62.84,62.84L59.24,59.64C59.06,59.48 58.82,59.39 58.58,59.39H13.26C12.97,59.39 12.7,59.51 12.51,59.73Z" />
<group>
<clip-path android:pathData="M0.74,34.5h71v71h-71z" />
<path
android:fillColor="#0055D4"
android:pathData="M39.84,81.26C39.85,80.61 39.68,79.97 39.36,79.41C39.04,78.84 38.57,78.38 38.01,78.06C37.46,77.74 36.82,77.58 36.18,77.59C35.54,77.6 34.91,77.78 34.36,78.12C33.82,78.46 33.37,78.94 33.07,79.52C32.76,80.09 32.62,80.74 32.65,81.39C32.68,82.04 32.87,82.66 33.22,83.21C33.57,83.76 34.06,84.2 34.63,84.49V90.46C34.62,90.69 34.65,90.91 34.73,91.12C34.81,91.33 34.92,91.52 35.07,91.68C35.22,91.84 35.41,91.97 35.61,92.05C35.81,92.14 36.03,92.19 36.25,92.19C36.46,92.19 36.68,92.14 36.88,92.05C37.08,91.97 37.27,91.84 37.42,91.68C37.57,91.51 37.69,91.32 37.76,91.12C37.84,90.91 37.87,90.69 37.86,90.46V84.48C38.45,84.18 38.95,83.72 39.3,83.15C39.65,82.58 39.83,81.93 39.84,81.26ZM58.75,57.16H56.76C56.6,57.16 56.45,57.12 56.3,57.06C56.16,57 56.03,56.91 55.92,56.8C55.81,56.69 55.72,56.55 55.66,56.41C55.6,56.26 55.57,56.1 55.57,55.94V55.06C55.66,50.08 53.94,45.24 50.73,41.46C47.52,37.68 43.05,35.24 38.17,34.59C35.48,34.33 32.75,34.64 30.19,35.51C27.62,36.37 25.26,37.77 23.25,39.62C21.25,41.47 19.65,43.72 18.56,46.23C17.47,48.74 16.91,51.45 16.91,54.2V55.63C16.91,55.66 16.81,57.15 15.53,57.17H13.74C12.15,57.17 10.63,57.82 9.5,58.96C8.38,60.1 7.75,61.65 7.75,63.26V99.39C7.75,101.01 8.38,102.55 9.5,103.7C10.63,104.84 12.15,105.49 13.74,105.5H58.75C60.34,105.49 61.86,104.84 62.98,103.7C64.1,102.56 64.73,101.01 64.73,99.4V63.26C64.73,62.46 64.58,61.67 64.28,60.93C63.98,60.19 63.54,59.52 62.99,58.95C62.43,58.39 61.77,57.94 61.04,57.63C60.32,57.32 59.54,57.16 58.75,57.16L58.75,57.16ZM21.3,54.2C21.29,51.93 21.78,49.7 22.74,47.65C23.69,45.61 25.09,43.8 26.83,42.38C28.56,40.95 30.59,39.93 32.76,39.4C34.93,38.86 37.19,38.83 39.38,39.29C42.78,40.1 45.81,42.06 47.96,44.85C50.11,47.65 51.25,51.1 51.19,54.63V55.94C51.19,56.1 51.16,56.26 51.1,56.41C51.04,56.55 50.95,56.69 50.84,56.8C50.73,56.91 50.6,57 50.45,57.06C50.31,57.12 50.15,57.16 49.99,57.16H23.13C22.91,57.18 22.68,57.15 22.47,57.08C22.26,57.01 22.06,56.9 21.89,56.76C21.72,56.61 21.58,56.43 21.47,56.23C21.37,56.03 21.31,55.81 21.29,55.58V54.2H21.3ZM60.35,99.39C60.35,99.83 60.18,100.24 59.88,100.55C59.58,100.86 59.17,101.03 58.75,101.03H13.74C13.53,101.03 13.32,100.99 13.13,100.9C12.93,100.82 12.75,100.7 12.6,100.55C12.45,100.4 12.34,100.22 12.26,100.02C12.18,99.82 12.14,99.61 12.14,99.39V63.26C12.13,63.04 12.18,62.83 12.26,62.63C12.33,62.44 12.45,62.26 12.6,62.1C12.75,61.95 12.93,61.83 13.12,61.75C13.32,61.67 13.52,61.62 13.73,61.62H58.75C59.17,61.63 59.58,61.8 59.88,62.11C60.18,62.41 60.35,62.82 60.35,63.26V99.39L60.35,99.39Z" />
</group>
<path
android:fillColor="#ffffff"
android:pathData="M87.48,47L210.26,47A1,1 0,0 1,211.26 48L211.26,96A1,1 0,0 1,210.26 97L87.48,97A1,1 0,0 1,86.48 96L86.48,48A1,1 0,0 1,87.48 47z"
android:strokeColor="#B2C5FF"
android:strokeWidth="5" />
<path
android:fillColor="#0055D4"
android:pathData="M174.17,82.66C173.85,83.09 173.23,83.18 172.79,82.87L172.18,82.43C171.75,82.12 171.63,81.52 171.93,81.07L176.37,74.29C176.71,73.77 176.5,73.07 175.93,72.82L169.45,70.02C168.97,69.82 168.73,69.27 168.9,68.78L169.1,68.19C169.28,67.68 169.84,67.4 170.36,67.57L176.86,69.71C177.49,69.92 178.14,69.47 178.17,68.81L178.53,61.25C178.56,60.71 179,60.3 179.53,60.3H180.12C180.65,60.3 181.09,60.71 181.12,61.25L181.48,68.81C181.51,69.47 182.16,69.92 182.79,69.71L189.29,67.57C189.81,67.4 190.37,67.68 190.55,68.19L190.75,68.78C190.92,69.27 190.68,69.82 190.2,70.02L183.72,72.82C183.15,73.07 182.94,73.77 183.28,74.29L187.72,81.07C188.02,81.52 187.9,82.12 187.47,82.43L186.86,82.87C186.42,83.18 185.8,83.09 185.48,82.66L180.62,76.22C180.22,75.69 179.43,75.69 179.03,76.22L174.17,82.66Z" />
<path
android:fillColor="#0055D4"
android:pathData="M142.99,82.66C142.67,83.09 142.05,83.18 141.61,82.87L141,82.43C140.57,82.12 140.45,81.52 140.75,81.07L145.19,74.29C145.53,73.77 145.32,73.07 144.75,72.82L138.27,70.02C137.79,69.82 137.55,69.27 137.72,68.78L137.92,68.19C138.1,67.68 138.66,67.4 139.18,67.57L145.68,69.71C146.31,69.92 146.96,69.47 146.99,68.81L147.35,61.25C147.38,60.71 147.82,60.3 148.35,60.3H148.94C149.47,60.3 149.91,60.71 149.94,61.25L150.3,68.81C150.33,69.47 150.98,69.92 151.61,69.71L158.11,67.57C158.63,67.4 159.19,67.68 159.37,68.19L159.57,68.78C159.74,69.27 159.51,69.82 159.02,70.02L152.54,72.82C151.98,73.07 151.76,73.77 152.1,74.29L156.54,81.07C156.84,81.52 156.73,82.12 156.29,82.43L155.68,82.87C155.24,83.18 154.63,83.09 154.3,82.66L149.44,76.22C149.04,75.69 148.25,75.69 147.85,76.22L142.99,82.66Z" />
<path
android:fillColor="#0055D4"
android:pathData="M111.81,82.66C111.49,83.09 110.88,83.18 110.43,82.87L109.82,82.43C109.39,82.12 109.27,81.52 109.57,81.07L114.01,74.29C114.35,73.77 114.14,73.07 113.57,72.82L107.09,70.02C106.61,69.82 106.37,69.27 106.54,68.78L106.74,68.19C106.92,67.68 107.48,67.4 108,67.57L114.5,69.71C115.13,69.92 115.78,69.47 115.81,68.81L116.17,61.25C116.2,60.71 116.64,60.3 117.17,60.3H117.76C118.29,60.3 118.73,60.71 118.76,61.25L119.12,68.81C119.15,69.47 119.8,69.92 120.43,69.71L126.93,67.57C127.45,67.4 128.01,67.68 128.19,68.19L128.39,68.78C128.57,69.27 128.33,69.82 127.85,70.02L121.36,72.82C120.79,73.07 120.58,73.77 120.93,74.29L125.36,81.07C125.66,81.52 125.54,82.12 125.11,82.43L124.5,82.87C124.06,83.18 123.45,83.09 123.12,82.66L118.26,76.22C117.86,75.69 117.07,75.69 116.67,76.22L111.81,82.66Z" />
</vector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="140dp"
android:height="140dp"
android:viewportWidth="140"
android:viewportHeight="140">
<group>
<clip-path
android:pathData="M0,0h140v140h-140z"/>
<path
android:pathData="M37.07,30.68C58.79,8.96 94,8.96 115.71,30.68C137.43,52.4 137.43,87.61 115.71,109.32C102.96,122.08 85.55,127.34 68.94,125.11C66.75,124.82 64.74,126.35 64.45,128.54C64.15,130.73 65.69,132.74 67.88,133.04C86.85,135.59 106.78,129.57 121.37,114.98C146.21,90.14 146.21,49.86 121.37,25.02C96.53,0.18 56.25,0.18 31.41,25.02C24.15,32.29 19,40.89 15.99,50.03L9.42,38.36C8.34,36.44 5.9,35.75 3.98,36.84C2.05,37.92 1.37,40.36 2.45,42.28L13.74,62.36C14.82,64.28 17.26,64.96 19.19,63.88L39.26,52.59C41.19,51.51 41.87,49.07 40.79,47.15C39.7,45.22 37.26,44.54 35.34,45.62L23.71,52.16C26.36,44.31 30.81,36.94 37.07,30.68ZM89.67,37.38C95.17,37.38 99.66,41.87 99.66,47.37V54.8H79.68V47.37C79.68,41.85 84.16,37.38 89.67,37.38ZM74.88,54.8V47.37C74.88,39.19 81.52,32.58 89.67,32.58C97.82,32.58 104.46,39.22 104.46,47.37V54.8H106.89C109.1,54.8 110.89,56.59 110.89,58.8V87.59C110.89,89.8 109.1,91.59 106.89,91.59H92.37V107.46C92.37,108.38 92.13,109.34 91.61,110.14C91.11,110.92 90.13,111.8 88.71,111.8H3.66C2.26,111.8 1.27,110.95 0.75,110.16C0.22,109.36 -0.03,108.39 0,107.41V77.66C0,76.74 0.24,75.78 0.75,74.98C1.26,74.2 2.23,73.32 3.66,73.32H69.04V58.8C69.04,56.59 70.83,54.8 73.04,54.8H74.88ZM73.84,73.32H88.71C90.13,73.32 91.11,74.2 91.61,74.98C92.13,75.78 92.37,76.74 92.37,77.66V86.79H106.09V59.6H73.84V73.32ZM87.57,86.79V78.12H4.8V107H87.57V91.59H87.46V86.79H87.57ZM4.8,107.6C4.8,107.61 4.8,107.61 4.8,107.61C4.8,107.61 4.8,107.61 4.8,107.61C4.8,107.61 4.8,107.61 4.8,107.6ZM47.05,96.97C47.23,96 47.33,94.84 47.33,93.49C47.33,91 46.97,89.08 46.26,87.73C45.85,86.95 45.34,86.29 44.74,85.77C44.16,85.23 43.47,84.83 42.68,84.56C41.9,84.28 41.02,84.14 40.06,84.14C38.6,84.14 37.34,84.45 36.29,85.08C35.24,85.69 34.44,86.59 33.89,87.78C33.57,88.49 33.34,89.35 33.19,90.34C33.04,91.34 32.96,92.46 32.96,93.7C32.96,94.66 33.03,95.56 33.16,96.39C33.3,97.21 33.51,97.96 33.8,98.64C34.37,99.89 35.22,100.87 36.34,101.58C37.47,102.29 38.75,102.64 40.17,102.64C41.41,102.64 42.53,102.38 43.54,101.85C44.55,101.32 45.37,100.57 46.01,99.6C46.51,98.82 46.86,97.94 47.05,96.97ZM43.17,89.55C43.38,90.51 43.48,91.74 43.48,93.24C43.48,94.83 43.38,96.11 43.19,97.09C42.99,98.07 42.65,98.82 42.16,99.35C41.68,99.87 41,100.14 40.14,100.14C39.31,100.14 38.65,99.88 38.17,99.38C37.68,98.87 37.33,98.12 37.13,97.14C36.92,96.15 36.82,94.89 36.82,93.34C36.82,91.06 37.06,89.38 37.53,88.28C38.02,87.19 38.88,86.64 40.12,86.64C40.98,86.64 41.65,86.89 42.14,87.38C42.62,87.87 42.97,88.59 43.17,89.55ZM18.8,89.44V100.67C18.8,101.32 18.97,101.82 19.32,102.16C19.67,102.5 20.12,102.66 20.69,102.66C21.98,102.66 22.62,101.84 22.62,100.18V86.06C22.62,85.47 22.47,85.01 22.17,84.67C21.87,84.33 21.47,84.16 20.98,84.16C20.54,84.16 20.24,84.24 20.08,84.39C19.92,84.53 19.58,84.93 19.05,85.57C18.53,86.21 17.93,86.79 17.24,87.31C16.57,87.83 15.67,88.32 14.54,88.79C13.78,89.1 13.25,89.36 12.95,89.55C12.65,89.75 12.5,90.06 12.5,90.48C12.5,90.84 12.65,91.16 12.95,91.44C13.26,91.71 13.61,91.85 14,91.85C14.83,91.85 16.43,91.05 18.8,89.44ZM60.64,100.67V89.44C58.27,91.05 56.67,91.85 55.85,91.85C55.45,91.85 55.1,91.71 54.79,91.44C54.5,91.16 54.35,90.84 54.35,90.48C54.35,90.06 54.5,89.75 54.79,89.55C55.09,89.36 55.62,89.1 56.38,88.79C57.51,88.32 58.41,87.83 59.09,87.31C59.77,86.79 60.37,86.21 60.9,85.57C61.42,84.93 61.76,84.53 61.92,84.39C62.08,84.24 62.38,84.16 62.82,84.16C63.31,84.16 63.71,84.33 64.01,84.67C64.31,85.01 64.46,85.47 64.46,86.06V100.18C64.46,101.84 63.82,102.66 62.54,102.66C61.97,102.66 61.51,102.5 61.16,102.16C60.82,101.82 60.64,101.32 60.64,100.67ZM77.73,89.44V100.67C77.73,101.32 77.9,101.82 78.25,102.16C78.59,102.5 79.05,102.66 79.62,102.66C80.9,102.66 81.55,101.84 81.55,100.18V86.06C81.55,85.47 81.4,85.01 81.1,84.67C80.8,84.33 80.4,84.16 79.9,84.16C79.46,84.16 79.17,84.24 79.01,84.39C78.85,84.53 78.51,84.93 77.98,85.57C77.46,86.21 76.86,86.79 76.17,87.31C75.5,87.83 74.6,88.32 73.47,88.79C72.71,89.1 72.18,89.36 71.88,89.55C71.58,89.75 71.43,90.06 71.43,90.48C71.43,90.84 71.58,91.16 71.88,91.44C72.19,91.71 72.54,91.85 72.93,91.85C73.76,91.85 75.35,91.05 77.73,89.44Z"
android:fillColor="#175DDC"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@ -74,4 +74,16 @@
<string name="language_change_x_description">The language has been changed to %1$s. Please restart the app to see the change</string>
<string name="show_website_icons">Show website icons</string>
<string name="show_website_icons_description">Show a recognizable image next to each login.</string>
<string name="secure_your_accounts_with_bitwarden_authenticator">Secure your accounts with Bitwarden Authneticator</string>
<string name="get_verification_codes_for_all_your_accounts">Get verification codes for all your accounts that support 2-step verification.</string>
<string name="use_your_device_camera_to_scan_codes">Use your device camera to scan codes</string>
<string name="scan_the_qr_code_in_your_2_step_verification_settings_for_any_account">Scan the QR code in your 2-step verification settings for any account.</string>
<string name="sign_in_using_unique_codes">Sign in using unique codes</string>
<string name="when_using_2_step_verification_youll_enter_your_username_and_password_and_a_code_generated_in_this_app">When using 2-step verification, youll enter your username and password and a code generated in this app.</string>
<string name="continue_button">Continue</string>
<string name="skip">Skip</string>
<string name="get_started">Get started</string>
<string name="unique_codes">Uniqe codes</string>
<string name="help">Help</string>
<string name="tutorial">Tutorial</string>
</resources>