From 05c53e851e2c0e9c0a2318d8e9d4862988b3179b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Zag=C3=B3rski?= Date: Mon, 16 Dec 2024 10:30:27 +0100 Subject: [PATCH] feat: alerts for non paying users [WPB-1826] (#3715) --- .../android/di/accountScoped/CallsModule.kt | 5 + .../calling/CallingFeatureActivatedDialog.kt | 44 +++ .../CallingFeatureUnavailableDialog.kt | 45 ++++ .../home/conversations/ConversationScreen.kt | 46 +++- .../ConversationScreenDialogType.kt | 3 + .../call/ConversationListCallViewModel.kt | 27 +- app/src/main/res/values/strings.xml | 10 + .../call/ConversationListCallViewModelTest.kt | 250 ++++++++++++------ 8 files changed, 341 insertions(+), 89 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureActivatedDialog.kt diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt index 516dc982bfd..803e627db87 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt @@ -197,4 +197,9 @@ class CallsModule { @Provides fun provideIsEligibleToStartCall(callsScope: CallsScope) = callsScope.isEligibleToStartCall + + @ViewModelScoped + @Provides + fun provideObserveConferenceCallingEnabledUseCase(callsScope: CallsScope) = + callsScope.observeConferenceCallingEnabled } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureActivatedDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureActivatedDialog.kt new file mode 100644 index 00000000000..370f24eb6ca --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureActivatedDialog.kt @@ -0,0 +1,44 @@ +/* + * 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.common.dialogs.calling + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.ui.common.DialogTextSuffixLink +import com.wire.android.ui.common.WireDialog +import com.wire.android.ui.common.WireDialogButtonProperties +import com.wire.android.ui.common.WireDialogButtonType + +@Composable +fun CallingFeatureActivatedDialog(onDialogDismiss: () -> Unit) { + WireDialog( + title = stringResource(id = R.string.calling_feature_enabled_title_alert), + text = stringResource(id = R.string.calling_feature_enabled_message_alert), + onDismiss = onDialogDismiss, + textSuffixLink = DialogTextSuffixLink( + linkText = stringResource(R.string.calling_feature_enabled_message_link_alert), + linkUrl = stringResource(R.string.url_wire_enterprise) + ), + optionButton1Properties = WireDialogButtonProperties( + onClick = onDialogDismiss, + text = stringResource(id = R.string.label_ok), + type = WireDialogButtonType.Primary + ) + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureUnavailableDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureUnavailableDialog.kt index 21864ebd840..55926a5503b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureUnavailableDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/calling/CallingFeatureUnavailableDialog.kt @@ -21,6 +21,7 @@ package com.wire.android.ui.common.dialogs.calling import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.wire.android.R +import com.wire.android.ui.common.DialogTextSuffixLink import com.wire.android.ui.common.WireDialog import com.wire.android.ui.common.WireDialogButtonProperties import com.wire.android.ui.common.WireDialogButtonType @@ -38,3 +39,47 @@ fun CallingFeatureUnavailableDialog(onDialogDismiss: () -> Unit) { ) ) } + +@Composable +fun CallingFeatureUnavailableTeamMemberDialog(onDialogDismiss: () -> Unit) { + WireDialog( + title = stringResource(id = R.string.calling_feature_unavailable_title_alert), + text = stringResource(id = R.string.calling_feature_unavailable_team_member_message_alert), + onDismiss = onDialogDismiss, + optionButton1Properties = WireDialogButtonProperties( + onClick = onDialogDismiss, + text = stringResource(id = R.string.label_ok), + type = WireDialogButtonType.Primary + ) + ) +} + +@Composable +fun CallingFeatureUnavailableTeamAdminDialog( + onUpgradeAction: (String) -> Unit, + onDialogDismiss: () -> Unit +) { + val upgradeLink = stringResource(R.string.url_team_management_login) + WireDialog( + title = stringResource(id = R.string.calling_feature_unavailable_team_admin_title_alert), + text = stringResource(id = R.string.calling_feature_unavailable_team_admin_message_alert), + onDismiss = onDialogDismiss, + textSuffixLink = DialogTextSuffixLink( + linkText = stringResource(R.string.calling_feature_unavailable_team_admin_message_link_alert), + linkUrl = stringResource(R.string.url_team_management_login) + ), + optionButton2Properties = WireDialogButtonProperties( + onClick = { + onUpgradeAction(upgradeLink) + onDialogDismiss() + }, + text = stringResource(id = R.string.calling_feature_unavailable_team_admin_upgrade_action_alert), + type = WireDialogButtonType.Primary + ), + optionButton1Properties = WireDialogButtonProperties( + onClick = onDialogDismiss, + text = stringResource(id = R.string.label_cancel), + type = WireDialogButtonType.Secondary + ) + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 164aeb80620..061b6c1a1b9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -101,7 +101,10 @@ import com.wire.android.ui.common.dialogs.InvalidLinkDialog import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog import com.wire.android.ui.common.dialogs.SureAboutMessagingInDegradedConversationDialog import com.wire.android.ui.common.dialogs.VisitLinkDialog +import com.wire.android.ui.common.dialogs.calling.CallingFeatureActivatedDialog import com.wire.android.ui.common.dialogs.calling.CallingFeatureUnavailableDialog +import com.wire.android.ui.common.dialogs.calling.CallingFeatureUnavailableTeamAdminDialog +import com.wire.android.ui.common.dialogs.calling.CallingFeatureUnavailableTeamMemberDialog import com.wire.android.ui.common.dialogs.calling.ConfirmStartCallDialog import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.dialogs.calling.OngoingActiveCallDialog @@ -173,6 +176,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.MessageAssetStatus import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.call.usecase.ConferenceCallingResult import kotlinx.collections.immutable.PersistentMap import kotlinx.coroutines.CoroutineScope @@ -299,6 +303,12 @@ fun ConversationScreen( } } + LaunchedEffect(Unit) { + conversationListCallViewModel.callingEnabled.collect { + showDialog.value = ConversationScreenDialogType.CALLING_FEATURE_ACTIVATED + } + } + conversationMigrationViewModel.migratedConversationId?.let { migratedConversationId -> navigator.navigate( NavigationCommand( @@ -387,9 +397,30 @@ fun ConversationScreen( } ConversationScreenDialogType.CALLING_FEATURE_UNAVAILABLE -> { - CallingFeatureUnavailableDialog(onDialogDismiss = { + CallingFeatureUnavailableDialog { showDialog.value = ConversationScreenDialogType.NONE - }) + } + } + + ConversationScreenDialogType.CALLING_FEATURE_UNAVAILABLE_TEAM_MEMBER -> { + CallingFeatureUnavailableTeamMemberDialog { + showDialog.value = ConversationScreenDialogType.NONE + } + } + + ConversationScreenDialogType.CALLING_FEATURE_UNAVAILABLE_TEAM_ADMIN -> { + CallingFeatureUnavailableTeamAdminDialog( + onUpgradeAction = uriHandler::openUri, + onDialogDismiss = { + showDialog.value = ConversationScreenDialogType.NONE + } + ) + } + + ConversationScreenDialogType.CALLING_FEATURE_ACTIVATED -> { + CallingFeatureActivatedDialog { + showDialog.value = ConversationScreenDialogType.NONE + } } ConversationScreenDialogType.VERIFICATION_DEGRADED -> { @@ -771,7 +802,16 @@ private fun startCallIfPossible( } ConferenceCallingResult.Disabled.OngoingCall -> ConversationScreenDialogType.ONGOING_ACTIVE_CALL - ConferenceCallingResult.Disabled.Unavailable -> ConversationScreenDialogType.CALLING_FEATURE_UNAVAILABLE + ConferenceCallingResult.Disabled.Unavailable -> { + when (conversationListCallViewModel.selfTeamRole.value) { + UserType.INTERNAL -> ConversationScreenDialogType.CALLING_FEATURE_UNAVAILABLE_TEAM_MEMBER + UserType.OWNER, + UserType.ADMIN -> ConversationScreenDialogType.CALLING_FEATURE_UNAVAILABLE_TEAM_ADMIN + + else -> ConversationScreenDialogType.CALLING_FEATURE_UNAVAILABLE + } + } + else -> ConversationScreenDialogType.NONE } showDialog.value = dialogValue diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenDialogType.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenDialogType.kt index 8cada4a4eeb..28b81774094 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenDialogType.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreenDialogType.kt @@ -25,5 +25,8 @@ enum class ConversationScreenDialogType { CALL_CONFIRMATION, PING_CONFIRMATION, CALLING_FEATURE_UNAVAILABLE, + CALLING_FEATURE_UNAVAILABLE_TEAM_MEMBER, + CALLING_FEATURE_UNAVAILABLE_TEAM_ADMIN, + CALLING_FEATURE_ACTIVATED, NONE } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModel.kt index 2f533ac6044..26c7fb86589 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModel.kt @@ -32,18 +32,22 @@ import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.sync.SyncState +import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.ConferenceCallingResult import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -64,7 +68,9 @@ class ConversationListCallViewModel @Inject constructor( private val isConferenceCallingEnabled: IsEligibleToStartCallUseCase, private val observeConversationDetails: ObserveConversationDetailsUseCase, private val setUserInformedAboutVerification: SetUserInformedAboutVerificationUseCase, - private val observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase + private val observeDegradedConversationNotified: ObserveDegradedConversationNotifiedUseCase, + private val observeConferenceCallingEnabled: ObserveConferenceCallingEnabledUseCase, + private val getSelf: GetSelfUserUseCase ) : SavedStateViewModel(savedStateHandle) { private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() @@ -72,6 +78,8 @@ class ConversationListCallViewModel @Inject constructor( var conversationCallViewState by mutableStateOf(ConversationCallViewState()) val shouldInformAboutVerification = mutableStateOf(false) + val selfTeamRole = mutableStateOf(UserType.GUEST) + val callingEnabled = MutableSharedFlow(replay = 1) var establishedCallConversationId: QualifiedID? = null @@ -80,6 +88,15 @@ class ConversationListCallViewModel @Inject constructor( observeEstablishedCall() observeParticipantsForConversation() observeInformedAboutDegradedVerification() + observeSelfTeamRole() + observeCallingActivatedEvent() + } + + private fun observeCallingActivatedEvent() { + viewModelScope.launch { + observeConferenceCallingEnabled() + .collectLatest { callingEnabled.emit(Unit) } + } } private fun observeParticipantsForConversation() { @@ -91,6 +108,14 @@ class ConversationListCallViewModel @Inject constructor( } } + private fun observeSelfTeamRole() { + viewModelScope.launch { + getSelf().collectLatest { self -> + selfTeamRole.value = self.userType + } + } + } + private fun observeInformedAboutDegradedVerification() = viewModelScope.launch { observeDegradedConversationNotified(conversationId).collect { shouldInformAboutVerification.value = !it } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3583986c1a3..718e9d41961 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -280,6 +280,8 @@ https://support.wire.com/hc/articles/360002855817 https://wire.com/pricing https://teams.wire.com/ + https://teams.wire.com/login + https://wire.com/en/enterprise Vault Archive @@ -981,6 +983,14 @@ Double tap to go back Feature unavailable The option to initiate a conference call is only available in the paid version of Wire. + To start a conference call, your team needs to upgrade to the Enterprise plan. + Upgrade to Enterprise + Your team is currently on the free Basic plan. Upgrade to Enterprise for access to features such as starting conferences and more. + Learn more about Wire\'s pricing + Upgrade now + Wire Enterprise + Your team was upgraded to Wire Enterprise, which gives you access to features such as conference calls and more. + Learn more about Wire Enterprise Connecting… Start a call Are you sure you want to call %1$s people? diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModelTest.kt index 183101b7bd6..272326163e7 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/call/ConversationListCallViewModelTest.kt @@ -18,20 +18,25 @@ package com.wire.android.ui.home.conversations.call import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension +import com.wire.android.framework.TestUser import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.details.participants.usecase.ObserveParticipantsForConversationUseCase import com.wire.android.ui.navArgs import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.feature.conversation.ObserveDegradedConversationNotifiedUseCase import com.wire.kalium.logic.feature.conversation.SetUserInformedAboutVerificationUseCase +import com.wire.kalium.logic.feature.user.GetSelfUserUseCase import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -40,125 +45,200 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -@ExtendWith(CoroutineTestExtension::class) -@ExtendWith(NavigationTestExtension::class) +@ExtendWith(CoroutineTestExtension::class, NavigationTestExtension::class) class ConversationListCallViewModelTest { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - - @MockK - private lateinit var observeOngoingCalls: ObserveOngoingCallsUseCase + @Test + fun `given join dialog displayed, when user dismiss it, then hide it`() = runTest { + val (_, viewModel) = Arrangement() + .withDefaultAnswers() + .arrange() + viewModel.conversationCallViewState = viewModel.conversationCallViewState.copy( + shouldShowJoinAnywayDialog = true + ) - @MockK - private lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase + viewModel.dismissJoinCallAnywayDialog() - @MockK - private lateinit var joinCall: AnswerCallUseCase + assertEquals(false, viewModel.conversationCallViewState.shouldShowJoinAnywayDialog) + } - @MockK - private lateinit var endCall: EndCallUseCase + @Test + fun `given no ongoing call, when user tries to join a call, then invoke answerCall call use case`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withDefaultAnswers() + .withJoinCallResponse() + .arrange() + viewModel.conversationCallViewState = viewModel.conversationCallViewState.copy(hasEstablishedCall = false) + + viewModel.joinOngoingCall(arrangement.onAnswered) + + coVerify(exactly = 1) { arrangement.joinCall(conversationId = any()) } + coVerify(exactly = 1) { arrangement.onAnswered(any()) } + assertEquals(false, viewModel.conversationCallViewState.shouldShowJoinAnywayDialog) + } - @MockK - private lateinit var observeSyncState: ObserveSyncStateUseCase + @Test + fun `given an ongoing call, when user tries to join a call, then show JoinCallAnywayDialog`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withDefaultAnswers() + .arrange() + viewModel.conversationCallViewState = viewModel.conversationCallViewState.copy(hasEstablishedCall = true) - @MockK - private lateinit var isConferenceCallingEnabled: IsEligibleToStartCallUseCase + viewModel.joinOngoingCall(arrangement.onAnswered) - @MockK(relaxed = true) - private lateinit var onAnswered: (conversationId: ConversationId) -> Unit + assertEquals(true, viewModel.conversationCallViewState.shouldShowJoinAnywayDialog) + coVerify(inverse = true) { arrangement.joinCall(conversationId = any()) } + } - @MockK - private lateinit var observeConversationDetails: ObserveConversationDetailsUseCase + @Test + fun `given an ongoing call, when user confirms dialog to join a call, then end current call and join the newer one`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withDefaultAnswers() + .withEndCallResponse() + .arrange() + viewModel.conversationCallViewState = + viewModel.conversationCallViewState.copy(hasEstablishedCall = true) + viewModel.establishedCallConversationId = ConversationId("value", "Domain") + + viewModel.joinAnyway(arrangement.onAnswered) + + coVerify(exactly = 1) { arrangement.endCall(any()) } + } - @MockK - private lateinit var observeParticipantsForConversation: ObserveParticipantsForConversationUseCase + @Test + fun `given self team role as admin in conversation, when we observe own role, then its properly propagated`() = runTest { + val (_, viewModel) = Arrangement() + .withDefaultAnswers() + .withSelfAsAdmin() + .arrange() - @MockK - lateinit var setUserInformedAboutVerificationUseCase: SetUserInformedAboutVerificationUseCase + val role = viewModel.selfTeamRole - @MockK - lateinit var observeDegradedConversationNotifiedUseCase: ObserveDegradedConversationNotifiedUseCase + assertEquals(UserType.ADMIN, role.value) + } - private lateinit var conversationListCallViewModel: ConversationListCallViewModel + @Test + fun `given calling enabled event, when we observe it, then its properly propagated`() = runTest { + val (_, viewModel) = Arrangement() + .withDefaultAnswers() + .withConferenceCallingEnabledResponse() + .arrange() - @BeforeEach - fun setUp() { - MockKAnnotations.init(this) - val conversationId = ConversationId("some-dummy-value", "some.dummy.domain") - every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) - coEvery { observeEstablishedCalls.invoke() } returns emptyFlow() - coEvery { observeOngoingCalls.invoke() } returns emptyFlow() - coEvery { observeConversationDetails(any()) } returns flowOf() - coEvery { observeParticipantsForConversation(any()) } returns flowOf() - coEvery { setUserInformedAboutVerificationUseCase(any()) } returns Unit - coEvery { observeDegradedConversationNotifiedUseCase(any()) } returns flowOf(false) + val callingEnabled = viewModel.callingEnabled - conversationListCallViewModel = ConversationListCallViewModel( - savedStateHandle = savedStateHandle, - observeOngoingCalls = observeOngoingCalls, - observeEstablishedCalls = observeEstablishedCalls, - answerCall = joinCall, - endCall = endCall, - observeSyncState = observeSyncState, - isConferenceCallingEnabled = isConferenceCallingEnabled, - observeConversationDetails = observeConversationDetails, - observeParticipantsForConversation = observeParticipantsForConversation, - setUserInformedAboutVerification = setUserInformedAboutVerificationUseCase, - observeDegradedConversationNotified = observeDegradedConversationNotifiedUseCase - ) + callingEnabled.test { + assertEquals(Unit, awaitItem()) + } } @Test - fun `given join dialog displayed, when user dismiss it, then hide it`() { - conversationListCallViewModel.conversationCallViewState = conversationListCallViewModel.conversationCallViewState.copy( - shouldShowJoinAnywayDialog = true - ) + fun `given no calling enabled event, when we observe it, then there are no events propagated`() = runTest { + val (_, viewModel) = Arrangement() + .withDefaultAnswers() + .arrange() - conversationListCallViewModel.dismissJoinCallAnywayDialog() + val callingEnabled = viewModel.callingEnabled - assertEquals(false, conversationListCallViewModel.conversationCallViewState.shouldShowJoinAnywayDialog) + callingEnabled.test { + expectNoEvents() + } } - @Test - fun `given no ongoing call, when user tries to join a call, then invoke answerCall call use case`() { - conversationListCallViewModel.conversationCallViewState = - conversationListCallViewModel.conversationCallViewState.copy(hasEstablishedCall = false) + private class Arrangement { + @MockK + private lateinit var savedStateHandle: SavedStateHandle - coEvery { joinCall(conversationId = any()) } returns Unit + @MockK + private lateinit var observeOngoingCalls: ObserveOngoingCallsUseCase - conversationListCallViewModel.joinOngoingCall(onAnswered) + @MockK + private lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase - coVerify(exactly = 1) { joinCall(conversationId = any()) } - coVerify(exactly = 1) { onAnswered(any()) } - assertEquals(false, conversationListCallViewModel.conversationCallViewState.shouldShowJoinAnywayDialog) - } + @MockK + lateinit var joinCall: AnswerCallUseCase - @Test - fun `given an ongoing call, when user tries to join a call, then show JoinCallAnywayDialog`() { - conversationListCallViewModel.conversationCallViewState = - conversationListCallViewModel.conversationCallViewState.copy(hasEstablishedCall = true) + @MockK + lateinit var endCall: EndCallUseCase - conversationListCallViewModel.joinOngoingCall(onAnswered) + @MockK + private lateinit var observeSyncState: ObserveSyncStateUseCase - assertEquals(true, conversationListCallViewModel.conversationCallViewState.shouldShowJoinAnywayDialog) - coVerify(inverse = true) { joinCall(conversationId = any()) } - } + @MockK + private lateinit var isConferenceCallingEnabled: IsEligibleToStartCallUseCase - @Test - fun `given an ongoing call, when user confirms dialog to join a call, then end current call and join the newer one`() { - conversationListCallViewModel.conversationCallViewState = - conversationListCallViewModel.conversationCallViewState.copy(hasEstablishedCall = true) - conversationListCallViewModel.establishedCallConversationId = ConversationId("value", "Domain") - coEvery { endCall(any()) } returns Unit + @MockK(relaxed = true) + lateinit var onAnswered: (conversationId: ConversationId) -> Unit + + @MockK + private lateinit var observeConversationDetails: ObserveConversationDetailsUseCase + + @MockK + private lateinit var observeParticipantsForConversation: ObserveParticipantsForConversationUseCase + + @MockK + lateinit var setUserInformedAboutVerificationUseCase: SetUserInformedAboutVerificationUseCase + + @MockK + lateinit var observeDegradedConversationNotifiedUseCase: ObserveDegradedConversationNotifiedUseCase + + @MockK + lateinit var getSelfUserUseCase: GetSelfUserUseCase + + @MockK + lateinit var observeConferenceCallingEnabled: ObserveConferenceCallingEnabledUseCase + + init { + MockKAnnotations.init(this) + } + + suspend fun withDefaultAnswers() = apply { + val conversationId = ConversationId("some-dummy-value", "some.dummy.domain") + every { savedStateHandle.navArgs() } returns ConversationNavArgs(conversationId = conversationId) + coEvery { observeEstablishedCalls.invoke() } returns emptyFlow() + coEvery { observeOngoingCalls.invoke() } returns emptyFlow() + coEvery { observeConversationDetails(any()) } returns flowOf() + coEvery { observeParticipantsForConversation(any()) } returns flowOf() + coEvery { setUserInformedAboutVerificationUseCase(any()) } returns Unit + coEvery { observeDegradedConversationNotifiedUseCase(any()) } returns flowOf(false) + coEvery { getSelfUserUseCase() } returns flowOf() + coEvery { observeConferenceCallingEnabled() } returns flowOf() + } + + suspend fun withSelfAsAdmin() = apply { + coEvery { getSelfUserUseCase.invoke() } returns flowOf(TestUser.SELF_USER.copy(userType = UserType.ADMIN)) + } - conversationListCallViewModel.joinAnyway(onAnswered) + suspend fun withConferenceCallingEnabledResponse() = apply { + coEvery { observeConferenceCallingEnabled() } returns flowOf(Unit) + } - coVerify(exactly = 1) { endCall(any()) } + suspend fun withJoinCallResponse() = apply { + coEvery { joinCall(conversationId = any()) } returns Unit + } + + suspend fun withEndCallResponse() = apply { + coEvery { endCall(any()) } returns Unit + } + + fun arrange(): Pair = this to ConversationListCallViewModel( + savedStateHandle = savedStateHandle, + observeOngoingCalls = observeOngoingCalls, + observeEstablishedCalls = observeEstablishedCalls, + answerCall = joinCall, + endCall = endCall, + observeSyncState = observeSyncState, + isConferenceCallingEnabled = isConferenceCallingEnabled, + observeConversationDetails = observeConversationDetails, + observeParticipantsForConversation = observeParticipantsForConversation, + setUserInformedAboutVerification = setUserInformedAboutVerificationUseCase, + observeDegradedConversationNotified = observeDegradedConversationNotifiedUseCase, + observeConferenceCallingEnabled = observeConferenceCallingEnabled, + getSelf = getSelfUserUseCase + ) } }