diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt index 48c8f1350a..ddbc4fa48f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/model/Organization.kt @@ -20,4 +20,5 @@ data class Organization( val shouldUseKeyConnector: Boolean, val role: OrganizationType, val keyConnectorUrl: String?, + val userIsClaimedByOrganization: Boolean, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt index ba2dde0e8c..f64bfa775e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensions.kt @@ -23,6 +23,7 @@ fun SyncResponseJson.Profile.Organization.toOrganization(): Organization = role = this.type, shouldManageResetPassword = this.permissions.shouldManageResetPassword, keyConnectorUrl = this.keyConnectorUrl, + userIsClaimedByOrganization = this.userIsClaimedByOrganization, ) /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt index a0f0738c01..ec6039b30a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt @@ -2,12 +2,14 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deletea import android.widget.Toast import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api @@ -20,22 +22,27 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect +import com.x8bit.bitwarden.ui.platform.base.util.cardStyle +import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledErrorButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedErrorButton import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPasswordDialog +import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme @@ -115,61 +122,52 @@ fun DeleteAccountScreen( .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - Spacer(modifier = Modifier.height(8.dp)) - Icon( - painter = rememberVectorPainter(id = R.drawable.ic_warning), - contentDescription = null, - tint = BitwardenTheme.colorScheme.status.error, - modifier = Modifier.padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.deleting_your_account_is_permanent), - style = BitwardenTheme.typography.headlineSmall, - color = BitwardenTheme.colorScheme.status.error, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = stringResource(id = R.string.delete_account_explanation), - style = BitwardenTheme.typography.bodyMedium, - color = BitwardenTheme.colorScheme.text.primary, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(24.dp)) - DeleteAccountButton( - onDeleteAccountConfirmDialogClick = remember(viewModel) { - { - viewModel.trySendAction( - DeleteAccountAction.DeleteAccountConfirmDialogClick(it), - ) - } - }, - onDeleteAccountClick = remember(viewModel) { - { viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick) } - }, - isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled, - modifier = Modifier - .testTag("DELETE ACCOUNT") - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.height(12.dp)) - BitwardenOutlinedErrorButton( - label = stringResource(id = R.string.cancel), - onClick = remember(viewModel) { - { viewModel.trySendAction(DeleteAccountAction.CancelClick) } - }, - modifier = Modifier - .testTag("CANCEL") - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - Spacer(modifier = Modifier.navigationBarsPadding()) + Spacer(modifier = Modifier.height(16.dp)) + if (state.isUserManagedByOrganization) { + WarningMessageCard( + headerText = stringResource(id = R.string.cannot_delete_your_account), + subtitleText = stringResource( + id = R.string.cannot_delete_your_account_explanation, + ), + modifier = Modifier.standardHorizontalMargin(), + ) + } else { + WarningMessageCard( + headerText = stringResource(id = R.string.deleting_your_account_is_permanent), + subtitleText = stringResource(id = R.string.delete_account_explanation), + modifier = Modifier.standardHorizontalMargin(), + ) + Spacer(modifier = Modifier.height(24.dp)) + DeleteAccountButton( + onDeleteAccountConfirmDialogClick = remember(viewModel) { + { + viewModel.trySendAction( + DeleteAccountAction.DeleteAccountConfirmDialogClick(it), + ) + } + }, + onDeleteAccountClick = remember(viewModel) { + { viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick) } + }, + isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled, + modifier = Modifier + .testTag("DELETE ACCOUNT") + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + BitwardenOutlinedErrorButton( + label = stringResource(id = R.string.cancel), + onClick = remember(viewModel) { + { viewModel.trySendAction(DeleteAccountAction.CancelClick) } + }, + modifier = Modifier + .testTag("CANCEL") + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.navigationBarsPadding()) + } } } } @@ -204,3 +202,50 @@ private fun DeleteAccountButton( modifier = modifier, ) } + +@Composable +private fun WarningMessageCard( + headerText: String, + subtitleText: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.cardStyle( + cardStyle = CardStyle.Full, + paddingHorizontal = 12.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = rememberVectorPainter(id = R.drawable.ic_warning), + contentDescription = null, + tint = BitwardenTheme.colorScheme.status.weak1, + ) + Spacer(Modifier.width(width = 12.dp)) + Column(modifier = Modifier.weight(weight = 1f)) { + Text( + text = headerText, + style = BitwardenTheme.typography.titleSmall, + color = BitwardenTheme.colorScheme.status.weak1, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(modifier = Modifier.height(height = 4.dp)) + Text( + text = subtitleText, + style = BitwardenTheme.typography.bodyMedium, + color = BitwardenTheme.colorScheme.text.secondary, + modifier = Modifier.fillMaxWidth(), + ) + } + Spacer(modifier = Modifier.width(width = 4.dp)) + } +} + +@Preview +@Composable +private fun WarningMessageCard_preview() { + WarningMessageCard( + headerText = stringResource(id = R.string.cannot_delete_your_account), + subtitleText = stringResource(id = R.string.cannot_delete_your_account_explanation), + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt index 0ee7565154..4b4606110c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt @@ -3,13 +3,13 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deletea import android.os.Parcelable import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.bitwarden.ui.util.Text +import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult import com.x8bit.bitwarden.ui.platform.base.BaseViewModel -import com.bitwarden.ui.util.Text -import com.bitwarden.ui.util.asText import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn @@ -32,12 +32,16 @@ class DeleteAccountViewModel @Inject constructor( private val authRepository: AuthRepository, savedStateHandle: SavedStateHandle, ) : BaseViewModel( - initialState = savedStateHandle[KEY_STATE] ?: DeleteAccountState( - dialog = null, - isUnlockWithPasswordEnabled = requireNotNull(authRepository.userStateFlow.value) - .activeAccount - .hasMasterPassword, - ), + initialState = savedStateHandle[KEY_STATE] ?: run { + val account = requireNotNull(authRepository.userStateFlow.value).activeAccount + DeleteAccountState( + dialog = null, + isUnlockWithPasswordEnabled = account.hasMasterPassword, + isUserManagedByOrganization = account + .organizations + .any { it.userIsClaimedByOrganization } == true, + ) + }, ) { init { @@ -160,11 +164,13 @@ class DeleteAccountViewModel @Inject constructor( * @param dialog The dialog for the [DeleteAccountScreen]. * @param isUnlockWithPasswordEnabled Whether or not the user is able to unlock the vault with * their master password. + * @param isUserManagedByOrganization Whether or not the user is managed by an organization. */ @Parcelize data class DeleteAccountState( val dialog: DeleteAccountDialog?, val isUnlockWithPasswordEnabled: Boolean, + val isUserManagedByOrganization: Boolean, ) : Parcelable { /** diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml index bb708159b2..f01c062791 100644 --- a/app/src/main/res/drawable/ic_warning.xml +++ b/app/src/main/res/drawable/ic_warning.xml @@ -1,20 +1,16 @@ - - - - - - + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30496e4e40..21bf84c0a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1263,4 +1263,6 @@ Do you want to switch to this account? A certificate with this alias \"%s\" already exists. Do you want to replace it?\nReplace the certificate may impact your connection to any environments currently using it. Replace certificate Unable to read certificate. + Cannot delete your account + This action cannot be completed because your account is owned by an organization. Contact your organization administrator for additional details. diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index 0e018a12ef..0cc10b53be 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -4679,6 +4679,7 @@ class AuthRepositoryTest { every { shouldUseKeyConnector } returns true every { type } returns OrganizationType.USER every { keyConnectorUrl } returns null + every { userIsClaimedByOrganization } returns false }, ) fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations) @@ -4708,6 +4709,7 @@ class AuthRepositoryTest { every { shouldUseKeyConnector } returns true every { type } returns OrganizationType.USER every { keyConnectorUrl } returns url + every { userIsClaimedByOrganization } returns false }, ) fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations) @@ -4745,6 +4747,7 @@ class AuthRepositoryTest { every { shouldUseKeyConnector } returns true every { type } returns OrganizationType.USER every { keyConnectorUrl } returns url + every { userIsClaimedByOrganization } returns false }, ) fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations) @@ -4785,6 +4788,7 @@ class AuthRepositoryTest { every { shouldUseKeyConnector } returns true every { type } returns OrganizationType.USER every { keyConnectorUrl } returns url + every { userIsClaimedByOrganization } returns false }, ) fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations) @@ -4824,6 +4828,7 @@ class AuthRepositoryTest { every { shouldUseKeyConnector } returns true every { type } returns OrganizationType.USER every { keyConnectorUrl } returns url + every { userIsClaimedByOrganization } returns false }, ) fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations) @@ -6887,6 +6892,42 @@ class AuthRepositoryTest { ) } + @Suppress("MaxLineLength") + @Test + fun `isUserManagedByOrganization should return true if any org userIsClaimedByOrganization is true`() = + runTest { + fakeAuthDiskSource.userState = SINGLE_USER_STATE_1 + fakeAuthDiskSource.storeUserKey(userId = USER_ID_1, userKey = ENCRYPTED_USER_KEY) + val organizations = listOf( + createMockOrganization(number = 0) + .copy( + userIsClaimedByOrganization = true, + ), + createMockOrganization(number = 1), + ) + fakeAuthDiskSource.storeOrganizations(userId = USER_ID_1, organizations = organizations) + assertEquals( + SINGLE_USER_STATE_1.toUserState( + vaultState = VAULT_UNLOCK_DATA, + userAccountTokens = emptyList(), + userOrganizationsList = listOf( + UserOrganizations( + userId = USER_ID_1, + organizations = organizations.toOrganizations(), + ), + ), + userIsUsingKeyConnectorList = emptyList(), + hasPendingAccountAddition = false, + onboardingStatus = null, + isBiometricsEnabledProvider = { false }, + vaultUnlockTypeProvider = { VaultUnlockType.MASTER_PASSWORD }, + isDeviceTrustedProvider = { false }, + firstTimeState = FIRST_TIME_STATE, + ), + repository.userStateFlow.value, + ) + } + companion object { private const val UNIQUE_APP_ID = "testUniqueAppId" private const val NAME = "Example Name" diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt index c94240b0c6..2b9da93620 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/AuthDiskSourceExtensionsTest.kt @@ -195,6 +195,7 @@ class AuthDiskSourceExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-1", + userIsClaimedByOrganization = false, ), ), ), @@ -208,6 +209,7 @@ class AuthDiskSourceExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-2", + userIsClaimedByOrganization = false, ), ), ), @@ -221,6 +223,7 @@ class AuthDiskSourceExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-3", + userIsClaimedByOrganization = false, ), ), ), @@ -369,6 +372,7 @@ class AuthDiskSourceExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-1", + userIsClaimedByOrganization = false, ), ), ), @@ -401,6 +405,7 @@ class AuthDiskSourceExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-1", + userIsClaimedByOrganization = false, ), ), ), @@ -414,6 +419,7 @@ class AuthDiskSourceExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-2", + userIsClaimedByOrganization = false, ), ), ), diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt index 7d2590388d..a6edd68474 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/SyncResponseJsonExtensionsTest.kt @@ -24,6 +24,7 @@ class SyncResponseJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-1", + userIsClaimedByOrganization = false, ), createMockOrganization(number = 1).toOrganization(), ) @@ -40,6 +41,7 @@ class SyncResponseJsonExtensionsTest { shouldUseKeyConnector = true, role = OrganizationType.ADMIN, keyConnectorUrl = "mockKeyConnectorUrl-1", + userIsClaimedByOrganization = false, ), Organization( id = "mockId-2", @@ -48,6 +50,7 @@ class SyncResponseJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.USER, keyConnectorUrl = "mockKeyConnectorUrl-2", + userIsClaimedByOrganization = false, ), ), listOf( diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt index 4bc0d17a98..04b5dac9ee 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/util/UserStateJsonExtensionsTest.kt @@ -367,6 +367,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = false, @@ -432,6 +433,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), @@ -477,6 +479,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = true, @@ -538,6 +541,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), @@ -584,6 +588,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = false, @@ -653,6 +658,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), @@ -699,6 +705,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = false, @@ -768,6 +775,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), @@ -814,6 +822,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = false, @@ -883,6 +892,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), @@ -930,6 +940,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.USER, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = false, @@ -1002,6 +1013,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.USER, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), @@ -1207,6 +1219,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.USER, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = false, @@ -1278,6 +1291,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.USER, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), @@ -1324,6 +1338,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = false, @@ -1395,6 +1410,7 @@ class UserStateJsonExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt index f44fd77735..0fa70c1c01 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/removepassword/RemovePasswordViewModelTest.kt @@ -295,6 +295,7 @@ private val DEFAULT_ACCOUNT = UserState.Account( shouldUseKeyConnector = true, role = OrganizationType.USER, keyConnectorUrl = KEY_CONNECTOR_URL, + userIsClaimedByOrganization = false, ), ), needsMasterPassword = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt index 8e2a76e3c7..739d9166f0 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModelTest.kt @@ -387,6 +387,7 @@ class RootNavViewModelTest : BaseViewModelTest() { shouldUseKeyConnector = true, role = OrganizationType.USER, keyConnectorUrl = "bitwarden.com", + userIsClaimedByOrganization = false, ), ), needsMasterPassword = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index 8b546a721f..c8ab3a0e87 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -998,6 +998,7 @@ private val DEFAULT_USER_STATE = UserState( shouldManageResetPassword = false, role = OrganizationType.USER, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "organizationAdmin", @@ -1006,6 +1007,7 @@ private val DEFAULT_USER_STATE = UserState( shouldManageResetPassword = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "organizationOwner", @@ -1014,6 +1016,7 @@ private val DEFAULT_USER_STATE = UserState( shouldManageResetPassword = false, role = OrganizationType.OWNER, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "organizationCustom", @@ -1022,6 +1025,7 @@ private val DEFAULT_USER_STATE = UserState( shouldManageResetPassword = false, role = OrganizationType.CUSTOM, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), needsMasterPassword = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt index 2fcfd93e73..07eaf8b02b 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt @@ -214,9 +214,48 @@ class DeleteAccountScreenTest : BaseComposeTest() { ) } } + + @Suppress("MaxLineLength") + @Test + fun `if isUserManagedByOrganization should display cannot delete message and hide delete button`() { + composeTestRule + .onNodeWithText("Cannot delete your account") + .assertDoesNotExist() + + composeTestRule + .onNodeWithText("This action cannot be completed because your account " + + "is owned by an organization. " + + "Contact your organization administrator for additional details.") + .assertDoesNotExist() + + composeTestRule + .onAllNodesWithText("Delete account") + .filterToOne(hasClickAction()) + .assertExists() + + mutableStateFlow.update { + it.copy(isUserManagedByOrganization = true) + } + + composeTestRule + .onNodeWithText("Cannot delete your account") + .assertExists() + + composeTestRule + .onNodeWithText("This action cannot be completed because your account " + + "is owned by an organization. " + + "Contact your organization administrator for additional details.") + .assertExists() + + composeTestRule + .onAllNodesWithText("Delete account") + .filterToOne(hasClickAction()) + .assertDoesNotExist() + } } private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState( dialog = null, isUnlockWithPasswordEnabled = true, + isUserManagedByOrganization = false, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt index 71452b3be8..6f873e8597 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt @@ -255,4 +255,5 @@ private val DEFAULT_USER_STATE: UserState = UserState( private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState( dialog = null, isUnlockWithPasswordEnabled = true, + isUserManagedByOrganization = false, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index 2c402fc346..9795f6ec4a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -4619,6 +4619,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = true, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt index 4bc97cc343..9ea2ffe5c9 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/util/CipherViewExtensionsTest.kt @@ -565,6 +565,7 @@ class CipherViewExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), isBiometricsEnabled = true, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt index de364b0200..e1d3dd5be1 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemViewModelTest.kt @@ -3350,6 +3350,7 @@ class VaultItemViewModelTest : BaseViewModelTest() { shouldUseKeyConnector = false, role = OrganizationType.OWNER, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt index 21906fb624..58b6b356f5 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/VaultMoveToOrganizationViewModelTest.kt @@ -499,6 +499,7 @@ private val DEFAULT_USER_STATE = UserState( shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "mockOrganizationId-2", @@ -507,6 +508,7 @@ private val DEFAULT_USER_STATE = UserState( shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "mockOrganizationId-3", @@ -515,6 +517,7 @@ private val DEFAULT_USER_STATE = UserState( shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt index c500275175..d781d8aa80 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/movetoorganization/util/VaultMoveToOrganizationExtensionsTest.kt @@ -109,6 +109,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState = shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "mockOrganizationId-2", @@ -117,6 +118,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState = shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "mockOrganizationId-3", @@ -125,6 +127,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState = shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ) } else { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 055ba94110..0820e457ac 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -259,6 +259,7 @@ class VaultViewModelTest : BaseViewModelTest() { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -346,6 +347,7 @@ class VaultViewModelTest : BaseViewModelTest() { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -634,6 +636,7 @@ class VaultViewModelTest : BaseViewModelTest() { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), ), diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt index dd01998962..513493b622 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/UserStateExtensionsTest.kt @@ -83,6 +83,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -111,6 +112,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -143,6 +145,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -175,6 +178,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -222,6 +226,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -267,6 +272,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -316,6 +322,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -395,6 +402,7 @@ class UserStateExtensionsTest { shouldManageResetPassword = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "organizationId-A", @@ -403,6 +411,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, @@ -455,6 +464,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), Organization( id = "organizationId-A", @@ -463,6 +473,7 @@ class UserStateExtensionsTest { shouldUseKeyConnector = false, role = OrganizationType.ADMIN, keyConnectorUrl = null, + userIsClaimedByOrganization = false, ), ), trustedDevice = null, diff --git a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt index 2295ba2a7b..2deb103872 100644 --- a/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt +++ b/network/src/main/kotlin/com/bitwarden/network/model/SyncResponseJson.kt @@ -344,6 +344,9 @@ data class SyncResponseJson( @SerialName("status") val status: OrganizationStatusType, + + @SerialName("userIsClaimedByOrganization") + val userIsClaimedByOrganization: Boolean, ) /** diff --git a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt index b6df398521..e7a0511388 100644 --- a/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/service/SyncServiceTest.kt @@ -101,7 +101,8 @@ private const val SYNC_SUCCESS_JSON = """ "name": "mockName-1", "useApi": false, "familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z", - "status": 1 + "status": 1, + "userIsClaimedByOrganization": false } ], "providers": [ @@ -176,7 +177,8 @@ private const val SYNC_SUCCESS_JSON = """ "name": "mockName-1", "useApi": false, "familySponsorshipValidUntil": "2023-10-27T12:00:00.00Z", - "status": 1 + "status": 1, + "userIsClaimedByOrganization": false } ] }, diff --git a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseProfileUtil.kt b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseProfileUtil.kt index 75a0b8b067..ebdf0b3d9b 100644 --- a/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseProfileUtil.kt +++ b/network/src/testFixtures/kotlin/com/bitwarden/network/model/SyncResponseProfileUtil.kt @@ -69,6 +69,7 @@ fun createMockOrganization( shouldUseApi = false, familySponsorshipValidUntil = ZonedDateTime.parse("2023-10-27T12:00:00Z"), status = OrganizationStatusType.ACCEPTED, + userIsClaimedByOrganization = false, ) /**