Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PM-16630 PM-16621 Add logins action card and add explore generator card to be able to trigger coach marks #4616

Merged
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -361,4 +361,34 @@ interface SettingsDiskSource {
* Stores the given [count] completed create send actions for the device.
*/
fun storeCreateSendActionCount(count: Int?)

/**
* Gets the Boolean value of if the Add Login CoachMark tour has been interacted with.
*/
fun getShouldShowAddLoginCoachMark(): Boolean?

/**
* Stores a value for if the Add Login CoachMark tour has been interacted with
*/
fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?)

/**
* Returns an [Flow] to observe updates to the "ShouldShowAddLoginCoachMark" value.
*/
fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?>

/**
* Gets the Boolean value of if the Generator CoachMark tour has been interacted with.
*/
fun getShouldShowGeneratorCoachMark(): Boolean?

/**
* Stores a value for if the Generator CoachMark tour has been interacted with
*/
fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?)

/**
* Returns an [Flow] to observe updates to the "ShouldShowGeneratorCoachMark" value.
*/
fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ private const val IS_VAULT_REGISTERED_FOR_EXPORT = "isVaultRegisteredForExport"
private const val ADD_ACTION_COUNT = "addActionCount"
private const val COPY_ACTION_COUNT = "copyActionCount"
private const val CREATE_ACTION_COUNT = "createActionCount"
private const val SHOULD_SHOW_ADD_LOGIN_COACH_MARK = "shouldShowAddLoginCoachMark"
private const val SHOULD_SHOW_GENERATOR_COACH_MARK = "shouldShowGeneratorCoachMark"

/**
* Primary implementation of [SettingsDiskSource].
Expand Down Expand Up @@ -78,6 +80,10 @@ class SettingsDiskSourceImpl(

private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()

private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()

private val mutableHasSeenGeneratorCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()

private val mutableScreenCaptureAllowedFlowMap =
mutableMapOf<String, MutableSharedFlow<Boolean?>>()

Expand Down Expand Up @@ -185,6 +191,8 @@ class SettingsDiskSourceImpl(
// - screen capture allowed
// - show autofill setting badge
// - show unlock setting badge
// - should show add login coach mark
// - should show generator coach mark
}

override fun getAccountBiometricIntegrityValidity(
Expand Down Expand Up @@ -486,6 +494,38 @@ class SettingsDiskSourceImpl(
)
}

override fun getShouldShowAddLoginCoachMark(): Boolean? =
getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK)

override fun storeShouldShowAddLoginCoachMark(shouldShow: Boolean?) {
putBoolean(
key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK,
value = shouldShow,
)
mutableHasSeenAddLoginCoachMarkFlow.tryEmit(shouldShow)
}

override fun getShouldShowAddLoginCoachMarkFlow(): Flow<Boolean?> =
mutableHasSeenAddLoginCoachMarkFlow.onSubscription {
emit(getBoolean(key = SHOULD_SHOW_ADD_LOGIN_COACH_MARK))
}

override fun getShouldShowGeneratorCoachMark(): Boolean? =
getBoolean(key = SHOULD_SHOW_GENERATOR_COACH_MARK)

override fun storeShouldShowGeneratorCoachMark(shouldShow: Boolean?) {
putBoolean(
key = SHOULD_SHOW_GENERATOR_COACH_MARK,
value = shouldShow,
)
mutableHasSeenGeneratorCoachMarkFlow.tryEmit(shouldShow)
}

override fun getShouldShowGeneratorCoachMarkFlow(): Flow<Boolean?> =
mutableHasSeenGeneratorCoachMarkFlow.onSubscription {
emit(getShouldShowGeneratorCoachMark())
}

private fun getMutableLastSyncFlow(
userId: String,
): MutableSharedFlow<Instant?> =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.manager

import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -39,6 +40,17 @@ interface FirstTimeActionManager {
*/
val firstTimeStateFlow: Flow<FirstTimeState>

/**
* Returns observable flow of if a user on the device has seen the Add Login coach mark tour.
*/
val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>

/**
* Returns observable flow of if a user on the device has seen the Generator screen
* coach mark tour.
*/
val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>

/**
* Get the current [FirstTimeState] of the active user if available, otherwise return
* a default configuration.
Expand Down Expand Up @@ -66,4 +78,9 @@ interface FirstTimeActionManager {
* Update the value of the showImportLoginsSettingsBadge status for the active user.
*/
fun storeShowImportLoginsSettingsBadge(showBadge: Boolean)

/**
* Can be called to indicate that a user has seen the AddLogin coach mark tour.
*/
fun markCoachMarkTourCompleted(tourCompleted: CoachMarkTourType)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.repository.util.activeUserIdChangesFlow
import com.x8bit.bitwarden.data.autofill.manager.AutofillEnabledManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.model.CoachMarkTourType
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
Expand Down Expand Up @@ -154,6 +155,30 @@ class FirstTimeActionManagerImpl @Inject constructor(
}
.distinctUntilChanged()

override val shouldShowAddLoginCoachMarkFlow: Flow<Boolean>
get() = settingsDiskSource
.getShouldShowAddLoginCoachMarkFlow()
.map { it ?: true }
.combine(
phil-livefront marked this conversation as resolved.
Show resolved Hide resolved
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
) { shouldShow, featureIsEnabled ->
// If the feature flag is off always return true so observers know
// the card has not been shown.
shouldShow && featureIsEnabled
}
.distinctUntilChanged()

override val shouldShowGeneratorCoachMarkFlow: Flow<Boolean>
get() = settingsDiskSource
.getShouldShowGeneratorCoachMarkFlow()
.map { it ?: true }
.combine(
featureFlagManager.getFeatureFlagFlow(FlagKey.OnboardingFlow),
) { shouldShow, featureFlagEnabled ->
shouldShow && featureFlagEnabled
}
.distinctUntilChanged()

/**
* Get the current [FirstTimeState] of the active user if available, otherwise return
* a default configuration.
Expand Down Expand Up @@ -211,6 +236,18 @@ class FirstTimeActionManagerImpl @Inject constructor(
)
}

override fun markCoachMarkTourCompleted(tourCompleted: CoachMarkTourType) {
when (tourCompleted) {
CoachMarkTourType.ADD_LOGIN -> {
phil-livefront marked this conversation as resolved.
Show resolved Hide resolved
settingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = false)
}

CoachMarkTourType.GENERATOR -> {
settingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = false)
}
}
}

/**
* Internal implementation to get a flow of the showImportLogins value which takes
* into account if the vault is empty.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.x8bit.bitwarden.data.platform.manager.model

/**
* Enumerated values to represent all the possible coach mark tours that can be
* completed.
*
* @see com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
*/
enum class CoachMarkTourType {
ADD_LOGIN,
GENERATOR,
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItem
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenPasswordField
Expand Down Expand Up @@ -319,6 +320,22 @@ private fun ScrollContent(
Spacer(modifier = Modifier.height(12.dp))
}

if (state.shouldShowExploreGeneratorCard) {
BitwardenActionCard(
cardTitle = stringResource(R.string.explore_the_generator),
cardSubtitle = stringResource(
R.string.learn_more_about_generating_secure_login_credentials_with_guided_tour,
),
actionText = stringResource(R.string.get_started),
onActionClick = passwordHandlers.onGeneratorActionCardClicked,
onDismissClick = passwordHandlers.onGeneratorActionCardDismissed,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(modifier = Modifier.height(24.dp))
}

GeneratedStringItem(
generatedText = state.generatedText,
onRegenerateClick = onRegenerateClick,
Expand Down
Loading