mirror of
https://github.com/bitwarden/android.git
synced 2025-02-22 16:49:13 +03:00
PM-16630 add logins action card
This commit is contained in:
parent
d573ce6e10
commit
a95fe46e73
14 changed files with 465 additions and 3 deletions
app/src
main
java/com/x8bit/bitwarden
data/platform
datasource/disk
manager
ui/vault/feature/addedit
res/values
test/java/com/x8bit/bitwarden
data/platform
datasource/disk
manager
ui/vault/feature/addedit
|
@ -361,4 +361,19 @@ 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 getHasSeenAddLoginCoachMark(): Boolean?
|
||||
|
||||
/**
|
||||
* Stores a value for if the Add Login CoachMark tour has been interacted with
|
||||
*/
|
||||
fun storeHasSeenAddLoginCoachMark(hasSeen: Boolean?)
|
||||
|
||||
/**
|
||||
* Returns an [Flow] to observe updates to the "HasSeenAddLoginCoachMark" value.
|
||||
*/
|
||||
fun getHasSeenAddLoginCoachMarkFlow(): Flow<Boolean?>
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ 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 HAS_SEEN_ADD_LOGIN_COACH_MARK = "hasSeenAddLoginCoachMark"
|
||||
|
||||
/**
|
||||
* Primary implementation of [SettingsDiskSource].
|
||||
|
@ -78,6 +79,8 @@ class SettingsDiskSourceImpl(
|
|||
|
||||
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
|
@ -185,6 +188,7 @@ class SettingsDiskSourceImpl(
|
|||
// - screen capture allowed
|
||||
// - show autofill setting badge
|
||||
// - show unlock setting badge
|
||||
// - has seen add login coach mark
|
||||
}
|
||||
|
||||
override fun getAccountBiometricIntegrityValidity(
|
||||
|
@ -486,6 +490,22 @@ class SettingsDiskSourceImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun getHasSeenAddLoginCoachMark(): Boolean? =
|
||||
getBoolean(key = HAS_SEEN_ADD_LOGIN_COACH_MARK)
|
||||
|
||||
override fun storeHasSeenAddLoginCoachMark(hasSeen: Boolean?) {
|
||||
putBoolean(
|
||||
key = HAS_SEEN_ADD_LOGIN_COACH_MARK,
|
||||
value = hasSeen,
|
||||
)
|
||||
mutableHasSeenAddLoginCoachMarkFlow.tryEmit(hasSeen)
|
||||
}
|
||||
|
||||
override fun getHasSeenAddLoginCoachMarkFlow(): Flow<Boolean?> =
|
||||
mutableHasSeenAddLoginCoachMarkFlow.onSubscription {
|
||||
emit(getBoolean(key = HAS_SEEN_ADD_LOGIN_COACH_MARK))
|
||||
}
|
||||
|
||||
private fun getMutableLastSyncFlow(
|
||||
userId: String,
|
||||
): MutableSharedFlow<Instant?> =
|
||||
|
|
|
@ -39,6 +39,11 @@ 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 hasSeenAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
|
||||
/**
|
||||
* Get the current [FirstTimeState] of the active user if available, otherwise return
|
||||
* a default configuration.
|
||||
|
@ -66,4 +71,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 hasSeenAddLoginCoachMarkTour()
|
||||
}
|
||||
|
|
|
@ -154,6 +154,13 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
|||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
override val hasSeenAddLoginCoachMarkFlow: Flow<Boolean>
|
||||
get() = settingsDiskSource
|
||||
.getHasSeenAddLoginCoachMarkFlow()
|
||||
// default value of false.
|
||||
.map { it ?: false }
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
* Get the current [FirstTimeState] of the active user if available, otherwise return
|
||||
* a default configuration.
|
||||
|
@ -211,6 +218,10 @@ class FirstTimeActionManagerImpl @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun hasSeenAddLoginCoachMarkTour() {
|
||||
settingsDiskSource.storeHasSeenAddLoginCoachMark(hasSeen = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation to get a flow of the showImportLogins value which takes
|
||||
* into account if the vault is empty.
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
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.coachmark.CoachMarkScope
|
||||
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
|
||||
|
@ -50,6 +51,7 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
|||
onPreviousCoachMark: () -> Unit,
|
||||
onCoachMarkTourComplete: () -> Unit,
|
||||
onCoachMarkDismissed: () -> Unit,
|
||||
shouldShowLearnAboutLoginsCard: Boolean,
|
||||
) {
|
||||
val launcher = permissionsManager.getLauncher(
|
||||
onResult = { isGranted ->
|
||||
|
@ -79,6 +81,21 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
|||
}
|
||||
}
|
||||
|
||||
if (shouldShowLearnAboutLoginsCard) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenActionCard(
|
||||
cardTitle = stringResource(R.string.learn_about_new_logins),
|
||||
cardSubtitle = stringResource(
|
||||
R.string.we_ll_walk_you_through_the_key_features_to_add_a_new_login,
|
||||
),
|
||||
actionText = stringResource(R.string.get_started),
|
||||
onActionClick = loginItemTypeHandlers.onStartLoginCoachMarkTour,
|
||||
onDismissClick = loginItemTypeHandlers.onDismissLearnAboutLoginsCard,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenListHeaderText(
|
||||
|
|
|
@ -384,6 +384,7 @@ fun VaultAddEditScreen(
|
|||
coachMarkState.coachingComplete(onComplete = scrollBackToTop)
|
||||
},
|
||||
onCoachMarkDismissed = scrollBackToTop,
|
||||
shouldShowLearnAboutLoginsCard = state.shouldShowLearnAboutNewLogins,
|
||||
modifier = Modifier
|
||||
.imePadding()
|
||||
.fillMaxSize(),
|
||||
|
|
|
@ -15,10 +15,13 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialReques
|
|||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.UserVerificationRequirement
|
||||
import com.x8bit.bitwarden.data.autofill.util.isActiveWithFido2Credentials
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSaveItemOrNull
|
||||
|
@ -63,6 +66,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCollection
|
|||
import com.x8bit.bitwarden.ui.vault.model.VaultIdentityTitle
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultLinkedFieldType
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -103,6 +107,8 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
private val clock: Clock,
|
||||
private val organizationEventManager: OrganizationEventManager,
|
||||
private val networkConnectionManager: NetworkConnectionManager,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : BaseViewModel<VaultAddEditState, VaultAddEditEvent, VaultAddEditAction>(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
|
@ -170,6 +176,7 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
shouldShowCloseButton = autofillSaveItem == null && fido2AttestationOptions == null,
|
||||
shouldExitOnSave = shouldExitOnSave,
|
||||
supportedItemTypes = getSupportedItemTypeOptions(),
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -211,6 +218,20 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
firstTimeActionManager
|
||||
.hasSeenAddLoginCoachMarkFlow
|
||||
.combine(
|
||||
featureFlagManager.getFeatureFlagFlow(key = FlagKey.OnboardingFlow),
|
||||
) { hasSeenTour, onboardFeatureFlag ->
|
||||
// The result of this will hide the card if the tour has been shown or if the
|
||||
// onboarding flow feature is off.
|
||||
VaultAddEditAction.Internal.HasSeenAddLoginCoachMarkValueChangeReceive(
|
||||
newValue = hasSeenTour || !onboardFeatureFlag,
|
||||
)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: VaultAddEditAction) {
|
||||
|
@ -967,9 +988,26 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
VaultAddEditAction.ItemType.LoginType.ClearFido2CredentialClick -> {
|
||||
handleLoginClearFido2Credential()
|
||||
}
|
||||
|
||||
VaultAddEditAction.ItemType.LoginType.LearnAboutLoginsDismissed -> {
|
||||
handleLearnAboutLoginsDismissed()
|
||||
}
|
||||
|
||||
VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins -> {
|
||||
handleStartLearnAboutLogins()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStartLearnAboutLogins() {
|
||||
firstTimeActionManager.hasSeenAddLoginCoachMarkTour()
|
||||
sendEvent(VaultAddEditEvent.StartAddLoginItemCoachMarkTour)
|
||||
}
|
||||
|
||||
private fun handleLearnAboutLoginsDismissed() {
|
||||
firstTimeActionManager.hasSeenAddLoginCoachMarkTour()
|
||||
}
|
||||
|
||||
private fun handleLoginUsernameTextInputChange(
|
||||
action: VaultAddEditAction.ItemType.LoginType.UsernameTextChange,
|
||||
) {
|
||||
|
@ -1445,6 +1483,20 @@ class VaultAddEditViewModel @Inject constructor(
|
|||
is VaultAddEditAction.Internal.ValidateFido2PinResultReceive -> {
|
||||
handleValidateFido2PinResultReceive(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Internal.HasSeenAddLoginCoachMarkValueChangeReceive -> {
|
||||
handleHasSeenAddLoginCoachMarkValueChange(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHasSeenAddLoginCoachMarkValueChange(
|
||||
action: VaultAddEditAction.Internal.HasSeenAddLoginCoachMarkValueChangeReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
hasSeenLearnAboutLoginsCard = action.newValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1992,6 +2044,7 @@ data class VaultAddEditState(
|
|||
// Internal
|
||||
val shouldExitOnSave: Boolean = false,
|
||||
val totpData: TotpData? = null,
|
||||
private val hasSeenLearnAboutLoginsCard: Boolean,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
|
@ -2040,6 +2093,11 @@ data class VaultAddEditState(
|
|||
?.canAssignToCollections
|
||||
?: false
|
||||
|
||||
val shouldShowLearnAboutNewLogins: Boolean
|
||||
get() = !hasSeenLearnAboutLoginsCard &&
|
||||
((viewState as? ViewState.Content)?.type is ViewState.Content.ItemType.Login) &&
|
||||
isAddItemMode
|
||||
|
||||
/**
|
||||
* Enum representing the main type options for the vault, such as LOGIN, CARD, etc.
|
||||
*
|
||||
|
@ -2850,6 +2908,16 @@ sealed class VaultAddEditAction {
|
|||
* Represents the action to clear the fido2 credential.
|
||||
*/
|
||||
data object ClearFido2CredentialClick : LoginType()
|
||||
|
||||
/**
|
||||
* User has clicked the call to action on the learn about logins card.
|
||||
*/
|
||||
data object StartLearnAboutLogins : LoginType()
|
||||
|
||||
/**
|
||||
* User has dismissed the learn about logins card.
|
||||
*/
|
||||
data object LearnAboutLoginsDismissed : LoginType()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3138,6 +3206,13 @@ sealed class VaultAddEditAction {
|
|||
data class ValidateFido2PinResultReceive(
|
||||
val result: ValidatePinResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* The value for the hasSeenAddLoginCoachMark has changed.
|
||||
*/
|
||||
data class HasSeenAddLoginCoachMarkValueChangeReceive(
|
||||
val newValue: Boolean,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,8 @@ data class VaultAddEditLoginTypeHandlers(
|
|||
val onAddNewUriClick: () -> Unit,
|
||||
val onPasswordVisibilityChange: (Boolean) -> Unit,
|
||||
val onClearFido2CredentialClick: () -> Unit,
|
||||
val onStartLoginCoachMarkTour: () -> Unit,
|
||||
val onDismissLearnAboutLoginsCard: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
|
@ -123,6 +125,16 @@ data class VaultAddEditLoginTypeHandlers(
|
|||
VaultAddEditAction.ItemType.LoginType.ClearFido2CredentialClick,
|
||||
)
|
||||
},
|
||||
onStartLoginCoachMarkTour = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins,
|
||||
)
|
||||
},
|
||||
onDismissLearnAboutLoginsCard = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.LoginType.LearnAboutLoginsDismissed,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1121,4 +1121,6 @@ Do you want to switch to this account?</string>
|
|||
<string name="mutual_tls">Mutual TLS</string>
|
||||
<string name="single_tap_passkey_creation">Single tap passkey creation</string>
|
||||
<string name="single_tap_passkey_authentication">Single tap passkey sign-on</string>
|
||||
<string name="learn_about_new_logins">Learn about new logins</string>
|
||||
<string name="we_ll_walk_you_through_the_key_features_to_add_a_new_login">We\'ll walk you through the key features to add a new login.</string>
|
||||
</resources>
|
||||
|
|
|
@ -1253,4 +1253,34 @@ class SettingsDiskSourceTest {
|
|||
settingsDiskSource.storeCreateSendActionCount(count = 1)
|
||||
assertEquals(1, fakeSharedPreferences.getInt(createActionCountKey, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHasSeenAddLoginCoachMark should pull value from SharedPreferences`() {
|
||||
val hasSeenAddLoginCoachMarkKey = "bwPreferencesStorage:hasSeenAddLoginCoachMark"
|
||||
fakeSharedPreferences.edit { putBoolean(hasSeenAddLoginCoachMarkKey, true) }
|
||||
assertTrue(settingsDiskSource.getHasSeenAddLoginCoachMark() == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storeHasSeenAddLoginCoachMark should update SharedPreferences`() {
|
||||
val hasSeenAddLoginCoachMarkKey = "bwPreferencesStorage:hasSeenAddLoginCoachMark"
|
||||
settingsDiskSource.storeHasSeenAddLoginCoachMark(hasSeen = true)
|
||||
assertTrue(
|
||||
fakeSharedPreferences.getBoolean(
|
||||
key = hasSeenAddLoginCoachMarkKey,
|
||||
defaultValue = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHasSeenAddLoginCoachMarkFlow emits changes to stored value`() = runTest {
|
||||
settingsDiskSource.getHasSeenAddLoginCoachMarkFlow().test {
|
||||
assertNull(awaitItem())
|
||||
settingsDiskSource.storeHasSeenAddLoginCoachMark(hasSeen = false)
|
||||
assertFalse(awaitItem() ?: true)
|
||||
settingsDiskSource.storeHasSeenAddLoginCoachMark(hasSeen = true)
|
||||
assertTrue(awaitItem() ?: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,8 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
private val mutableHasUserLoggedInOrCreatedAccount =
|
||||
bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableHasSeenAddLoginCoachMarkFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||
|
||||
private val mutableScreenCaptureAllowedFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
||||
|
@ -70,6 +72,7 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
private var addCipherActionCount: Int? = null
|
||||
private var generatedActionCount: Int? = null
|
||||
private var createSendActionCount: Int? = null
|
||||
private var hasSeenAddLoginCoachMark: Boolean? = null
|
||||
|
||||
private val mutableShowAutoFillSettingBadgeFlowMap =
|
||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||
|
@ -390,6 +393,20 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
|||
createSendActionCount = count
|
||||
}
|
||||
|
||||
override fun getHasSeenAddLoginCoachMark(): Boolean? {
|
||||
return hasSeenAddLoginCoachMark
|
||||
}
|
||||
|
||||
override fun storeHasSeenAddLoginCoachMark(hasSeen: Boolean?) {
|
||||
hasSeenAddLoginCoachMark = hasSeen
|
||||
mutableHasSeenAddLoginCoachMarkFlow.tryEmit(hasSeen)
|
||||
}
|
||||
|
||||
override fun getHasSeenAddLoginCoachMarkFlow(): Flow<Boolean?> =
|
||||
mutableHasSeenAddLoginCoachMarkFlow.onSubscription {
|
||||
emit(getHasSeenAddLoginCoachMark())
|
||||
}
|
||||
|
||||
//region Private helper functions
|
||||
private fun getMutableScreenCaptureAllowedFlow(userId: String): MutableSharedFlow<Boolean?> {
|
||||
return mutableScreenCaptureAllowedFlowMap.getOrPut(userId) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
|
@ -296,6 +297,23 @@ class FirstTimeActionManagerTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasSeenAddLoginCoachMarkFlow updates when disk source updates`() = runTest {
|
||||
firstTimeActionManager.hasSeenAddLoginCoachMarkFlow.test {
|
||||
// null will be mapped to false
|
||||
assertFalse(awaitItem())
|
||||
fakeSettingsDiskSource.storeHasSeenAddLoginCoachMark(hasSeen = true)
|
||||
assertTrue(awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasSeenAddLoginCoachMarkTour sets the value to true in the disk source`() {
|
||||
assertNull(fakeSettingsDiskSource.getHasSeenAddLoginCoachMark())
|
||||
firstTimeActionManager.hasSeenAddLoginCoachMarkTour()
|
||||
assertTrue(fakeSettingsDiskSource.getHasSeenAddLoginCoachMark() == true)
|
||||
}
|
||||
}
|
||||
|
||||
private const val USER_ID: String = "userId"
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.test.click
|
|||
import androidx.compose.ui.test.filter
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.hasAnySibling
|
||||
import androidx.compose.ui.test.hasContentDescription
|
||||
import androidx.compose.ui.test.hasSetTextAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
|
@ -448,19 +449,26 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `close button should update according to state`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithContentDescription("Close")
|
||||
.assertCountEquals(2)
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowCloseButton = false)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithContentDescription("Close").assertDoesNotExist()
|
||||
composeTestRule
|
||||
.onAllNodesWithContentDescription("Close")
|
||||
.assertCountEquals(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking close button should send CloseClick action`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription(label = "Close")
|
||||
.onNode(
|
||||
hasContentDescription("Close")
|
||||
and !hasAnySibling(hasText("Learn about new logins")),
|
||||
)
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
|
@ -3560,6 +3568,101 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `learn about add logins card should show when state is add mode, login type content, and hasSeenTour is false`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about new logins")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `learn about add logins card should not show when state is add mode, login type content, and hasSeenTour is true`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(
|
||||
vaultItemCipherType = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
hasSeenLearnAboutLoginsCard = true,
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about new logins")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `learn about add logins card should not show when state is edit mode, login type content, and hasSeenTour is false`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
vaultAddEditType = VaultAddEditType.EditItem(vaultItemId = "1234"),
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about new logins")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `learn about add logins card should not show when state is add mode, card type content, and hasSeenTour is false`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_CARD.copy(
|
||||
vaultAddEditType = VaultAddEditType.EditItem(vaultItemId = "1234"),
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Learn about new logins")
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when learn about logins card is showing, clicking the close button sends LearnAboutLoginsDismissed action`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNode(
|
||||
hasContentDescription("Close")
|
||||
and hasAnySibling(hasText("Learn about new logins")),
|
||||
)
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.LoginType.LearnAboutLoginsDismissed,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when learn about logins card is showing, clicking the call to action sends StartLearnAboutLogins action`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_LOGIN.copy(
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Get started")
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region Helper functions
|
||||
|
||||
private fun updateLoginType(
|
||||
|
@ -3685,6 +3788,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
dialog = VaultAddEditState.DialogState.Generic(message = "test".asText()),
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
|
||||
|
@ -3696,6 +3800,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
),
|
||||
dialog = null,
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
|
||||
|
@ -3707,6 +3812,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
),
|
||||
dialog = null,
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_CARD = VaultAddEditState(
|
||||
|
@ -3718,6 +3824,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
),
|
||||
dialog = null,
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
|
@ -3740,6 +3847,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
dialog = null,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SECURE_NOTE),
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
|
||||
|
@ -3751,6 +3859,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
),
|
||||
dialog = null,
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SSH_KEYS = VaultAddEditState(
|
||||
|
@ -3762,6 +3871,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
),
|
||||
dialog = null,
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
private val ALTERED_COLLECTIONS = listOf(
|
||||
|
|
|
@ -25,12 +25,15 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CreateCreden
|
|||
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
|
||||
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
|
||||
|
@ -87,10 +90,13 @@ import io.mockk.unmockkStatic
|
|||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -163,6 +169,20 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
every { isNetworkConnected } returns true
|
||||
}
|
||||
|
||||
private val mutableHasSeenAddLoginCoachMarkFlow = MutableStateFlow(false)
|
||||
private val firstTimeActionManager = mockk<FirstTimeActionManager> {
|
||||
every { hasSeenAddLoginCoachMarkTour() } just runs
|
||||
every { hasSeenAddLoginCoachMarkFlow } returns mutableHasSeenAddLoginCoachMarkFlow
|
||||
}
|
||||
|
||||
// Feature flag default to be enabled.
|
||||
private val mutableOnboardingFeatureFlagFlow = MutableStateFlow(true)
|
||||
private val featureFlagManager = mockk<FeatureFlagManager> {
|
||||
every {
|
||||
getFeatureFlagFlow(FlagKey.OnboardingFlow)
|
||||
} returns mutableOnboardingFeatureFlagFlow
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockkStatic(CipherView::toViewState)
|
||||
|
@ -191,6 +211,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
shouldExitOnSave = false,
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries
|
||||
.filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS },
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
|
@ -273,6 +294,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
dialog = null,
|
||||
supportedItemTypes = VaultAddEditState.ItemTypeOption.entries
|
||||
.filter { it != VaultAddEditState.ItemTypeOption.SSH_KEYS },
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
|
@ -2601,6 +2623,103 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when OnboardFlow feature flag is off, shouldShowLearnAboutNewLogins should be false`() {
|
||||
mutableOnboardingFeatureFlagFlow.update { true }
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = createVaultAddItemState(
|
||||
typeContentViewState = createLoginTypeContentViewState(),
|
||||
),
|
||||
vaultAddEditType = VaultAddEditType.AddItem(
|
||||
vaultItemCipherType = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertTrue(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
|
||||
|
||||
mutableOnboardingFeatureFlagFlow.update { false }
|
||||
assertFalse(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when first time action manager has seen logins tour value updates to true shouldShowLearnAboutNewLogins should update to false`() {
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = createVaultAddItemState(
|
||||
typeContentViewState = createLoginTypeContentViewState(),
|
||||
),
|
||||
vaultAddEditType = VaultAddEditType.AddItem(
|
||||
vaultItemCipherType = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertTrue(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
|
||||
mutableHasSeenAddLoginCoachMarkFlow.update { true }
|
||||
assertFalse(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when first time action manager value is false, but type content is not login shouldShowLearnAboutNewLogins should be false`() {
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = createVaultAddItemState(
|
||||
typeContentViewState = createLoginTypeContentViewState(),
|
||||
),
|
||||
vaultAddEditType = VaultAddEditType.AddItem(
|
||||
vaultItemCipherType = VaultItemCipherType.LOGIN,
|
||||
),
|
||||
),
|
||||
)
|
||||
assertTrue(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.TypeOptionSelect(
|
||||
VaultAddEditState.ItemTypeOption.SSH_KEYS,
|
||||
),
|
||||
)
|
||||
assertFalse(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when first time action manager value is false, but edit type is EditItem shouldShowLearnAboutNewLogins should be false`() {
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
state = createVaultAddItemState(
|
||||
vaultAddEditType = VaultAddEditType.EditItem(vaultItemId = "1234"),
|
||||
typeContentViewState = createLoginTypeContentViewState(),
|
||||
),
|
||||
vaultAddEditType = VaultAddEditType.EditItem(
|
||||
vaultItemId = "1234",
|
||||
),
|
||||
),
|
||||
)
|
||||
assertFalse(viewModel.stateFlow.value.shouldShowLearnAboutNewLogins)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `LearnAboutLoginsDismissed action calls first time action manager hasSeenAddLoginCoachMarkTour called`() {
|
||||
val viewModel = createAddVaultItemViewModel()
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.LearnAboutLoginsDismissed)
|
||||
verify(exactly = 1) { firstTimeActionManager.hasSeenAddLoginCoachMarkTour() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `LearnAboutLoginsDismissed action calls first time action manager hasSeenAddLoginCoachMarkTour called and show coach mark event sent`() =
|
||||
runTest {
|
||||
val viewModel = createAddVaultItemViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.StartLearnAboutLogins)
|
||||
assertEquals(VaultAddEditEvent.StartAddLoginItemCoachMarkTour, awaitItem())
|
||||
}
|
||||
verify(exactly = 1) { firstTimeActionManager.hasSeenAddLoginCoachMarkTour() }
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class VaultAddEditIdentityTypeItemActions {
|
||||
private lateinit var viewModel: VaultAddEditViewModel
|
||||
|
@ -3126,6 +3245,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
clock = fixedClock,
|
||||
organizationEventManager = organizationEventManager,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4276,6 +4397,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
shouldExitOnSave = shouldExitOnSave,
|
||||
totpData = totpData,
|
||||
supportedItemTypes = supportedItemTypes,
|
||||
hasSeenLearnAboutLoginsCard = false,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
@ -4385,6 +4507,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
|||
clock = clock,
|
||||
organizationEventManager = organizationEventManager,
|
||||
networkConnectionManager = networkConnectionManager,
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
|
||||
private fun createVaultData(
|
||||
|
|
Loading…
Add table
Reference in a new issue