diff --git a/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt b/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt index 807a3fdb7b..73a12c4dcb 100644 --- a/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt +++ b/app/src/main/kotlin/com/wire/android/datastore/UserDataStore.kt @@ -92,7 +92,7 @@ class UserDataStore(private val context: Context, userId: UserId) { } fun isCreateTeamNoticeRead(): Flow = context.dataStore.data.map { - it[IS_CREATE_TEAM_NOTICE_READ] ?: true + it[IS_CREATE_TEAM_NOTICE_READ] ?: false } suspend fun setIsCreateTeamNoticeRead(isRead: Boolean) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt index 3ebd68d2f9..5648ace4cf 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt @@ -38,7 +38,6 @@ import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -167,7 +166,10 @@ fun HomeScreen( homeDrawerState = homeDrawerViewModel.drawerState, homeStateHolder = homeScreenState, onNewConversationClick = { navigator.navigate(NavigationCommand(NewConversationSearchPeopleScreenDestination)) }, - onSelfUserClick = remember(navigator) { { navigator.navigate(NavigationCommand(SelfUserProfileScreenDestination)) } } + onSelfUserClick = { + homeViewModel.sendOpenProfileEvent() + navigator.navigate(NavigationCommand(SelfUserProfileScreenDestination)) + } ) BackHandler(homeScreenState.drawerState.isOpen) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt index ed52129f5c..7d0444dea4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeViewModel.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.home +import androidx.annotation.VisibleForTesting import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -25,6 +26,8 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.migration.userDatabase.ShouldTriggerMigrationForUserUserCase import com.wire.android.model.ImageAsset.UserAvatarAsset import com.wire.android.model.NameBasedAvatar @@ -51,9 +54,11 @@ class HomeViewModel @Inject constructor( private val needsToRegisterClient: NeedsToRegisterClientUseCase, private val observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase, private val wireSessionImageLoader: WireSessionImageLoader, - private val shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase + private val shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase, + private val analyticsManager: AnonymousAnalyticsManager ) : SavedStateViewModel(savedStateHandle) { + @VisibleForTesting var homeState by mutableStateOf(HomeState()) private set @@ -136,4 +141,8 @@ class HomeViewModel @Inject constructor( homeState = homeState.copy(shouldDisplayWelcomeMessage = false) } } + + fun sendOpenProfileEvent() { + analyticsManager.sendEvent(AnalyticsEvent.UserProfileOpened(homeState.shouldShowCreateTeamUnreadIndicator)) + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index 04a9391523..66fa1d87b4 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -139,6 +139,7 @@ fun SelfUserProfileScreen( navigator.navigate(NavigationCommand(SelfQRCodeScreenDestination(viewModelSelf.userProfileState.userName))) }, onCreateAccount = { + viewModelSelf.sendPersonalToTeamMigrationEvent() navigator.navigate(NavigationCommand(TeamMigrationScreenDestination)) }, isUserInCall = viewModelSelf::isUserInCall, diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt index 6c21fc4239..99d9ec2889 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModel.kt @@ -104,7 +104,7 @@ class SelfUserProfileViewModel @Inject constructor( private val notificationManager: WireNotificationManager, private val globalDataStore: GlobalDataStore, private val qualifiedIdMapper: QualifiedIdMapper, - private val analyticsManager: AnonymousAnalyticsManager + private val anonymousAnalyticsManager: AnonymousAnalyticsManager ) : ViewModel() { var userProfileState by mutableStateOf(SelfUserProfileState(userId = selfUserId, isAvatarLoading = true)) @@ -342,10 +342,18 @@ class SelfUserProfileViewModel @Inject constructor( } fun trackQrCodeClick() { - analyticsManager.sendEvent(AnalyticsEvent.QrCode.Click(!userProfileState.teamName.isNullOrBlank())) + anonymousAnalyticsManager.sendEvent(AnalyticsEvent.QrCode.Click(!userProfileState.teamName.isNullOrBlank())) + } + + fun sendPersonalToTeamMigrationEvent() { + anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.ClickedPersonalTeamMigrationCta( + createTeamButtonClicked = true + ) + ) } sealed class ErrorCodes { - object DownloadUserInfoError : ErrorCodes() + data object DownloadUserInfoError : ErrorCodes() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/PersonalToTeamMigrationNavGraph.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/PersonalToTeamMigrationNavGraph.kt index a61c7f8cba..88558414d5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/PersonalToTeamMigrationNavGraph.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/PersonalToTeamMigrationNavGraph.kt @@ -18,9 +18,7 @@ package com.wire.android.ui.userprofile.teammigration import com.ramcosta.composedestinations.annotation.NavGraph -import com.ramcosta.composedestinations.annotation.RootNavGraph -@RootNavGraph @NavGraph annotation class PersonalToTeamMigrationNavGraph( val start: Boolean = false diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt index 60336b4abc..42d921e53d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -99,6 +98,7 @@ fun TeamMigrationScreen( if (navController.currentDestination?.route == NavGraphs.personalToTeamMigration.destinations.last().route) { navigator.navigateBack() } else { + teamMigrationViewModel.sendPersonalToTeamMigrationDismissed() teamMigrationViewModel.showMigrationLeaveDialog() } } @@ -116,10 +116,7 @@ fun TeamMigrationScreen( dependenciesContainerBuilder = { dependency(navigator) dependency(NavGraphs.personalToTeamMigration) { - val parentEntry = remember(navBackStackEntry) { - navController.getBackStackEntry(NavGraphs.personalToTeamMigration.route) - } - hiltViewModel(parentEntry) + teamMigrationViewModel } } ) @@ -128,10 +125,16 @@ fun TeamMigrationScreen( if (teamMigrationViewModel.teamMigrationState.shouldShowMigrationLeaveDialog) { ConfirmMigrationLeaveDialog( onContinue = { + teamMigrationViewModel.sendPersonalTeamCreationFlowCanceledEvent( + modalContinueClicked = true + ) teamMigrationViewModel.hideMigrationLeaveDialog() } ) { teamMigrationViewModel.hideMigrationLeaveDialog() + teamMigrationViewModel.sendPersonalTeamCreationFlowCanceledEvent( + modalLeaveClicked = true + ) navigator.navigateBack() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt index 90eb57b456..89494b1f5f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt @@ -21,11 +21,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class TeamMigrationViewModel @Inject constructor() : ViewModel() { +class TeamMigrationViewModel @Inject constructor( + private val anonymousAnalyticsManager: AnonymousAnalyticsManager +) : ViewModel() { var teamMigrationState by mutableStateOf(TeamMigrationState()) private set @@ -37,4 +41,44 @@ class TeamMigrationViewModel @Inject constructor() : ViewModel() { fun hideMigrationLeaveDialog() { teamMigrationState = teamMigrationState.copy(shouldShowMigrationLeaveDialog = false) } + + fun sendPersonalToTeamMigrationDismissed() { + anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.ClickedPersonalTeamMigrationCta( + dismissCreateTeamButtonClicked = true + ) + ) + } + + fun sendPersonalTeamCreationFlowStartedEvent(step: Int) { + anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowStarted(step) + ) + } + + fun sendPersonalTeamCreationFlowCanceledEvent( + modalLeaveClicked: Boolean? = null, + modalContinueClicked: Boolean? = null + ) { + anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowCanceled( + teamName = teamMigrationState.teamNameTextState.text.toString(), + modalLeaveClicked = modalLeaveClicked, + modalContinueClicked = modalContinueClicked + ) + ) + } + + fun sendPersonalTeamCreationFlowCompletedEvent( + modalOpenTeamManagementButtonClicked: Boolean? = null, + backToWireButtonClicked: Boolean? = null + ) { + anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowCompleted( + teamName = teamMigrationState.teamNameTextState.text.toString(), + modalOpenTeamManagementButtonClicked = modalOpenTeamManagementButtonClicked, + backToWireButtonClicked = backToWireButtonClicked + ) + ) + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt index ef884500d9..f73d88d402 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.material.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -55,6 +56,7 @@ import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireTypography import com.wire.android.ui.userprofile.teammigration.common.BottomLineButtons import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph +import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel import com.wire.android.util.CustomTabsHelper import com.wire.android.util.ui.PreviewMultipleThemes @@ -64,13 +66,18 @@ import com.wire.android.util.ui.PreviewMultipleThemes ) @Composable fun TeamMigrationTeamPlanStepScreen( - navigator: DestinationsNavigator + navigator: DestinationsNavigator, + teamMigrationViewModel: TeamMigrationViewModel ) { TeamMigrationTeamPlanStepScreenContent( onContinueButtonClicked = { navigator.navigate(TeamMigrationTeamNameStepScreenDestination) } ) + + LaunchedEffect(Unit) { + teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(1) + } } @Composable diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt index ebf288845c..468c20d4f7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -66,6 +67,9 @@ fun TeamMigrationTeamNameStepScreen( }, teamNameTextFieldState = teamMigrationViewModel.teamMigrationState.teamNameTextState ) + LaunchedEffect(Unit) { + teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(2) + } } @Composable diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt index 92bee2e9f1..cede3cf147 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -72,6 +73,7 @@ fun TeamMigrationConfirmationStepScreen( navigator: DestinationsNavigator, teamMigrationViewModel: TeamMigrationViewModel ) { + TeamMigrationConfirmationStepScreenContent( onContinueButtonClicked = { // TODO: call the API to migrate the user to the team, if successful navigate to next screen @@ -82,6 +84,9 @@ fun TeamMigrationConfirmationStepScreen( }, passwordTextState = teamMigrationViewModel.teamMigrationState.passwordTextState ) + LaunchedEffect(Unit) { + teamMigrationViewModel.sendPersonalTeamCreationFlowStartedEvent(3) + } } @Composable diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt index 2c6d5647db..cd2b4c3f1b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt @@ -65,6 +65,9 @@ fun TeamMigrationDoneStepScreen( val teamManagementUrl = stringResource(R.string.url_team_management) TeamMigrationDoneStepContent( onBackToWireClicked = { + teamMigrationViewModel.sendPersonalTeamCreationFlowCompletedEvent( + backToWireButtonClicked = true + ) navigator.navigate( NavigationCommand( HomeScreenDestination, @@ -73,6 +76,9 @@ fun TeamMigrationDoneStepScreen( ) }, onOpenTeamManagementClicked = { + teamMigrationViewModel.sendPersonalTeamCreationFlowCompletedEvent( + modalOpenTeamManagementButtonClicked = true + ) CustomTabsHelper.launchUrl(context, teamManagementUrl) }, teamName = teamMigrationViewModel.teamMigrationState.teamNameTextState.text.toString() diff --git a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt index 35c54f1a20..ba7d91972c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/HomeViewModelTest.kt @@ -21,6 +21,8 @@ import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.datastore.GlobalDataStore import com.wire.android.datastore.UserDataStore +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.framework.TestUser import com.wire.android.migration.userDatabase.ShouldTriggerMigrationForUserUserCase import com.wire.android.util.ui.WireSessionImageLoader @@ -33,6 +35,7 @@ import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -46,14 +49,16 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(CoroutineTestExtension::class) class HomeViewModelTest { @Test - fun `given legal hold request pending, then shouldDisplayLegalHoldIndicator is true`() = runTest { - // given - val (_, viewModel) = Arrangement() - .withLegalHoldStatus(flowOf(LegalHoldStateForSelfUser.PendingRequest)) - .arrange() - // then - assertEquals(true, viewModel.homeState.shouldDisplayLegalHoldIndicator) - } + fun `given legal hold request pending, then shouldDisplayLegalHoldIndicator is true`() = + runTest { + // given + val (_, viewModel) = Arrangement() + .withLegalHoldStatus(flowOf(LegalHoldStateForSelfUser.PendingRequest)) + .arrange() + // then + assertEquals(true, viewModel.homeState.shouldDisplayLegalHoldIndicator) + } + @Test fun `given legal hold enabled, then shouldDisplayLegalHoldIndicator is true`() = runTest { // given @@ -63,28 +68,51 @@ class HomeViewModelTest { // then assertEquals(true, viewModel.homeState.shouldDisplayLegalHoldIndicator) } + @Test - fun `given legal hold disabled and no request available, then shouldDisplayLegalHoldIndicator is false`() = runTest { - // given - val (_, viewModel) = Arrangement() - .withLegalHoldStatus(flowOf(LegalHoldStateForSelfUser.Disabled)) - .arrange() - // then - assertEquals(false, viewModel.homeState.shouldDisplayLegalHoldIndicator) - } + fun `given legal hold disabled and no request available, then shouldDisplayLegalHoldIndicator is false`() = + runTest { + // given + val (_, viewModel) = Arrangement() + .withLegalHoldStatus(flowOf(LegalHoldStateForSelfUser.Disabled)) + .arrange() + // then + assertEquals(false, viewModel.homeState.shouldDisplayLegalHoldIndicator) + } + @Test - fun `given legal hold enabled, when user status changes, then shouldDisplayLegalHoldIndicator should keep the same`() = runTest { - // given - val selfFlow = MutableStateFlow(TestUser.SELF_USER.copy(availabilityStatus = UserAvailabilityStatus.AVAILABLE)) - val (_, viewModel) = Arrangement() - .withLegalHoldStatus(flowOf(LegalHoldStateForSelfUser.Enabled)) - .withGetSelf(selfFlow) - .arrange() - // when - selfFlow.emit(TestUser.SELF_USER.copy(availabilityStatus = UserAvailabilityStatus.AWAY)) - // then - assertEquals(true, viewModel.homeState.shouldDisplayLegalHoldIndicator) - } + fun `given legal hold enabled, when user status changes, then shouldDisplayLegalHoldIndicator should keep the same`() = + runTest { + // given + val selfFlow = + MutableStateFlow(TestUser.SELF_USER.copy(availabilityStatus = UserAvailabilityStatus.AVAILABLE)) + val (_, viewModel) = Arrangement() + .withLegalHoldStatus(flowOf(LegalHoldStateForSelfUser.Enabled)) + .withGetSelf(selfFlow) + .arrange() + // when + selfFlow.emit(TestUser.SELF_USER.copy(availabilityStatus = UserAvailabilityStatus.AWAY)) + // then + assertEquals(true, viewModel.homeState.shouldDisplayLegalHoldIndicator) + } + + @Test + fun `given open profile event, when sendOpenProfileEvent is called, then send the event with the unread indicator value`() = + runTest { + val (arrangement, viewModel) = Arrangement() + .withLegalHoldStatus(flowOf(LegalHoldStateForSelfUser.Enabled)) + .arrange() + + viewModel.sendOpenProfileEvent() + + verify(exactly = 1) { + arrangement.analyticsManager.sendEvent( + AnalyticsEvent.UserProfileOpened( + isMigrationDotActive = viewModel.homeState.shouldShowCreateTeamUnreadIndicator + ) + ) + } + } internal class Arrangement { @@ -112,6 +140,9 @@ class HomeViewModelTest { @MockK lateinit var shouldTriggerMigrationForUser: ShouldTriggerMigrationForUserUserCase + @MockK + lateinit var analyticsManager: AnonymousAnalyticsManager + private val viewModel by lazy { HomeViewModel( savedStateHandle = savedStateHandle, @@ -121,19 +152,24 @@ class HomeViewModelTest { needsToRegisterClient = needsToRegisterClient, observeLegalHoldStatusForSelfUser = observeLegalHoldStatusForSelfUser, wireSessionImageLoader = wireSessionImageLoader, - shouldTriggerMigrationForUser = shouldTriggerMigrationForUser + shouldTriggerMigrationForUser = shouldTriggerMigrationForUser, + analyticsManager = analyticsManager ) } + init { MockKAnnotations.init(this, relaxUnitFun = true) withGetSelf(flowOf(TestUser.SELF_USER)) } + fun withGetSelf(result: Flow) = apply { coEvery { getSelf.invoke() } returns result } + fun withLegalHoldStatus(result: Flow) = apply { coEvery { observeLegalHoldStatusForSelfUser.invoke() } returns result } + fun arrange() = this to viewModel } } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt index 4479bcd837..9ea1e9b925 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelArrangement.kt @@ -51,45 +51,63 @@ import kotlinx.coroutines.flow.flowOf class SelfUserProfileViewModelArrangement { @MockK lateinit var userDataStore: UserDataStore + @MockK lateinit var getSelf: GetSelfUserUseCase + @MockK lateinit var getSelfTeam: GetUpdatedSelfTeamUseCase + @MockK lateinit var observeValidAccounts: ObserveValidAccountsUseCase + @MockK lateinit var updateStatus: UpdateSelfAvailabilityStatusUseCase + @MockK lateinit var logout: LogoutUseCase + @MockK lateinit var observeLegalHoldStatusForSelfUser: ObserveLegalHoldStateForSelfUserUseCase + @MockK lateinit var dispatchers: DispatcherProvider + @MockK lateinit var wireSessionImageLoader: WireSessionImageLoader + @MockK lateinit var authServerConfigProvider: AuthServerConfigProvider + @MockK lateinit var selfServerLinks: SelfServerConfigUseCase + @MockK lateinit var otherAccountMapper: OtherAccountMapper + @MockK lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase + @MockK lateinit var accountSwitch: AccountSwitchUseCase + @MockK lateinit var endCall: EndCallUseCase + @MockK lateinit var isReadOnlyAccount: IsReadOnlyAccountUseCase + @MockK lateinit var notificationManager: WireNotificationManager + @MockK lateinit var globalDataStore: GlobalDataStore + @MockK lateinit var qualifiedIdMapper: QualifiedIdMapper @MockK - lateinit var analyticsManager: AnonymousAnalyticsManager + lateinit var anonymousAnalyticsManager: AnonymousAnalyticsManager private val viewModel by lazy { SelfUserProfileViewModel( @@ -113,7 +131,7 @@ class SelfUserProfileViewModelArrangement { notificationManager = notificationManager, globalDataStore = globalDataStore, qualifiedIdMapper = qualifiedIdMapper, - analyticsManager = analyticsManager + anonymousAnalyticsManager = anonymousAnalyticsManager ) } @@ -127,8 +145,10 @@ class SelfUserProfileViewModelArrangement { coEvery { isReadOnlyAccount.invoke() } returns false coEvery { observeEstablishedCalls.invoke() } returns flowOf(emptyList()) } + fun withLegalHoldStatus(result: LegalHoldStateForSelfUser) = apply { coEvery { observeLegalHoldStatusForSelfUser.invoke() } returns flowOf(result) } + fun arrange() = this to viewModel } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelTest.kt index 95b4fd9397..74d035e56c 100644 --- a/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileViewModelTest.kt @@ -19,8 +19,10 @@ package com.wire.android.ui.userprofile.self import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension +import com.wire.android.feature.analytics.model.AnalyticsEvent import com.wire.android.ui.legalhold.banner.LegalHoldUIState import com.wire.kalium.logic.feature.legalhold.LegalHoldStateForSelfUser +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals @@ -53,12 +55,31 @@ class SelfUserProfileViewModelTest { } @Test - fun `given legal hold disabled and no request available, then isUnderLegalHold is none`() = runTest { - // given - val (_, viewModel) = SelfUserProfileViewModelArrangement() - .withLegalHoldStatus(LegalHoldStateForSelfUser.Disabled) - .arrange() - // then - assertEquals(LegalHoldUIState.None, viewModel.userProfileState.legalHoldStatus) - } + fun `given legal hold disabled and no request available, then isUnderLegalHold is none`() = + runTest { + // given + val (_, viewModel) = SelfUserProfileViewModelArrangement() + .withLegalHoldStatus(LegalHoldStateForSelfUser.Disabled) + .arrange() + // then + assertEquals(LegalHoldUIState.None, viewModel.userProfileState.legalHoldStatus) + } + + @Test + fun `given a createTeamButtonClicked event, when sendPersonalToTeamMigrationEvent is called, then the event is sent`() = + runTest { + val (arrangement, viewModel) = SelfUserProfileViewModelArrangement() + .withLegalHoldStatus(LegalHoldStateForSelfUser.Disabled) + .arrange() + + viewModel.sendPersonalToTeamMigrationEvent() + + verify(exactly = 1) { + arrangement.anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.ClickedPersonalTeamMigrationCta( + createTeamButtonClicked = true + ) + ) + } + } } diff --git a/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt new file mode 100644 index 0000000000..331f4b5c00 --- /dev/null +++ b/app/src/test/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModelTest.kt @@ -0,0 +1,173 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.userprofile.teammigration + +import com.wire.android.feature.analytics.AnonymousAnalyticsManager +import com.wire.android.feature.analytics.model.AnalyticsEvent +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.internal.assertEquals +import org.junit.jupiter.api.Test + +class TeamMigrationViewModelTest { + + @Test + fun `given dialog state, when showMigrationLeaveDialog is called, then update shouldShowMigrationLeaveDialog to true`() = + runTest { + val (_, viewModel) = Arrangement() + .arrange() + + viewModel.showMigrationLeaveDialog() + + assertEquals(true, viewModel.teamMigrationState.shouldShowMigrationLeaveDialog) + } + + @Test + fun `given dialog state, when hideMigrationLeaveDialog is called, then update shouldShowMigrationLeaveDialog to false`() = + runTest { + val (_, viewModel) = Arrangement() + .arrange() + + viewModel.hideMigrationLeaveDialog() + + assertEquals(false, viewModel.teamMigrationState.shouldShowMigrationLeaveDialog) + } + + @Test + fun `given close modal event, when sendPersonalToTeamMigrationDismissed is called, then send the event`() = + runTest { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.sendPersonalToTeamMigrationDismissed() + + verify(exactly = 1) { + arrangement.anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.ClickedPersonalTeamMigrationCta( + dismissCreateTeamButtonClicked = true + ) + ) + } + } + + @Test + fun `given the step of migration flow, when sendPersonalTeamCreationFlowStartedEvent is called, then send the event`() = + runTest { + val step = 2 + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.sendPersonalTeamCreationFlowStartedEvent(2) + + verify(exactly = 1) { + arrangement.anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowStarted(step) + ) + } + } + + @Test + fun `given modalLeaveClicked event, when sendPersonalTeamCreationFlowCanceledEvent is called, then send the event`() = + runTest { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.sendPersonalTeamCreationFlowCanceledEvent(modalLeaveClicked = true) + + verify(exactly = 1) { + arrangement.anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowCanceled( + teamName = viewModel.teamMigrationState.teamNameTextState.text.toString(), + modalLeaveClicked = true + ) + ) + } + } + + @Test + fun `given modalContinueClicked event, when sendPersonalTeamCreationFlowCanceledEvent is called, then send the event`() = + runTest { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.sendPersonalTeamCreationFlowCanceledEvent(modalContinueClicked = true) + + verify(exactly = 1) { + arrangement.anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowCanceled( + teamName = viewModel.teamMigrationState.teamNameTextState.text.toString(), + modalContinueClicked = true + ) + ) + } + } + + @Test + fun `given modalOpenTeamManagementButtonClicked event, when sendPersonalTeamCreationFlowCompletedEvent is called, then send the event`() = + runTest { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.sendPersonalTeamCreationFlowCompletedEvent( + modalOpenTeamManagementButtonClicked = true + ) + + verify(exactly = 1) { + arrangement.anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowCompleted( + teamName = viewModel.teamMigrationState.teamNameTextState.text.toString(), + modalOpenTeamManagementButtonClicked = true + ) + ) + } + } + + @Test + fun `given backToWireButtonClicked event, when sendPersonalTeamCreationFlowCompletedEvent is called, then send the event`() = + runTest { + val (arrangement, viewModel) = Arrangement() + .arrange() + + viewModel.sendPersonalTeamCreationFlowCompletedEvent(backToWireButtonClicked = true) + + verify(exactly = 1) { + arrangement.anonymousAnalyticsManager.sendEvent( + AnalyticsEvent.PersonalTeamMigration.PersonalTeamCreationFlowCompleted( + teamName = viewModel.teamMigrationState.teamNameTextState.text.toString(), + backToWireButtonClicked = true + ) + ) + } + } + + private class Arrangement { + + @MockK + lateinit var anonymousAnalyticsManager: AnonymousAnalyticsManager + + init { + MockKAnnotations.init(this, relaxUnitFun = true) + } + + fun arrange() = this to TeamMigrationViewModel( + anonymousAnalyticsManager = anonymousAnalyticsManager + ) + } +} diff --git a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt index cb1e4ecbcd..b08eb4585b 100644 --- a/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt +++ b/core/analytics/src/main/kotlin/com/wire/android/feature/analytics/model/AnalyticsEvent.kt @@ -24,10 +24,24 @@ import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_ import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_KEY import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_LABEL_NOT_DISPLAYED import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CALLING_QUALITY_REVIEW_SCORE_KEY +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CLICKED_CREATE_TEAM +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CLICKED_DISMISS_CTA +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CLICKED_PERSONAL_MIGRATION_CTA_EVENT import com.wire.android.feature.analytics.model.AnalyticsEventConstants.CONTRIBUTED_LOCATION import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MESSAGE_ACTION_KEY import com.wire.android.feature.analytics.model.AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL import com.wire.android.feature.analytics.model.AnalyticsEventConstants.QR_CODE_SEGMENTATION_USER_TYPE_TEAM +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MIGRATION_DOT_ACTIVE +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MODAL_BACK_TO_WIRE_CLICKED +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MODAL_CONTINUE_CLICKED +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MODAL_LEAVE_CLICKED +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MODAL_OPEN_TEAM_MANAGEMENT_CLICKED +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.MODAL_TEAM_NAME +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.PERSONAL_TEAM_CREATION_FLOW_CANCELLED +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.PERSONAL_TEAM_CREATION_FLOW_COMPLETED +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.PERSONAL_TEAM_CREATION_FLOW_STARTED_EVENT +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.STEP_MODAL_CREATE_TEAM +import com.wire.android.feature.analytics.model.AnalyticsEventConstants.USER_PROFILE_OPENED interface AnalyticsEvent { /** @@ -221,6 +235,93 @@ interface AnalyticsEvent { } } } + + data class UserProfileOpened(val isMigrationDotActive: Boolean) : AnalyticsEvent { + override val key: String = USER_PROFILE_OPENED + + override fun toSegmentation(): Map { + return mapOf( + MIGRATION_DOT_ACTIVE to isMigrationDotActive + ) + } + } + + sealed interface PersonalTeamMigration : AnalyticsEvent { + + data class ClickedPersonalTeamMigrationCta( + val createTeamButtonClicked: Boolean? = null, + val dismissCreateTeamButtonClicked: Boolean? = null + ) : AnalyticsEvent { + override val key: String = CLICKED_PERSONAL_MIGRATION_CTA_EVENT + + override fun toSegmentation(): Map { + val segmentations = mutableMapOf() + createTeamButtonClicked?.let { + segmentations.put(CLICKED_CREATE_TEAM, it) + } + dismissCreateTeamButtonClicked?.let { + segmentations.put(CLICKED_DISMISS_CTA, it) + } + return segmentations + } + } + + data class PersonalTeamCreationFlowStarted( + val step: Int + ) : AnalyticsEvent { + override val key: String = PERSONAL_TEAM_CREATION_FLOW_STARTED_EVENT + + override fun toSegmentation(): Map { + return mapOf( + STEP_MODAL_CREATE_TEAM to step + ) + } + } + + data class PersonalTeamCreationFlowCanceled( + val teamName: String?, + val modalLeaveClicked: Boolean? = null, + val modalContinueClicked: Boolean? = null + ) : AnalyticsEvent { + override val key: String = PERSONAL_TEAM_CREATION_FLOW_CANCELLED + + override fun toSegmentation(): Map { + val segmentations = mutableMapOf() + modalLeaveClicked?.let { + segmentations.put(MODAL_LEAVE_CLICKED, it) + } + modalContinueClicked?.let { + segmentations.put(MODAL_CONTINUE_CLICKED, it) + } + teamName?.let { + segmentations.put(MODAL_TEAM_NAME, it) + } + return segmentations + } + } + + data class PersonalTeamCreationFlowCompleted( + val teamName: String? = null, + val modalOpenTeamManagementButtonClicked: Boolean? = null, + val backToWireButtonClicked: Boolean? = null + ) : AnalyticsEvent { + override val key: String = PERSONAL_TEAM_CREATION_FLOW_COMPLETED + + override fun toSegmentation(): Map { + val segmentations = mutableMapOf() + teamName?.let { + segmentations.put(MODAL_TEAM_NAME, it) + } + modalOpenTeamManagementButtonClicked?.let { + segmentations.put(MODAL_OPEN_TEAM_MANAGEMENT_CLICKED, it) + } + backToWireButtonClicked?.let { + segmentations.put(MODAL_BACK_TO_WIRE_CLICKED, it) + } + return segmentations + } + } + } } object AnalyticsEventConstants { @@ -281,4 +382,26 @@ object AnalyticsEventConstants { const val QR_CODE_SEGMENTATION_USER_TYPE = "user_type" const val QR_CODE_SEGMENTATION_USER_TYPE_PERSONAL = "personal" const val QR_CODE_SEGMENTATION_USER_TYPE_TEAM = "team" + + /** + * user profile + */ + const val USER_PROFILE_OPENED = "ui.clicked-profile" + + /** + * Personal to team migration + */ + const val CLICKED_PERSONAL_MIGRATION_CTA_EVENT = "ui.clicked-personal-migration-cta" + const val PERSONAL_TEAM_CREATION_FLOW_STARTED_EVENT = "user.personal-team-creation-flow-started" + const val PERSONAL_TEAM_CREATION_FLOW_CANCELLED = "user.personal-team-creation-flow-cancelled" + const val PERSONAL_TEAM_CREATION_FLOW_COMPLETED = "user.personal-team-creation-flow-completed" + const val MIGRATION_DOT_ACTIVE = "migration_dot_active" + const val CLICKED_CREATE_TEAM = "clicked_create_team" + const val CLICKED_DISMISS_CTA = "clicked_dismiss_cta" + const val STEP_MODAL_CREATE_TEAM = "step_modalcreateteam" + const val MODAL_TEAM_NAME = "modal_team-name" + const val MODAL_CONTINUE_CLICKED = "modal_continue-clicked" + const val MODAL_LEAVE_CLICKED = "modal_leave-clicked" + const val MODAL_BACK_TO_WIRE_CLICKED = "modal_back-to-wire-clicked" + const val MODAL_OPEN_TEAM_MANAGEMENT_CLICKED = "modal_open-tm-clicked" }