Skip to content

Commit

Permalink
Update Contacts and Contacts group creation logic
Browse files Browse the repository at this point in the history
MAIlANDR-2147
  • Loading branch information
nick0602 committed Sep 26, 2024
1 parent f60f5b6 commit 141b147
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -387,12 +387,6 @@ internal fun NavGraphBuilder.addContacts(
exitWithErrorMessage = { message ->
navController.navigateBack()
showErrorSnackbar(message)
},
showNormSnackbar = { message ->
showNormalSnackbar(message)
},
showErrorSnackbar = { message ->
showErrorSnackbar(message)
}
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
import ch.protonmail.android.mailcommon.presentation.ConsumableTextEffect
import ch.protonmail.android.mailcommon.presentation.NO_CONTENT_DESCRIPTION
import ch.protonmail.android.mailcommon.presentation.compose.MailDimens
import ch.protonmail.android.uicomponents.dismissKeyboard
import ch.protonmail.android.mailcommon.presentation.ui.CommonTestTags
import ch.protonmail.android.mailcontact.presentation.R
import ch.protonmail.android.mailcontact.presentation.model.ContactGroupFormMember
Expand All @@ -72,14 +72,14 @@ import ch.protonmail.android.mailcontact.presentation.ui.DeleteContactGroupDialo
import ch.protonmail.android.mailcontact.presentation.ui.FormInputField
import ch.protonmail.android.mailcontact.presentation.ui.IconContactAvatar
import ch.protonmail.android.maillabel.presentation.ui.FormDeleteButton
import ch.protonmail.android.uicomponents.dismissKeyboard
import ch.protonmail.android.uicomponents.snackbar.DismissableSnackbarHost
import me.proton.core.compose.component.ProtonCenteredProgress
import me.proton.core.compose.component.ProtonSecondaryButton
import me.proton.core.compose.component.ProtonSnackbarHostState
import me.proton.core.compose.component.ProtonSnackbarType
import me.proton.core.compose.component.ProtonTextButton
import me.proton.core.compose.component.appbar.ProtonTopAppBar
import me.proton.core.compose.flow.rememberAsState
import me.proton.core.compose.theme.ProtonDimens
import me.proton.core.compose.theme.ProtonTheme
import me.proton.core.compose.theme.defaultNorm
Expand All @@ -97,8 +97,8 @@ fun ContactGroupFormScreen(
val context = LocalContext.current
val view = LocalView.current
val keyboardController = LocalSoftwareKeyboardController.current
val snackbarHostErrorState = ProtonSnackbarHostState(defaultType = ProtonSnackbarType.ERROR)
val state = rememberAsState(flow = viewModel.state, initial = ContactGroupFormViewModel.initialState).value
val snackbarHostErrorState = remember { ProtonSnackbarHostState(defaultType = ProtonSnackbarType.ERROR) }
val state = viewModel.state.collectAsStateWithLifecycle().value
val selectionSubmitted = remember { mutableStateOf(false) }

if (!selectionSubmitted.value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,5 @@ internal sealed interface ContactListEvent : ContactListOperation {
data object OpenBottomSheet : ContactListEvent
data object OpenContactSearch : ContactListEvent
data object DismissBottomSheet : ContactListEvent
data object UpsellingInProgress : ContactListEvent
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class ContactListReducer @Inject constructor() {
is ContactListEvent.OpenContactSearch -> reduceOpenContactSearch(currentState)
is ContactListEvent.SubscriptionUpgradeRequiredError -> reduceErrorSubscriptionUpgradeRequired(currentState)
is ContactListEvent.OpenUpsellingBottomSheet -> reduceOpenUpsellingBottomSheet(currentState)
is ContactListEvent.UpsellingInProgress -> reduceUpsellingInProgress(currentState)
}
}

Expand Down Expand Up @@ -197,4 +198,18 @@ class ContactListReducer @Inject constructor() {
)
}
}

private fun reduceUpsellingInProgress(currentState: ContactListState): ContactListState {
val upsellingInProgressEffect = Effect.of(TextUiModel(R.string.upselling_snackbar_upgrade_in_progress))
return when (currentState) {
is ContactListState.Loading -> currentState
is ContactListState.Loaded.Data -> currentState.copy(
upsellingInProgress = upsellingInProgressEffect
)

is ContactListState.Loaded.Empty -> currentState.copy(
upsellingInProgress = upsellingInProgressEffect
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ sealed interface ContactListState {
val openImportContact: Effect<Unit>
val openContactSearch: Effect<Boolean>
val subscriptionError: Effect<TextUiModel>
val upsellingInProgress: Effect<TextUiModel>
val bottomSheetType: BottomSheetType

data class Data(
Expand All @@ -54,6 +55,7 @@ sealed interface ContactListState {
override val openImportContact: Effect<Unit> = Effect.empty(),
override val openContactSearch: Effect<Boolean> = Effect.empty(),
override val subscriptionError: Effect<TextUiModel> = Effect.empty(),
override val upsellingInProgress: Effect<TextUiModel> = Effect.empty(),
override val isContactGroupsCrudEnabled: Boolean = false,
override val isContactGroupsUpsellingVisible: Boolean = false,
override val isContactSearchEnabled: Boolean = false,
Expand All @@ -69,6 +71,7 @@ sealed interface ContactListState {
override val openImportContact: Effect<Unit> = Effect.empty(),
override val openContactSearch: Effect<Boolean> = Effect.empty(),
override val subscriptionError: Effect<TextUiModel> = Effect.empty(),
override val upsellingInProgress: Effect<TextUiModel> = Effect.empty(),
override val isContactGroupsCrudEnabled: Boolean = false,
override val isContactGroupsUpsellingVisible: Boolean = false,
override val isContactSearchEnabled: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import ch.protonmail.android.mailcontact.domain.usecase.featureflags.IsContactSe
import ch.protonmail.android.mailcontact.presentation.model.ContactGroupItemUiModelMapper
import ch.protonmail.android.mailcontact.presentation.model.ContactListItemUiModelMapper
import ch.protonmail.android.mailupselling.domain.model.UpsellingEntryPoint
import ch.protonmail.android.mailupselling.domain.model.UserUpgradeState
import ch.protonmail.android.mailupselling.presentation.usecase.ObserveUpsellingVisibility
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -57,6 +58,7 @@ class ContactListViewModel @Inject constructor(
private val contactGroupItemUiModelMapper: ContactGroupItemUiModelMapper,
private val isContactGroupsCrudEnabled: IsContactGroupsCrudEnabled,
private val observeUpsellingVisibility: ObserveUpsellingVisibility,
private val userUpgradeState: UserUpgradeState,
private val isContactSearchEnabled: IsContactSearchEnabled,
observePrimaryUserId: ObservePrimaryUserId
) : ViewModel() {
Expand Down Expand Up @@ -88,6 +90,8 @@ class ContactListViewModel @Inject constructor(
}

private suspend fun handleOnNewContactGroupClick() {
if (userUpgradeState.isUserPendingUpgrade) return emitNewStateFor(ContactListEvent.UpsellingInProgress)

val shouldShowUpselling = observeUpsellingVisibility(UpsellingEntryPoint.BottomSheet.ContactGroups).first()

if (shouldShowUpselling) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
import ch.protonmail.android.mailcommon.presentation.ConsumableTextEffect
import ch.protonmail.android.mailcommon.presentation.ui.CommonTestTags
import ch.protonmail.android.mailcontact.presentation.R
import ch.protonmail.android.mailcontact.presentation.contactlist.ContactListState
import ch.protonmail.android.mailcontact.presentation.contactlist.ContactListViewAction
Expand All @@ -28,8 +30,12 @@ import ch.protonmail.android.mailcontact.presentation.utils.ContactFeatureFlags.
import ch.protonmail.android.mailupselling.presentation.model.BottomSheetVisibilityEffect
import ch.protonmail.android.mailupselling.presentation.ui.bottomsheet.UpsellingBottomSheet
import ch.protonmail.android.uicomponents.bottomsheet.bottomSheetHeightConstrainedContent
import ch.protonmail.android.uicomponents.snackbar.DismissableSnackbarHost
import kotlinx.coroutines.launch
import me.proton.core.compose.component.ProtonCenteredProgress
import me.proton.core.compose.component.ProtonModalBottomSheetLayout
import me.proton.core.compose.component.ProtonSnackbarHostState
import me.proton.core.compose.component.ProtonSnackbarType
import me.proton.core.contact.domain.entity.ContactId
import me.proton.core.label.domain.entity.LabelId

Expand All @@ -41,13 +47,25 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
skipHalfExpanded = true
)
val scope = rememberCoroutineScope()
val snackbarHostState = remember { ProtonSnackbarHostState() }

val state = viewModel.state.collectAsStateWithLifecycle().value
var showBottomSheet by remember { mutableStateOf(false) }

val actions = listActions.copy(
onNewGroupClick = { viewModel.submit(ContactListViewAction.OnNewContactGroupClick) }
)

val bottomSheetActions = UpsellingBottomSheet.Actions.Empty.copy(
onDismiss = { viewModel.submit(ContactListViewAction.OnDismissBottomSheet) },
onUpgrade = { message ->
scope.launch { snackbarHostState.showSnackbar(ProtonSnackbarType.NORM, message) }
},
onError = { message ->
scope.launch { snackbarHostState.showSnackbar(ProtonSnackbarType.ERROR, message) }
}
)

BackHandler(bottomSheetState.isVisible) {
viewModel.submit(ContactListViewAction.OnDismissBottomSheet)
}
Expand Down Expand Up @@ -76,13 +94,7 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
}

ContactListState.BottomSheetType.Upselling -> {
ContactGroupsUpsellingBottomSheet(
actions = UpsellingBottomSheet.Actions.Empty.copy(
onDismiss = { viewModel.submit(ContactListViewAction.OnDismissBottomSheet) },
onUpgrade = { message -> actions.showNormSnackbar(message) },
onError = { message -> actions.showErrorSnackbar(message) }
)
)
ContactGroupsUpsellingBottomSheet(actions = bottomSheetActions)
}
}
}
Expand Down Expand Up @@ -133,6 +145,10 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
}
}
}
ConsumableTextEffect(effect = state.upsellingInProgress) { message ->
snackbarHostState.snackbarHostState.currentSnackbarData?.dismiss()
snackbarHostState.showSnackbar(ProtonSnackbarType.NORM, message)
}
}

when (state) {
Expand Down Expand Up @@ -172,6 +188,12 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
}
}
}
},
snackbarHost = {
DismissableSnackbarHost(
modifier = Modifier.testTag(CommonTestTags.SnackbarHost),
protonSnackbarHostState = snackbarHostState
)
}
)
}
Expand All @@ -189,9 +211,7 @@ object ContactListScreen {
val onNewGroupClick: () -> Unit,
val openImportContact: () -> Unit,
val onSubscriptionUpgradeRequired: (String) -> Unit,
val exitWithErrorMessage: (String) -> Unit,
val showNormSnackbar: (String) -> Unit,
val showErrorSnackbar: (String) -> Unit
val exitWithErrorMessage: (String) -> Unit
) {

companion object {
Expand All @@ -206,9 +226,7 @@ object ContactListScreen {
openImportContact = {},
onNewGroupClick = {},
onSubscriptionUpgradeRequired = {},
exitWithErrorMessage = {},
showNormSnackbar = {},
showErrorSnackbar = {}
exitWithErrorMessage = {}
)

fun fromContactSearchActions(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ import ch.protonmail.android.mailcontact.presentation.R
import ch.protonmail.android.mailcontact.presentation.model.ContactGroupItemUiModelMapper
import ch.protonmail.android.mailcontact.presentation.model.ContactListItemUiModelMapper
import ch.protonmail.android.maillabel.presentation.getHexStringFromColor
import ch.protonmail.android.mailupselling.domain.model.UserUpgradeState
import ch.protonmail.android.mailupselling.presentation.model.BottomSheetVisibilityEffect
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsUpsellingContactGroupsEnabled
import ch.protonmail.android.mailupselling.presentation.usecase.ObserveUpsellingVisibility
import ch.protonmail.android.test.utils.rule.MainDispatcherRule
import ch.protonmail.android.testdata.user.UserIdTestData
Expand Down Expand Up @@ -112,8 +112,9 @@ class ContactListViewModelTest {
private val observeUpsellingVisibilityMock = mockk<ObserveUpsellingVisibility> {
every { this@mockk(any()) } returns flowOf(false)
}
private val isUpsellingContactGroupsEnabledMock = mockk<IsUpsellingContactGroupsEnabled> {
every { this@mockk() } returns true

private val userUpgradeState = mockk<UserUpgradeState> {
every { this@mockk.isUserPendingUpgrade } returns false
}

private val reducer = ContactListReducer()
Expand All @@ -134,6 +135,7 @@ class ContactListViewModelTest {
contactGroupItemUiModelMapper,
isContactGroupsCrudEnabledMock,
observeUpsellingVisibilityMock,
userUpgradeState,
isContactSearchEnabledMock,
observePrimaryUserId
)
Expand Down Expand Up @@ -303,6 +305,38 @@ class ContactListViewModelTest {
}
}

@Test
fun `when user subscription upgrade is pending, emit the upselling in progress event`() = runTest {
// Given
expectContactsData()
coEvery { observeUpsellingVisibilityMock(any()) } returns flowOf(false)
every { userUpgradeState.isUserPendingUpgrade } returns true

// When
contactListViewModel.state.test {
// Then
skipItems(1)

contactListViewModel.submit(ContactListViewAction.OnNewContactGroupClick)

val actual = awaitItem()
val expected = ContactListState.Loaded.Data(
contacts = contactListItemUiModelMapper.toContactListItemUiModel(
listOf(defaultTestContact)
),
contactGroups = contactGroupItemUiModelMapper.toContactGroupItemUiModel(
listOf(defaultTestContact), listOf(defaultTestContactGroupLabel)
),
isContactGroupsCrudEnabled = true,
isContactGroupsUpsellingVisible = false,
isContactSearchEnabled = true,
upsellingInProgress = Effect.of(TextUiModel(R.string.upselling_snackbar_upgrade_in_progress))
)

assertEquals(expected, actual)
}
}

@Test
fun `given contact list, when action open bottom sheet, then emits open state`() = runTest {
// Given
Expand Down

0 comments on commit 141b147

Please sign in to comment.