diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt index 8607c10248..f6d4386b03 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepository.kt @@ -26,6 +26,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult +import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult @@ -402,4 +403,11 @@ interface AuthRepository : suspend fun leaveOrganization( organizationId: String, ): LeaveOrganizationResult + + /** + * Revokes self from the organization that matches the given [organizationId] + */ + suspend fun revokeFromOrganization( + organizationId: String, + ): RevokeFromOrganizationResult } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 935490c574..f6512c0d3f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -79,6 +79,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.RemovePasswordResult import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult +import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResult import com.x8bit.bitwarden.data.auth.repository.model.SetPasswordResult import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult @@ -1393,6 +1394,14 @@ class AuthRepositoryImpl( onFailure = { LeaveOrganizationResult.Error(error = it) }, ) + override suspend fun revokeFromOrganization( + organizationId: String, + ): RevokeFromOrganizationResult = + organizationService.revokeFromOrganization(organizationId).fold( + onSuccess = { RevokeFromOrganizationResult.Success }, + onFailure = { RevokeFromOrganizationResult.Error(error = it) }, + ) + @Suppress("CyclomaticComplexMethod") private suspend fun validatePasswordAgainstPolicy( password: String, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/RevokeFromOrganizationResult.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/RevokeFromOrganizationResult.kt new file mode 100644 index 0000000000..ac93a03ce6 --- /dev/null +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/auth/repository/model/RevokeFromOrganizationResult.kt @@ -0,0 +1,18 @@ +package com.x8bit.bitwarden.data.auth.repository.model + +/** + * Models result of leaving an organization. + */ +sealed class RevokeFromOrganizationResult { + /** + * Revoke from organization succeeded. + */ + data object Success : RevokeFromOrganizationResult() + + /** + * There was an error revoking from the organization. + */ + data class Error( + val error: Throwable?, + ) : RevokeFromOrganizationResult() +} diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt index e308e6c108..a78847700f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManager.kt @@ -29,4 +29,10 @@ interface VaultMigrationManager { userId: String, organizationId: String, ): MigratePersonalVaultResult + + /** + * Clears the migration state, setting it to [VaultMigrationData.NoMigrationRequired]. + * This should be called when the user declines migration or leaves the organization. + */ + fun clearMigrationState() } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt index 53b16468bc..8c8c61e2e1 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerImpl.kt @@ -190,7 +190,7 @@ class VaultMigrationManagerImpl( .getOrElse { return MigratePersonalVaultResult.Failure(it) } if (personalCiphers.isEmpty()) { - mutableVaultMigrationDataStateFlow.update { VaultMigrationData.NoMigrationRequired } + clearMigrationState() return MigratePersonalVaultResult.Success } @@ -212,10 +212,14 @@ class VaultMigrationManagerImpl( collectionIds = listOfNotNull(defaultUserCollection.id), ).getOrElse { return MigratePersonalVaultResult.Failure(it) } - mutableVaultMigrationDataStateFlow.update { VaultMigrationData.NoMigrationRequired } + clearMigrationState() return MigratePersonalVaultResult.Success } + override fun clearMigrationState() { + mutableVaultMigrationDataStateFlow.update { VaultMigrationData.NoMigrationRequired } + } + private fun getDefaultUserCollection( vaultData: VaultData, organizationId: String, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index 1ae133a434..d13636317d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -57,6 +57,8 @@ import com.x8bit.bitwarden.ui.vault.feature.importlogins.navigateToImportLoginsS import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestinationAsRoot +import com.x8bit.bitwarden.ui.vault.feature.leaveorganization.leaveOrganizationDestination +import com.x8bit.bitwarden.ui.vault.feature.leaveorganization.navigateToLeaveOrganization import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.vaultManualCodeEntryDestination import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.migrateToMyItemsDestination @@ -267,7 +269,16 @@ fun NavGraphBuilder.vaultUnlockedGraph( migrateToMyItemsDestination( onNavigateToVault = { navController.popBackStack() }, - onNavigateToLeaveOrganization = { }, + onNavigateToLeaveOrganization = { organizationId, organizationName -> + navController.navigateToLeaveOrganization( + organizationId = organizationId, + organizationName = organizationName, + ) + }, + ) + + leaveOrganizationDestination( + onNavigateBack = { navController.popBackStack() }, ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationNavigation.kt index d64c97aa18..0e772ad748 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationNavigation.kt @@ -51,12 +51,10 @@ fun SavedStateHandle.toLeaveOrganizationArgs(): LeaveOrganizationArgs { */ fun NavGraphBuilder.leaveOrganizationDestination( onNavigateBack: () -> Unit, - onNavigateToVault: () -> Unit, ) { composableWithPushTransitions { LeaveOrganizationScreen( onNavigateBack = onNavigateBack, - onNavigateToVault = onNavigateToVault, ) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreen.kt index f7eb0529cc..32a6177f5a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreen.kt @@ -50,7 +50,6 @@ import com.x8bit.bitwarden.ui.vault.feature.leaveorganization.handlers.rememberL @Composable fun LeaveOrganizationScreen( onNavigateBack: () -> Unit, - onNavigateToVault: () -> Unit, viewModel: LeaveOrganizationViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, ) { @@ -60,7 +59,6 @@ fun LeaveOrganizationScreen( EventsEffect(viewModel = viewModel) { event -> when (event) { LeaveOrganizationEvent.NavigateBack -> onNavigateBack() - LeaveOrganizationEvent.NavigateToVault -> onNavigateToVault() is LeaveOrganizationEvent.LaunchUri -> { intentManager.launchUri(event.uri.toUri()) } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModel.kt index 19b01450ba..0cd54702b3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModel.kt @@ -10,9 +10,10 @@ import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.repository.AuthRepository -import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult +import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent +import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn @@ -32,6 +33,7 @@ class LeaveOrganizationViewModel @Inject constructor( private val authRepository: AuthRepository, private val snackbarRelayManager: SnackbarRelayManager, private val organizationEventManager: OrganizationEventManager, + private val vaultMigrationManager: VaultMigrationManager, savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { @@ -56,8 +58,8 @@ class LeaveOrganizationViewModel @Inject constructor( LeaveOrganizationAction.LeaveOrganizationClick -> handleLeaveOrganizationClick() LeaveOrganizationAction.HelpLinkClick -> handleHelpLinkClick() LeaveOrganizationAction.DismissDialog -> handleDismissDialog() - is LeaveOrganizationAction.Internal.LeaveOrganizationResultReceived -> { - handleLeaveOrganizationResultReceived(action) + is LeaveOrganizationAction.Internal.RevokeFromOrganizationResultReceived -> { + handleRevokeFromOrganizationResultReceived(action) } } } @@ -71,9 +73,9 @@ class LeaveOrganizationViewModel @Inject constructor( it.copy(dialogState = LeaveOrganizationState.DialogState.Loading) } viewModelScope.launch { - val result = authRepository.leaveOrganization(state.organizationId) + val result = authRepository.revokeFromOrganization(state.organizationId) sendAction( - LeaveOrganizationAction.Internal.LeaveOrganizationResultReceived(result), + LeaveOrganizationAction.Internal.RevokeFromOrganizationResultReceived(result), ) } } @@ -92,27 +94,28 @@ class LeaveOrganizationViewModel @Inject constructor( } } - private fun handleLeaveOrganizationResultReceived( - action: LeaveOrganizationAction.Internal.LeaveOrganizationResultReceived, + private fun handleRevokeFromOrganizationResultReceived( + action: LeaveOrganizationAction.Internal.RevokeFromOrganizationResultReceived, ) { when (val result = action.result) { - is LeaveOrganizationResult.Success -> { - organizationEventManager.trackEvent( - event = OrganizationEvent.ItemOrganizationDeclined, - ) - mutableStateFlow.update { - it.copy(dialogState = null) - } + is RevokeFromOrganizationResult.Success -> { snackbarRelayManager.sendSnackbarData( relay = SnackbarRelay.LEFT_ORGANIZATION, data = BitwardenSnackbarData( message = BitwardenString.you_left_the_organization.asText(), ), ) - sendEvent(LeaveOrganizationEvent.NavigateToVault) + organizationEventManager.trackEvent( + event = OrganizationEvent.ItemOrganizationDeclined, + ) + mutableStateFlow.update { + it.copy(dialogState = null) + } + // Navigation will be handled on RootNavViewModel by migration state change + vaultMigrationManager.clearMigrationState() } - is LeaveOrganizationResult.Error -> { + is RevokeFromOrganizationResult.Error -> { mutableStateFlow.update { it.copy( dialogState = LeaveOrganizationState.DialogState.Error( @@ -166,11 +169,6 @@ sealed class LeaveOrganizationEvent { */ data object NavigateBack : LeaveOrganizationEvent() - /** - * Navigate to the Vault screen. - */ - data object NavigateToVault : LeaveOrganizationEvent() - /** * Launch external URI. */ @@ -206,10 +204,10 @@ sealed class LeaveOrganizationAction { */ sealed class Internal : LeaveOrganizationAction() { /** - * Leave organization result received from repository. + * Revoke from organization result received from repository. */ - data class LeaveOrganizationResultReceived( - val result: LeaveOrganizationResult, + data class RevokeFromOrganizationResultReceived( + val result: RevokeFromOrganizationResult, ) : Internal() } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsNavigation.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsNavigation.kt index 904c26274a..193344dcc6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsNavigation.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsNavigation.kt @@ -71,7 +71,7 @@ fun NavController.navigateToMigrateToMyItems( */ fun NavGraphBuilder.migrateToMyItemsDestination( onNavigateToVault: () -> Unit, - onNavigateToLeaveOrganization: () -> Unit, + onNavigateToLeaveOrganization: (organizationId: String, organizationName: String) -> Unit, ) { composableWithSlideTransitions { MigrateToMyItemsScreen( diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreen.kt index 5d3f480fa8..8d6852dcab 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreen.kt @@ -45,7 +45,7 @@ import com.x8bit.bitwarden.ui.vault.feature.migratetomyitems.handler.rememberMig @Composable fun MigrateToMyItemsScreen( onNavigateToVault: () -> Unit, - onNavigateToLeaveOrganization: () -> Unit, + onNavigateToLeaveOrganization: (organizationId: String, organizationName: String) -> Unit, viewModel: MigrateToMyItemsViewModel = hiltViewModel(), intentManager: IntentManager = LocalIntentManager.current, ) { @@ -55,7 +55,9 @@ fun MigrateToMyItemsScreen( EventsEffect(viewModel = viewModel) { event -> when (event) { MigrateToMyItemsEvent.NavigateToVault -> onNavigateToVault() - MigrateToMyItemsEvent.NavigateToLeaveOrganization -> onNavigateToLeaveOrganization() + is MigrateToMyItemsEvent.NavigateToLeaveOrganization -> { + onNavigateToLeaveOrganization(event.organizationId, event.organizationName) + } is MigrateToMyItemsEvent.LaunchUri -> intentManager.launchUri(event.uri.toUri()) } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModel.kt index c4eec5c662..10a1772c5f 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModel.kt @@ -105,7 +105,12 @@ class MigrateToMyItemsViewModel @Inject constructor( } private fun handleDeclineAndLeaveClicked() { - sendEvent(MigrateToMyItemsEvent.NavigateToLeaveOrganization) + sendEvent( + MigrateToMyItemsEvent.NavigateToLeaveOrganization( + organizationId = state.organizationId, + organizationName = state.organizationName, + ), + ) } private fun handleHelpLinkClicked() { @@ -207,7 +212,10 @@ sealed class MigrateToMyItemsEvent { /** * Navigate to the leave organization flow after declining. */ - data object NavigateToLeaveOrganization : MigrateToMyItemsEvent() + data class NavigateToLeaveOrganization( + val organizationId: String, + val organizationName: String, + ) : MigrateToMyItemsEvent() /** * Launch a URI in the browser or appropriate handler. diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index c2a745eb81..e8686b6636 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -205,6 +205,7 @@ class VaultViewModel @Inject constructor( SnackbarRelay.CIPHER_UPDATED, SnackbarRelay.FOLDER_CREATED, SnackbarRelay.LOGINS_IMPORTED, + SnackbarRelay.LEFT_ORGANIZATION, ), ) .map { VaultAction.Internal.SnackbarDataReceive(it) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt index 78ade56aae..b70eb2f2d0 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/data/vault/manager/VaultMigrationManagerTest.kt @@ -1164,6 +1164,26 @@ class VaultMigrationManagerTest { vaultRepository.migrateAttachments(userId, match { it.id == "mockId-2" }) } } + + @Test + fun `clearMigrationState should set migration state to NoMigrationRequired`() = runTest { + val vaultMigrationManager = createVaultMigrationManager() + + // Initially state should be NoMigrationRequired + assertEquals( + VaultMigrationData.NoMigrationRequired, + vaultMigrationManager.vaultMigrationDataStateFlow.value, + ) + + // Call clearMigrationState (should remain NoMigrationRequired) + vaultMigrationManager.clearMigrationState() + + // Verify state is still NoMigrationRequired + assertEquals( + VaultMigrationData.NoMigrationRequired, + vaultMigrationManager.vaultMigrationDataStateFlow.value, + ) + } } private fun createVaultData( diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreenTest.kt index 9151bb180c..a89a4b2f0d 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationScreenTest.kt @@ -24,7 +24,6 @@ import org.junit.Test class LeaveOrganizationScreenTest : BitwardenComposeTest() { private var onNavigateBackCalled = false - private var onNavigateToVaultCalled = false private val mutableEventFlow = bufferedMutableSharedFlow() private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE) @@ -38,7 +37,6 @@ class LeaveOrganizationScreenTest : BitwardenComposeTest() { setContent { LeaveOrganizationScreen( onNavigateBack = { onNavigateBackCalled = true }, - onNavigateToVault = { onNavigateToVaultCalled = true }, viewModel = viewModel, ) } @@ -50,12 +48,6 @@ class LeaveOrganizationScreenTest : BitwardenComposeTest() { assertTrue(onNavigateBackCalled) } - @Test - fun `NavigateToVault event should call onNavigateToVault`() { - mutableEventFlow.tryEmit(LeaveOrganizationEvent.NavigateToVault) - assertTrue(onNavigateToVaultCalled) - } - @Test fun `back button click should emit NavigateBack event`() { composeTestRule diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModelTest.kt index ac8cab7902..674ad09fb8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/leaveorganization/LeaveOrganizationViewModelTest.kt @@ -11,13 +11,14 @@ import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.asText import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus import com.x8bit.bitwarden.data.auth.repository.AuthRepository -import com.x8bit.bitwarden.data.auth.repository.model.LeaveOrganizationResult import com.x8bit.bitwarden.data.auth.repository.model.Organization +import com.x8bit.bitwarden.data.auth.repository.model.RevokeFromOrganizationResult import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType 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.OrganizationEvent +import com.x8bit.bitwarden.data.vault.manager.VaultMigrationManager import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import io.mockk.coEvery import io.mockk.every @@ -49,6 +50,10 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { every { trackEvent(any()) } just runs } + private val mockVaultMigrationManager: VaultMigrationManager = mockk { + every { clearMigrationState() } just runs + } + @BeforeEach fun setup() { mockkStatic(SavedStateHandle::toLeaveOrganizationArgs) @@ -96,9 +101,9 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { @Test fun `LeaveOrganizationClick should show loading dialog`() = runTest { coEvery { - mockAuthRepository.leaveOrganization(any()) + mockAuthRepository.revokeFromOrganization(any()) } coAnswers { - LeaveOrganizationResult.Success + RevokeFromOrganizationResult.Success } val viewModel = createViewModel() @@ -116,17 +121,14 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { @Suppress("MaxLineLength") @Test - fun `LeaveOrganizationClick with Success should track ItemOrganizationDeclined event, send snackbar, and navigate to vault`() = + fun `LeaveOrganizationClick with Success should track ItemOrganizationDeclined event, send snackbar, and clear migration state`() = runTest { coEvery { - mockAuthRepository.leaveOrganization(ORGANIZATION_ID) - } returns LeaveOrganizationResult.Success + mockAuthRepository.revokeFromOrganization(ORGANIZATION_ID) + } returns RevokeFromOrganizationResult.Success val viewModel = createViewModel() - viewModel.eventFlow.test { - viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick) - assertEquals(LeaveOrganizationEvent.NavigateToVault, awaitItem()) - } + viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick) verify { mockSnackbarRelayManager.sendSnackbarData( @@ -138,15 +140,18 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { mockOrganizationEventManager.trackEvent( event = OrganizationEvent.ItemOrganizationDeclined, ) + mockVaultMigrationManager.clearMigrationState() } + + assertNull(viewModel.stateFlow.value.dialogState) } @Test fun `LeaveOrganizationClick with Error should show error dialog`() = runTest { val error = Throwable("Test error") coEvery { - mockAuthRepository.leaveOrganization(ORGANIZATION_ID) - } returns LeaveOrganizationResult.Error(error) + mockAuthRepository.revokeFromOrganization(ORGANIZATION_ID) + } returns RevokeFromOrganizationResult.Error(error) val viewModel = createViewModel() viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick) @@ -161,8 +166,8 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { @Test fun `DismissDialog should clear dialog state`() = runTest { coEvery { - mockAuthRepository.leaveOrganization(ORGANIZATION_ID) - } returns LeaveOrganizationResult.Error(Throwable("Error")) + mockAuthRepository.revokeFromOrganization(ORGANIZATION_ID) + } returns RevokeFromOrganizationResult.Error(Throwable("Error")) val viewModel = createViewModel() viewModel.trySendAction(LeaveOrganizationAction.LeaveOrganizationClick) @@ -187,6 +192,7 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { authRepository = mockAuthRepository, snackbarRelayManager = mockSnackbarRelayManager, organizationEventManager = mockOrganizationEventManager, + vaultMigrationManager = mockVaultMigrationManager, savedStateHandle = savedStateHandle, ) @@ -204,6 +210,7 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { authRepository = mockAuthRepository, snackbarRelayManager = mockSnackbarRelayManager, organizationEventManager = mockOrganizationEventManager, + vaultMigrationManager = mockVaultMigrationManager, savedStateHandle = savedStateHandle, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreenTest.kt index d817f92a1b..f46bb704b8 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsScreenTest.kt @@ -48,7 +48,9 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() { MigrateToMyItemsScreen( viewModel = viewModel, onNavigateToVault = { onNavigateToVaultCalled = true }, - onNavigateToLeaveOrganization = { onNavigateToLeaveOrganizationCalled = true }, + onNavigateToLeaveOrganization = { _, _ -> + onNavigateToLeaveOrganizationCalled = true + }, ) } } @@ -107,7 +109,12 @@ class MigrateToMyItemsScreenTest : BitwardenComposeTest() { @Test fun `NavigateToLeaveOrganization event should trigger navigation callback`() { - mutableEventFlow.tryEmit(MigrateToMyItemsEvent.NavigateToLeaveOrganization) + mutableEventFlow.tryEmit( + MigrateToMyItemsEvent.NavigateToLeaveOrganization( + organizationId = "test-org-id", + organizationName = "Test Organization", + ), + ) assertTrue(onNavigateToLeaveOrganizationCalled) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt index 98500c3eb4..529b5800f3 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/migratetomyitems/MigrateToMyItemsViewModelTest.kt @@ -192,7 +192,13 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() { val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(MigrateToMyItemsAction.DeclineAndLeaveClicked) - assertEquals(MigrateToMyItemsEvent.NavigateToLeaveOrganization, awaitItem()) + assertEquals( + MigrateToMyItemsEvent.NavigateToLeaveOrganization( + organizationId = ORGANIZATION_ID, + organizationName = ORGANIZATION_NAME, + ), + awaitItem(), + ) } } diff --git a/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedOrganizationApi.kt b/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedOrganizationApi.kt index c2c499ee35..6ae9be7801 100644 --- a/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedOrganizationApi.kt +++ b/network/src/main/kotlin/com/bitwarden/network/api/AuthenticatedOrganizationApi.kt @@ -47,4 +47,12 @@ internal interface AuthenticatedOrganizationApi { suspend fun leaveOrganization( @Path("id") organizationId: String, ): NetworkResult + + /** + * Revokes self from organization + */ + @PUT("/organizations/{orgId}/users/revoke-self") + suspend fun revokeFromOrganization( + @Path("orgId") organizationId: String, + ): NetworkResult } diff --git a/network/src/main/kotlin/com/bitwarden/network/service/OrganizationService.kt b/network/src/main/kotlin/com/bitwarden/network/service/OrganizationService.kt index aa65b1ad21..d8c14b5530 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/OrganizationService.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/OrganizationService.kt @@ -46,4 +46,11 @@ interface OrganizationService { suspend fun leaveOrganization( organizationId: String, ): Result + + /** + * Make a request to revoke self from the organization + */ + suspend fun revokeFromOrganization( + organizationId: String, + ): Result } diff --git a/network/src/main/kotlin/com/bitwarden/network/service/OrganizationServiceImpl.kt b/network/src/main/kotlin/com/bitwarden/network/service/OrganizationServiceImpl.kt index 41a250568d..f6d23f5ff3 100644 --- a/network/src/main/kotlin/com/bitwarden/network/service/OrganizationServiceImpl.kt +++ b/network/src/main/kotlin/com/bitwarden/network/service/OrganizationServiceImpl.kt @@ -62,4 +62,9 @@ internal class OrganizationServiceImpl( authenticatedOrganizationApi .leaveOrganization(organizationId = organizationId) .toResult() + + override suspend fun revokeFromOrganization(organizationId: String): Result = + authenticatedOrganizationApi + .revokeFromOrganization(organizationId = organizationId) + .toResult() }