Skip to content

Commit

Permalink
PM-13019: Add special circumstance to navigate to the vault listing U…
Browse files Browse the repository at this point in the history
…I for TOTP code (#4033)
  • Loading branch information
david-livefront authored Oct 7, 2024
1 parent 8d578a9 commit c4467f0
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 5 deletions.
7 changes: 7 additions & 0 deletions app/src/main/java/com/x8bit/bitwarden/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
Expand Down Expand Up @@ -232,6 +233,7 @@ class MainViewModel @Inject constructor(
val autofillSaveItem = intent.getAutofillSaveItemOrNull()
val autofillSelectionData = intent.getAutofillSelectionDataOrNull()
val shareData = intentManager.getShareDataFromIntent(intent)
val totpData = intent.getTotpDataOrNull()
val hasGeneratorShortcut = intent.isPasswordGeneratorShortcut
val hasVaultShortcut = intent.isMyVaultShortcut
val fido2CredentialRequestData = intent.getFido2CredentialRequestOrNull()
Expand Down Expand Up @@ -270,6 +272,11 @@ class MainViewModel @Inject constructor(
)
}

totpData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.AddTotpLoginItem(data = totpData)
}

shareData != null -> {
specialCircumstanceManager.specialCircumstance =
SpecialCircumstance.ShareNewSend(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.vault.model.TotpData
import kotlinx.parcelize.Parcelize

/**
* Represents a special circumstance the app may be in. These circumstances could require some kind
* of navigation that is counter to what otherwise may happen based on the state of the app.
*/
sealed class SpecialCircumstance : Parcelable {
/**
* The app was launched in order to add a new TOTP to a cipher.
*/
@Parcelize
data class AddTotpLoginItem(
val data: TotpData,
) : SpecialCircumstance()

/**
* The app was launched in order to create/share a new Send using the given [data].
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.ui.vault.model.TotpData

/**
* Returns [AutofillSaveItem] when contained in the given [SpecialCircumstance].
Expand Down Expand Up @@ -51,3 +52,12 @@ fun SpecialCircumstance.toFido2GetCredentialsRequestOrNull(): Fido2GetCredential
is SpecialCircumstance.Fido2GetCredentials -> this.fido2GetCredentialsRequest
else -> null
}

/**
* Returns the [TotpData] when contained in the given [SpecialCircumstance].
*/
fun SpecialCircumstance.toTotpDataOrNull(): TotpData? =
when (this) {
is SpecialCircumstance.AddTotpLoginItem -> this.data
else -> null
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ fun RootNavScreen(
is RootNavState.VaultUnlockedForAutofillSave,
is RootNavState.VaultUnlockedForAutofillSelection,
is RootNavState.VaultUnlockedForNewSend,
is RootNavState.VaultUnlockedForNewTotp,
is RootNavState.VaultUnlockedForAuthRequest,
is RootNavState.VaultUnlockedForFido2Save,
is RootNavState.VaultUnlockedForFido2Assertion,
Expand Down Expand Up @@ -197,6 +198,14 @@ fun RootNavScreen(
)
}

is RootNavState.VaultUnlockedForNewTotp -> {
navController.navigateToVaultUnlock(rootNavOptions)
navController.navigateToVaultItemListingAsRoot(
vaultItemListingType = VaultItemListingType.Login,
navOptions = rootNavOptions,
)
}

is RootNavState.VaultUnlockedForAutofillSave -> {
navController.navigateToVaultUnlockedGraph(rootNavOptions)
navController.navigateToVaultAddEdit(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ class RootNavViewModel @Inject constructor(
)
}

is SpecialCircumstance.AddTotpLoginItem -> {
RootNavState.VaultUnlockedForNewTotp(
activeUserId = userState.activeAccount.userId,
)
}

is SpecialCircumstance.ShareNewSend -> RootNavState.VaultUnlockedForNewSend

is SpecialCircumstance.PasswordlessRequest -> {
Expand Down Expand Up @@ -305,6 +311,14 @@ sealed class RootNavState : Parcelable {
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
) : RootNavState()

/**
* App should show the new verification codes listing screen for an unlocked user.
*/
@Parcelize
data class VaultUnlockedForNewTotp(
val activeUserId: String,
) : RootNavState()

/**
* App should show the new send screen for an unlocked user.
*/
Expand Down
68 changes: 68 additions & 0 deletions app/src/test/java/com/x8bit/bitwarden/MainViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppThem
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.util.isMyVaultShortcut
import com.x8bit.bitwarden.ui.platform.util.isPasswordGeneratorShortcut
import com.x8bit.bitwarden.ui.vault.model.TotpData
import com.x8bit.bitwarden.ui.vault.util.getTotpDataOrNull
import io.mockk.coEvery
import io.mockk.every
import io.mockk.just
Expand Down Expand Up @@ -119,6 +121,7 @@ class MainViewModelTest : BaseViewModelTest() {
@BeforeEach
fun setup() {
mockkStatic(
Intent::getTotpDataOrNull,
Intent::getPasswordlessRequestDataIntentOrNull,
Intent::getAutofillSaveItemOrNull,
Intent::getAutofillSelectionDataOrNull,
Expand All @@ -134,6 +137,7 @@ class MainViewModelTest : BaseViewModelTest() {
@AfterEach
fun tearDown() {
unmockkStatic(
Intent::getTotpDataOrNull,
Intent::getPasswordlessRequestDataIntentOrNull,
Intent::getAutofillSaveItemOrNull,
Intent::getAutofillSelectionDataOrNull,
Expand Down Expand Up @@ -294,12 +298,35 @@ class MainViewModelTest : BaseViewModelTest() {
}
}

@Suppress("MaxLineLength")
@Test
fun `on ReceiveFirstIntent with TOTP data should set the special circumstance to AddTotpLoginItem`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val totpData = mockk<TotpData>()
every { mockIntent.getTotpDataOrNull() } returns totpData
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false

viewModel.trySendAction(MainAction.ReceiveFirstIntent(intent = mockIntent))
assertEquals(
SpecialCircumstance.AddTotpLoginItem(data = totpData),
specialCircumstanceManager.specialCircumstance,
)
}

@Suppress("MaxLineLength")
@Test
fun `on ReceiveFirstIntent with share data should set the special circumstance to ShareNewSend`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
Expand Down Expand Up @@ -328,6 +355,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
Expand Down Expand Up @@ -359,6 +387,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns "token"
}
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
Expand Down Expand Up @@ -394,6 +423,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns "token"
}
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
Expand Down Expand Up @@ -431,6 +461,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns token
}
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
Expand Down Expand Up @@ -470,6 +501,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns token
}
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
Expand Down Expand Up @@ -511,6 +543,7 @@ class MainViewModelTest : BaseViewModelTest() {
every { verificationToken } returns token
}
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { getCompleteRegistrationDataIntentOrNull() } returns completeRegistrationData
Expand Down Expand Up @@ -548,6 +581,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSaveItem = mockk<AutofillSaveItem>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
Expand Down Expand Up @@ -578,6 +612,7 @@ class MainViewModelTest : BaseViewModelTest() {
every {
mockIntent.getPasswordlessRequestDataIntentOrNull()
} returns passwordlessRequestData
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
Expand Down Expand Up @@ -663,6 +698,7 @@ class MainViewModelTest : BaseViewModelTest() {
origin = "mockOrigin",
)
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
Expand Down Expand Up @@ -701,6 +737,7 @@ class MainViewModelTest : BaseViewModelTest() {
)
val mockIntent = mockk<Intent> {
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
Expand Down Expand Up @@ -773,6 +810,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val shareData = mockk<IntentManager.ShareData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
Expand All @@ -795,12 +833,35 @@ class MainViewModelTest : BaseViewModelTest() {
)
}

@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with TOTP data should set the special circumstance to AddTotpLoginItem`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val totpData = mockk<TotpData>()
every { mockIntent.getTotpDataOrNull() } returns totpData
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
every { intentManager.getShareDataFromIntent(mockIntent) } returns null
every { mockIntent.isMyVaultShortcut } returns false
every { mockIntent.isPasswordGeneratorShortcut } returns false

viewModel.trySendAction(MainAction.ReceiveNewIntent(intent = mockIntent))
assertEquals(
SpecialCircumstance.AddTotpLoginItem(data = totpData),
specialCircumstanceManager.specialCircumstance,
)
}

@Suppress("MaxLineLength")
@Test
fun `on ReceiveNewIntent with autofill data should set the special circumstance to AutofillSelection`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSelectionData = mockk<AutofillSelectionData>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
Expand Down Expand Up @@ -829,6 +890,7 @@ class MainViewModelTest : BaseViewModelTest() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent>()
val autofillSaveItem = mockk<AutofillSaveItem>()
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getPasswordlessRequestDataIntentOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns autofillSaveItem
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
Expand Down Expand Up @@ -859,6 +921,7 @@ class MainViewModelTest : BaseViewModelTest() {
every {
mockIntent.getPasswordlessRequestDataIntentOrNull()
} returns passwordlessRequestData
every { mockIntent.getTotpDataOrNull() } returns null
every { mockIntent.getAutofillSaveItemOrNull() } returns null
every { mockIntent.getAutofillSelectionDataOrNull() } returns null
every { mockIntent.getCompleteRegistrationDataIntentOrNull() } returns null
Expand All @@ -885,6 +948,7 @@ class MainViewModelTest : BaseViewModelTest() {
fun `on ReceiveNewIntent with a Vault deeplink data should set the special circumstance to VaultShortcut`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
Expand All @@ -910,6 +974,7 @@ class MainViewModelTest : BaseViewModelTest() {
fun `on ReceiveNewIntent with a password generator deeplink data should set the special circumstance to GeneratorShortcut`() {
val viewModel = createViewModel()
val mockIntent = mockk<Intent> {
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
Expand Down Expand Up @@ -1043,6 +1108,7 @@ private fun createMockFido2RegistrationIntent(
fido2CredentialRequest: Fido2CredentialRequest = createMockFido2CredentialRequest(number = 1),
): Intent = mockk<Intent> {
every { getFido2CredentialRequestOrNull() } returns fido2CredentialRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
Expand All @@ -1056,6 +1122,7 @@ private fun createMockFido2AssertionIntent(
createMockFido2CredentialAssertionRequest(number = 1),
): Intent = mockk<Intent> {
every { getFido2AssertionRequestOrNull() } returns fido2CredentialAssertionRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
Expand All @@ -1070,6 +1137,7 @@ private fun createMockFido2GetCredentialsIntent(
),
): Intent = mockk<Intent> {
every { getFido2GetCredentialsRequestOrNull() } returns fido2GetCredentialsRequest
every { getTotpDataOrNull() } returns null
every { getPasswordlessRequestDataIntentOrNull() } returns null
every { getAutofillSelectionDataOrNull() } returns null
every { getAutofillSaveItemOrNull() } returns null
Expand Down
Loading

0 comments on commit c4467f0

Please sign in to comment.