From 7cd0e2c176bb7c2626b6866f35cfcc233ebdeb5e Mon Sep 17 00:00:00 2001 From: Patrick Honkonen <1883101+SaintPatrck@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:18:10 -0500 Subject: [PATCH] PM-29843: Record item org migration events (#6275) --- .../LeaveOrganizationViewModel.kt | 6 ++++++ .../MigrateToMyItemsViewModel.kt | 6 ++++++ .../LeaveOrganizationViewModelTest.kt | 14 ++++++++++++- .../MigrateToMyItemsViewModelTest.kt | 20 ++++++++++++++++++- 4 files changed, 44 insertions(+), 2 deletions(-) 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 d443d085d1..19b01450ba 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 @@ -11,6 +11,8 @@ 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.platform.manager.event.OrganizationEventManager +import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import com.x8bit.bitwarden.ui.platform.model.SnackbarRelay import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.launchIn @@ -29,6 +31,7 @@ private const val KEY_STATE = "state" class LeaveOrganizationViewModel @Inject constructor( private val authRepository: AuthRepository, private val snackbarRelayManager: SnackbarRelayManager, + private val organizationEventManager: OrganizationEventManager, savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { @@ -94,6 +97,9 @@ class LeaveOrganizationViewModel @Inject constructor( ) { when (val result = action.result) { is LeaveOrganizationResult.Success -> { + organizationEventManager.trackEvent( + event = OrganizationEvent.ItemOrganizationDeclined, + ) mutableStateFlow.update { it.copy(dialogState = null) } 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 dff00f3850..10812d2a3a 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 @@ -7,6 +7,8 @@ import com.bitwarden.ui.platform.base.BaseViewModel import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager +import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn @@ -23,6 +25,7 @@ private const val KEY_STATE = "state" */ @HiltViewModel class MigrateToMyItemsViewModel @Inject constructor( + private val organizationEventManager: OrganizationEventManager, savedStateHandle: SavedStateHandle, ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: run { @@ -101,6 +104,9 @@ class MigrateToMyItemsViewModel @Inject constructor( action: MigrateToMyItemsAction.Internal.MigrateToMyItemsResultReceived, ) { if (action.success) { + organizationEventManager.trackEvent( + event = OrganizationEvent.ItemOrganizationAccepted, + ) clearDialog() sendEvent(MigrateToMyItemsEvent.NavigateToVault) } else { 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 f6f8b60180..ac8cab7902 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 @@ -15,7 +15,9 @@ 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.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.ui.platform.model.SnackbarRelay import io.mockk.coEvery import io.mockk.every @@ -43,6 +45,10 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { every { sendSnackbarData(data = any(), relay = any()) } just runs } + private val mockOrganizationEventManager: OrganizationEventManager = mockk { + every { trackEvent(any()) } just runs + } + @BeforeEach fun setup() { mockkStatic(SavedStateHandle::toLeaveOrganizationArgs) @@ -108,8 +114,9 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") @Test - fun `LeaveOrganizationClick with Success should send snackbar and navigate to vault`() = + fun `LeaveOrganizationClick with Success should track ItemOrganizationDeclined event, send snackbar, and navigate to vault`() = runTest { coEvery { mockAuthRepository.leaveOrganization(ORGANIZATION_ID) @@ -128,6 +135,9 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { message = BitwardenString.you_left_the_organization.asText(), ), ) + mockOrganizationEventManager.trackEvent( + event = OrganizationEvent.ItemOrganizationDeclined, + ) } } @@ -176,6 +186,7 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { val viewModel = LeaveOrganizationViewModel( authRepository = mockAuthRepository, snackbarRelayManager = mockSnackbarRelayManager, + organizationEventManager = mockOrganizationEventManager, savedStateHandle = savedStateHandle, ) @@ -192,6 +203,7 @@ class LeaveOrganizationViewModelTest : BaseViewModelTest() { return LeaveOrganizationViewModel( authRepository = mockAuthRepository, snackbarRelayManager = mockSnackbarRelayManager, + organizationEventManager = mockOrganizationEventManager, savedStateHandle = savedStateHandle, ) } 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 26f61fcf0b..d64d1227ae 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 @@ -5,9 +5,15 @@ import app.cash.turbine.test import com.bitwarden.ui.platform.base.BaseViewModelTest import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.asText +import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager +import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent import io.mockk.every +import io.mockk.just +import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.unmockkStatic +import io.mockk.verify import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals @@ -17,6 +23,10 @@ import org.junit.jupiter.api.Test class MigrateToMyItemsViewModelTest : BaseViewModelTest() { + private val mockOrganizationEventManager: OrganizationEventManager = mockk { + every { trackEvent(any()) } just runs + } + @BeforeEach fun setup() { mockkStatic(SavedStateHandle::toMigrateToMyItemsArgs) @@ -60,8 +70,9 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() { } } + @Suppress("MaxLineLength") @Test - fun `MigrateToMyItemsResultReceived with success should clear dialog and navigate to vault`() = + fun `MigrateToMyItemsResultReceived with success should track ItemOrganizationAccepted event, clear dialog, and navigate to vault`() = runTest { val viewModel = createViewModel() @@ -78,6 +89,12 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() { } assertNull(viewModel.stateFlow.value.dialog) + + verify { + mockOrganizationEventManager.trackEvent( + event = OrganizationEvent.ItemOrganizationAccepted, + ) + } } @Test @@ -158,6 +175,7 @@ class MigrateToMyItemsViewModelTest : BaseViewModelTest() { organizationName = ORGANIZATION_NAME, ) return MigrateToMyItemsViewModel( + organizationEventManager = mockOrganizationEventManager, savedStateHandle = savedStateHandle, ) }