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-13019: Add special circumstance to navigate to the vault listing UI for TOTP code #4033

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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 @@
)
}

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 @@
val fido2GetCredentialsRequest: Fido2GetCredentialsRequest,
) : RootNavState()

/**
* App should show the new verification codes listing screen for an unlocked user.
*/
@Parcelize

Check warning on line 317 in app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt#L317

Added line #L317 was not covered by tests
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 share 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 share 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