Skip to content

Commit e610a75

Browse files
authored
[PM-27001] Skip account selection only one exists on cxp flow (#6055)
1 parent ae4b398 commit e610a75

File tree

8 files changed

+208
-7
lines changed

8 files changed

+208
-7
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toVaultItemCipherType
6868
import com.x8bit.bitwarden.ui.vault.feature.exportitems.ExportItemsGraphRoute
6969
import com.x8bit.bitwarden.ui.vault.feature.exportitems.exportItemsGraph
7070
import com.x8bit.bitwarden.ui.vault.feature.exportitems.navigateToExportItemsGraph
71+
import com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword.navigateToVerifyPassword
7172
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.navigateToVaultItemListingAsRoot
7273
import com.x8bit.bitwarden.ui.vault.model.VaultAddEditType
7374
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
@@ -142,7 +143,10 @@ fun RootNavScreen(
142143
is RootNavState.VaultUnlockedForProviderGetCredentials,
143144
-> VaultUnlockedGraphRoute
144145

145-
is RootNavState.CredentialExchangeExport -> ExportItemsGraphRoute
146+
is RootNavState.CredentialExchangeExport,
147+
is RootNavState.CredentialExchangeExportSkipAccountSelection,
148+
-> ExportItemsGraphRoute
149+
146150
RootNavState.OnboardingAccountLockSetup -> SetupUnlockRoute.AsRoot
147151
RootNavState.OnboardingAutoFillSetup -> SetupAutofillRoute.AsRoot
148152
RootNavState.OnboardingBrowserAutofillSetup -> SetupBrowserAutofillRoute.AsRoot
@@ -288,6 +292,13 @@ fun RootNavScreen(
288292
is RootNavState.CredentialExchangeExport -> {
289293
navController.navigateToExportItemsGraph(rootNavOptions)
290294
}
295+
296+
is RootNavState.CredentialExchangeExportSkipAccountSelection -> {
297+
navController.navigateToVerifyPassword(
298+
userId = currentState.userId,
299+
navOptions = rootNavOptions,
300+
)
301+
}
291302
}
292303
}
293304
}

app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class RootNavViewModel @Inject constructor(
5959
}
6060
}
6161

62-
@Suppress("CyclomaticComplexMethod", "MaxLineLength", "LongMethod")
62+
@Suppress("CyclomaticComplexMethod", "LongMethod")
6363
private fun handleUserStateUpdateReceive(
6464
action: RootNavAction.Internal.UserStateUpdateReceive,
6565
) {
@@ -89,7 +89,13 @@ class RootNavViewModel @Inject constructor(
8989
}
9090

9191
specialCircumstance is SpecialCircumstance.CredentialExchangeExport -> {
92-
RootNavState.CredentialExchangeExport
92+
if (userState.accounts.size == 1) {
93+
RootNavState.CredentialExchangeExportSkipAccountSelection(
94+
userId = userState.accounts.first().userId,
95+
)
96+
} else {
97+
RootNavState.CredentialExchangeExport
98+
}
9399
}
94100

95101
userState.activeAccount.isVaultUnlocked &&
@@ -424,6 +430,14 @@ sealed class RootNavState : Parcelable {
424430
*/
425431
@Parcelize
426432
data object CredentialExchangeExport : RootNavState()
433+
434+
/**
435+
* App should begin the export items flow, skipping the account selection screen.
436+
*/
437+
@Parcelize
438+
data class CredentialExchangeExportSkipAccountSelection(
439+
val userId: String,
440+
) : RootNavState()
427441
}
428442

429443
/**

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/verifypassword/VerifyPasswordScreen.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ import androidx.compose.ui.text.input.KeyboardType
2323
import androidx.compose.ui.text.style.TextAlign
2424
import androidx.compose.ui.tooling.preview.Preview
2525
import androidx.compose.ui.unit.dp
26+
import androidx.credentials.providerevents.exception.ImportCredentialsCancellationException
2627
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
2728
import androidx.lifecycle.compose.collectAsStateWithLifecycle
29+
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
30+
import com.bitwarden.cxf.manager.model.ExportCredentialsResult
31+
import com.bitwarden.cxf.ui.composition.LocalCredentialExchangeCompletionManager
2832
import com.bitwarden.ui.platform.base.util.EventsEffect
2933
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
3034
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
@@ -54,6 +58,8 @@ fun VerifyPasswordScreen(
5458
onNavigateBack: () -> Unit,
5559
onPasswordVerified: (userId: String) -> Unit,
5660
viewModel: VerifyPasswordViewModel = hiltViewModel(),
61+
credentialExchangeCompletionManager: CredentialExchangeCompletionManager =
62+
LocalCredentialExchangeCompletionManager.current,
5763
snackbarHostState: BitwardenSnackbarHostState = rememberBitwardenSnackbarHostState(),
5864
) {
5965
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@@ -63,6 +69,16 @@ fun VerifyPasswordScreen(
6369
EventsEffect(viewModel) { event ->
6470
when (event) {
6571
VerifyPasswordEvent.NavigateBack -> onNavigateBack()
72+
VerifyPasswordEvent.CancelExport -> {
73+
credentialExchangeCompletionManager
74+
.completeCredentialExport(
75+
exportResult = ExportCredentialsResult.Failure(
76+
error = ImportCredentialsCancellationException(
77+
errorMessage = "User cancelled import.",
78+
),
79+
),
80+
)
81+
}
6682

6783
is VerifyPasswordEvent.PasswordVerified -> {
6884
onPasswordVerified(event.userId)
@@ -81,7 +97,11 @@ fun VerifyPasswordScreen(
8197

8298
ExportItemsScaffold(
8399
navIcon = rememberVectorPainter(
84-
BitwardenDrawable.ic_back,
100+
id = if (state.hasOtherAccounts) {
101+
BitwardenDrawable.ic_back
102+
} else {
103+
BitwardenDrawable.ic_close
104+
},
85105
),
86106
onNavigationIconClick = handler.onNavigateBackClick,
87107
navigationIconContentDescription = stringResource(BitwardenString.back),
@@ -263,6 +283,7 @@ private fun VerifyPasswordContent_MasterPassword_preview() {
263283
val state = VerifyPasswordState(
264284
title = BitwardenString.verify_your_master_password.asText(),
265285
subtext = null,
286+
hasOtherAccounts = true,
266287
accountSummaryListItem = accountSummaryListItem,
267288
)
268289
ExportItemsScaffold(
@@ -303,6 +324,7 @@ private fun VerifyPasswordContent_Otp_preview() {
303324
.asText(),
304325
accountSummaryListItem = accountSummaryListItem,
305326
showResendCodeButton = true,
327+
hasOtherAccounts = true,
306328
)
307329
ExportItemsScaffold(
308330
navIcon = rememberVectorPainter(

app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/verifypassword/VerifyPasswordViewModel.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ class VerifyPasswordViewModel @Inject constructor(
5656
?.firstOrNull { it.userId == args.userId }
5757
?: throw IllegalStateException("Account not found")
5858

59+
val singleAccount = authRepository
60+
.userStateFlow
61+
.value
62+
?.accounts
63+
?.size == 1
64+
5965
val restrictedItemPolicyOrgIds = policyManager
6066
.getActivePolicies(PolicyTypeJson.RESTRICT_ITEM_TYPES)
6167
.filter { it.isEnabled }
@@ -81,6 +87,7 @@ class VerifyPasswordViewModel @Inject constructor(
8187
.any { it.id in restrictedItemPolicyOrgIds },
8288
),
8389
showResendCodeButton = !account.hasMasterPassword,
90+
hasOtherAccounts = !singleAccount,
8491
)
8592
},
8693
) {
@@ -138,7 +145,11 @@ class VerifyPasswordViewModel @Inject constructor(
138145
}
139146

140147
private fun handleNavigateBackClick() {
141-
sendEvent(VerifyPasswordEvent.NavigateBack)
148+
if (state.hasOtherAccounts) {
149+
sendEvent(VerifyPasswordEvent.NavigateBack)
150+
} else {
151+
sendEvent(VerifyPasswordEvent.CancelExport)
152+
}
142153
}
143154

144155
private fun handleContinueClick() {
@@ -421,8 +432,10 @@ data class VerifyPasswordState(
421432
val accountSummaryListItem: AccountSelectionListItem,
422433
val title: Text,
423434
val subtext: Text?,
435+
val hasOtherAccounts: Boolean,
424436
// We never want this saved since the input is sensitive data.
425-
@IgnoredOnParcel val input: String = "",
437+
@IgnoredOnParcel
438+
val input: String = "",
426439
val dialog: DialogState? = null,
427440
val showResendCodeButton: Boolean = false,
428441
) : Parcelable {
@@ -475,6 +488,11 @@ sealed class VerifyPasswordEvent {
475488
*/
476489
data class PasswordVerified(val userId: String) : VerifyPasswordEvent()
477490

491+
/**
492+
* Cancel the export request.
493+
*/
494+
data object CancelExport : VerifyPasswordEvent()
495+
478496
/**
479497
* Show a snackbar with the given data.
480498
*/

app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreenTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.model.SendItemType
2525
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditMode
2626
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditRoute
2727
import com.x8bit.bitwarden.ui.vault.feature.exportitems.ExportItemsGraphRoute
28+
import com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword.VerifyPasswordRoute
2829
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.ItemListingType
2930
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingRoute
3031
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
@@ -450,6 +451,26 @@ class RootNavScreenTest : BitwardenComposeTest() {
450451
)
451452
}
452453
}
454+
455+
// Make sure navigating to export items graph works as expected:
456+
rootNavStateFlow.value = RootNavState.CredentialExchangeExportSkipAccountSelection(
457+
userId = "activeUserId",
458+
)
459+
composeTestRule.runOnIdle {
460+
verify {
461+
mockNavHostController.navigate(
462+
route = ExportItemsGraphRoute,
463+
navOptions = expectedNavOptions,
464+
)
465+
466+
mockNavHostController.navigate(
467+
route = VerifyPasswordRoute(
468+
userId = "activeUserId",
469+
),
470+
navOptions = expectedNavOptions,
471+
)
472+
}
473+
}
453474
}
454475
}
455476

app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1441,14 +1441,34 @@ class RootNavViewModelTest : BaseViewModelTest() {
14411441
requestJson = "mockRequestJson",
14421442
),
14431443
)
1444-
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
1444+
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_MULTIPLE_ACCOUNTS_STATE)
14451445
val viewModel = createViewModel()
14461446
assertEquals(
14471447
RootNavState.CredentialExchangeExport,
14481448
viewModel.stateFlow.value,
14491449
)
14501450
}
14511451

1452+
@Suppress("MaxLineLength")
1453+
@Test
1454+
fun `when SpecialCircumstance is CredentialExchangeExport and only has 1 account, the nav state should be CredentialExchangeExportSkipAccountSelection`() {
1455+
specialCircumstanceManager.specialCircumstance =
1456+
SpecialCircumstance.CredentialExchangeExport(
1457+
data = ImportCredentialsRequestData(
1458+
uri = mockk(),
1459+
requestJson = "mockRequestJson",
1460+
),
1461+
)
1462+
mutableUserStateFlow.tryEmit(MOCK_VAULT_UNLOCKED_USER_STATE)
1463+
val viewModel = createViewModel()
1464+
assertEquals(
1465+
RootNavState.CredentialExchangeExportSkipAccountSelection(
1466+
userId = "activeUserId",
1467+
),
1468+
viewModel.stateFlow.value,
1469+
)
1470+
}
1471+
14521472
private fun createViewModel(): RootNavViewModel =
14531473
RootNavViewModel(
14541474
authRepository = authRepository,
@@ -1487,3 +1507,48 @@ private val MOCK_VAULT_UNLOCKED_USER_STATE = UserState(
14871507
),
14881508
),
14891509
)
1510+
1511+
private val MOCK_VAULT_UNLOCKED_USER_MULTIPLE_ACCOUNTS_STATE = UserState(
1512+
activeUserId = "activeUserId",
1513+
accounts = listOf(
1514+
UserState.Account(
1515+
userId = "activeUserId",
1516+
name = "name",
1517+
email = "email",
1518+
avatarColorHex = "avatarColorHex",
1519+
environment = Environment.Us,
1520+
isPremium = true,
1521+
isLoggedIn = true,
1522+
isVaultUnlocked = true,
1523+
needsPasswordReset = false,
1524+
isBiometricsEnabled = false,
1525+
organizations = emptyList(),
1526+
needsMasterPassword = false,
1527+
trustedDevice = null,
1528+
hasMasterPassword = true,
1529+
isUsingKeyConnector = false,
1530+
firstTimeState = FirstTimeState(false),
1531+
onboardingStatus = OnboardingStatus.COMPLETE,
1532+
),
1533+
1534+
UserState.Account(
1535+
userId = "activeUserTwoId",
1536+
name = "name two",
1537+
email = "email two",
1538+
avatarColorHex = "avatarColorHex",
1539+
environment = Environment.Us,
1540+
isPremium = true,
1541+
isLoggedIn = true,
1542+
isVaultUnlocked = true,
1543+
needsPasswordReset = false,
1544+
isBiometricsEnabled = false,
1545+
organizations = emptyList(),
1546+
needsMasterPassword = false,
1547+
trustedDevice = null,
1548+
hasMasterPassword = true,
1549+
isUsingKeyConnector = false,
1550+
firstTimeState = FirstTimeState(false),
1551+
onboardingStatus = OnboardingStatus.COMPLETE,
1552+
),
1553+
),
1554+
)

app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/exportitems/verifypassword/VerifyPasswordScreenTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,5 @@ private val DEFAULT_STATE = VerifyPasswordState(
297297
accountSummaryListItem = DEFAULT_ACCOUNT_SELECTION_LIST_ITEM,
298298
input = "",
299299
dialog = null,
300+
hasOtherAccounts = true,
300301
)

0 commit comments

Comments
 (0)