Skip to content

Commit 141b147

Browse files
committed
Update Contacts and Contacts group creation logic
MAIlANDR-2147
1 parent f60f5b6 commit 141b147

File tree

8 files changed

+95
-26
lines changed

8 files changed

+95
-26
lines changed

app/src/main/kotlin/ch/protonmail/android/navigation/route/HomeRoutes.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,6 @@ internal fun NavGraphBuilder.addContacts(
387387
exitWithErrorMessage = { message ->
388388
navController.navigateBack()
389389
showErrorSnackbar(message)
390-
},
391-
showNormSnackbar = { message ->
392-
showNormalSnackbar(message)
393-
},
394-
showErrorSnackbar = { message ->
395-
showErrorSnackbar(message)
396390
}
397391
)
398392
)

mail-contact/presentation/src/main/kotlin/ch/protonmail/android/mailcontact/presentation/contactgroupform/ContactGroupFormScreen.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ import androidx.compose.ui.res.stringResource
5858
import androidx.compose.ui.text.style.TextAlign
5959
import androidx.compose.ui.tooling.preview.Preview
6060
import androidx.hilt.navigation.compose.hiltViewModel
61+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
6162
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
6263
import ch.protonmail.android.mailcommon.presentation.ConsumableTextEffect
6364
import ch.protonmail.android.mailcommon.presentation.NO_CONTENT_DESCRIPTION
6465
import ch.protonmail.android.mailcommon.presentation.compose.MailDimens
65-
import ch.protonmail.android.uicomponents.dismissKeyboard
6666
import ch.protonmail.android.mailcommon.presentation.ui.CommonTestTags
6767
import ch.protonmail.android.mailcontact.presentation.R
6868
import ch.protonmail.android.mailcontact.presentation.model.ContactGroupFormMember
@@ -72,14 +72,14 @@ import ch.protonmail.android.mailcontact.presentation.ui.DeleteContactGroupDialo
7272
import ch.protonmail.android.mailcontact.presentation.ui.FormInputField
7373
import ch.protonmail.android.mailcontact.presentation.ui.IconContactAvatar
7474
import ch.protonmail.android.maillabel.presentation.ui.FormDeleteButton
75+
import ch.protonmail.android.uicomponents.dismissKeyboard
7576
import ch.protonmail.android.uicomponents.snackbar.DismissableSnackbarHost
7677
import me.proton.core.compose.component.ProtonCenteredProgress
7778
import me.proton.core.compose.component.ProtonSecondaryButton
7879
import me.proton.core.compose.component.ProtonSnackbarHostState
7980
import me.proton.core.compose.component.ProtonSnackbarType
8081
import me.proton.core.compose.component.ProtonTextButton
8182
import me.proton.core.compose.component.appbar.ProtonTopAppBar
82-
import me.proton.core.compose.flow.rememberAsState
8383
import me.proton.core.compose.theme.ProtonDimens
8484
import me.proton.core.compose.theme.ProtonTheme
8585
import me.proton.core.compose.theme.defaultNorm
@@ -97,8 +97,8 @@ fun ContactGroupFormScreen(
9797
val context = LocalContext.current
9898
val view = LocalView.current
9999
val keyboardController = LocalSoftwareKeyboardController.current
100-
val snackbarHostErrorState = ProtonSnackbarHostState(defaultType = ProtonSnackbarType.ERROR)
101-
val state = rememberAsState(flow = viewModel.state, initial = ContactGroupFormViewModel.initialState).value
100+
val snackbarHostErrorState = remember { ProtonSnackbarHostState(defaultType = ProtonSnackbarType.ERROR) }
101+
val state = viewModel.state.collectAsStateWithLifecycle().value
102102
val selectionSubmitted = remember { mutableStateOf(false) }
103103

104104
if (!selectionSubmitted.value) {

mail-contact/presentation/src/main/kotlin/ch/protonmail/android/mailcontact/presentation/contactlist/ContactListOperation.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ internal sealed interface ContactListEvent : ContactListOperation {
4949
data object OpenBottomSheet : ContactListEvent
5050
data object OpenContactSearch : ContactListEvent
5151
data object DismissBottomSheet : ContactListEvent
52+
data object UpsellingInProgress : ContactListEvent
5253
}

mail-contact/presentation/src/main/kotlin/ch/protonmail/android/mailcontact/presentation/contactlist/ContactListReducer.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class ContactListReducer @Inject constructor() {
3838
is ContactListEvent.OpenContactSearch -> reduceOpenContactSearch(currentState)
3939
is ContactListEvent.SubscriptionUpgradeRequiredError -> reduceErrorSubscriptionUpgradeRequired(currentState)
4040
is ContactListEvent.OpenUpsellingBottomSheet -> reduceOpenUpsellingBottomSheet(currentState)
41+
is ContactListEvent.UpsellingInProgress -> reduceUpsellingInProgress(currentState)
4142
}
4243
}
4344

@@ -197,4 +198,18 @@ class ContactListReducer @Inject constructor() {
197198
)
198199
}
199200
}
201+
202+
private fun reduceUpsellingInProgress(currentState: ContactListState): ContactListState {
203+
val upsellingInProgressEffect = Effect.of(TextUiModel(R.string.upselling_snackbar_upgrade_in_progress))
204+
return when (currentState) {
205+
is ContactListState.Loading -> currentState
206+
is ContactListState.Loaded.Data -> currentState.copy(
207+
upsellingInProgress = upsellingInProgressEffect
208+
)
209+
210+
is ContactListState.Loaded.Empty -> currentState.copy(
211+
upsellingInProgress = upsellingInProgressEffect
212+
)
213+
}
214+
}
200215
}

mail-contact/presentation/src/main/kotlin/ch/protonmail/android/mailcontact/presentation/contactlist/ContactListState.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ sealed interface ContactListState {
4545
val openImportContact: Effect<Unit>
4646
val openContactSearch: Effect<Boolean>
4747
val subscriptionError: Effect<TextUiModel>
48+
val upsellingInProgress: Effect<TextUiModel>
4849
val bottomSheetType: BottomSheetType
4950

5051
data class Data(
@@ -54,6 +55,7 @@ sealed interface ContactListState {
5455
override val openImportContact: Effect<Unit> = Effect.empty(),
5556
override val openContactSearch: Effect<Boolean> = Effect.empty(),
5657
override val subscriptionError: Effect<TextUiModel> = Effect.empty(),
58+
override val upsellingInProgress: Effect<TextUiModel> = Effect.empty(),
5759
override val isContactGroupsCrudEnabled: Boolean = false,
5860
override val isContactGroupsUpsellingVisible: Boolean = false,
5961
override val isContactSearchEnabled: Boolean = false,
@@ -69,6 +71,7 @@ sealed interface ContactListState {
6971
override val openImportContact: Effect<Unit> = Effect.empty(),
7072
override val openContactSearch: Effect<Boolean> = Effect.empty(),
7173
override val subscriptionError: Effect<TextUiModel> = Effect.empty(),
74+
override val upsellingInProgress: Effect<TextUiModel> = Effect.empty(),
7275
override val isContactGroupsCrudEnabled: Boolean = false,
7376
override val isContactGroupsUpsellingVisible: Boolean = false,
7477
override val isContactSearchEnabled: Boolean = false,

mail-contact/presentation/src/main/kotlin/ch/protonmail/android/mailcontact/presentation/contactlist/ContactListViewModel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import ch.protonmail.android.mailcontact.domain.usecase.featureflags.IsContactSe
3030
import ch.protonmail.android.mailcontact.presentation.model.ContactGroupItemUiModelMapper
3131
import ch.protonmail.android.mailcontact.presentation.model.ContactListItemUiModelMapper
3232
import ch.protonmail.android.mailupselling.domain.model.UpsellingEntryPoint
33+
import ch.protonmail.android.mailupselling.domain.model.UserUpgradeState
3334
import ch.protonmail.android.mailupselling.presentation.usecase.ObserveUpsellingVisibility
3435
import dagger.hilt.android.lifecycle.HiltViewModel
3536
import kotlinx.coroutines.flow.Flow
@@ -57,6 +58,7 @@ class ContactListViewModel @Inject constructor(
5758
private val contactGroupItemUiModelMapper: ContactGroupItemUiModelMapper,
5859
private val isContactGroupsCrudEnabled: IsContactGroupsCrudEnabled,
5960
private val observeUpsellingVisibility: ObserveUpsellingVisibility,
61+
private val userUpgradeState: UserUpgradeState,
6062
private val isContactSearchEnabled: IsContactSearchEnabled,
6163
observePrimaryUserId: ObservePrimaryUserId
6264
) : ViewModel() {
@@ -88,6 +90,8 @@ class ContactListViewModel @Inject constructor(
8890
}
8991

9092
private suspend fun handleOnNewContactGroupClick() {
93+
if (userUpgradeState.isUserPendingUpgrade) return emitNewStateFor(ContactListEvent.UpsellingInProgress)
94+
9195
val shouldShowUpselling = observeUpsellingVisibility(UpsellingEntryPoint.BottomSheet.ContactGroups).first()
9296

9397
if (shouldShowUpselling) {

mail-contact/presentation/src/main/kotlin/ch/protonmail/android/mailcontact/presentation/contactlist/ui/ContactListScreen.kt

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import androidx.compose.runtime.remember
1414
import androidx.compose.runtime.rememberCoroutineScope
1515
import androidx.compose.runtime.setValue
1616
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.platform.testTag
1718
import androidx.compose.ui.res.stringResource
1819
import androidx.hilt.navigation.compose.hiltViewModel
1920
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2021
import ch.protonmail.android.mailcommon.presentation.ConsumableLaunchedEffect
2122
import ch.protonmail.android.mailcommon.presentation.ConsumableTextEffect
23+
import ch.protonmail.android.mailcommon.presentation.ui.CommonTestTags
2224
import ch.protonmail.android.mailcontact.presentation.R
2325
import ch.protonmail.android.mailcontact.presentation.contactlist.ContactListState
2426
import ch.protonmail.android.mailcontact.presentation.contactlist.ContactListViewAction
@@ -28,8 +30,12 @@ import ch.protonmail.android.mailcontact.presentation.utils.ContactFeatureFlags.
2830
import ch.protonmail.android.mailupselling.presentation.model.BottomSheetVisibilityEffect
2931
import ch.protonmail.android.mailupselling.presentation.ui.bottomsheet.UpsellingBottomSheet
3032
import ch.protonmail.android.uicomponents.bottomsheet.bottomSheetHeightConstrainedContent
33+
import ch.protonmail.android.uicomponents.snackbar.DismissableSnackbarHost
34+
import kotlinx.coroutines.launch
3135
import me.proton.core.compose.component.ProtonCenteredProgress
3236
import me.proton.core.compose.component.ProtonModalBottomSheetLayout
37+
import me.proton.core.compose.component.ProtonSnackbarHostState
38+
import me.proton.core.compose.component.ProtonSnackbarType
3339
import me.proton.core.contact.domain.entity.ContactId
3440
import me.proton.core.label.domain.entity.LabelId
3541

@@ -41,13 +47,25 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
4147
skipHalfExpanded = true
4248
)
4349
val scope = rememberCoroutineScope()
50+
val snackbarHostState = remember { ProtonSnackbarHostState() }
51+
4452
val state = viewModel.state.collectAsStateWithLifecycle().value
4553
var showBottomSheet by remember { mutableStateOf(false) }
4654

4755
val actions = listActions.copy(
4856
onNewGroupClick = { viewModel.submit(ContactListViewAction.OnNewContactGroupClick) }
4957
)
5058

59+
val bottomSheetActions = UpsellingBottomSheet.Actions.Empty.copy(
60+
onDismiss = { viewModel.submit(ContactListViewAction.OnDismissBottomSheet) },
61+
onUpgrade = { message ->
62+
scope.launch { snackbarHostState.showSnackbar(ProtonSnackbarType.NORM, message) }
63+
},
64+
onError = { message ->
65+
scope.launch { snackbarHostState.showSnackbar(ProtonSnackbarType.ERROR, message) }
66+
}
67+
)
68+
5169
BackHandler(bottomSheetState.isVisible) {
5270
viewModel.submit(ContactListViewAction.OnDismissBottomSheet)
5371
}
@@ -76,13 +94,7 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
7694
}
7795

7896
ContactListState.BottomSheetType.Upselling -> {
79-
ContactGroupsUpsellingBottomSheet(
80-
actions = UpsellingBottomSheet.Actions.Empty.copy(
81-
onDismiss = { viewModel.submit(ContactListViewAction.OnDismissBottomSheet) },
82-
onUpgrade = { message -> actions.showNormSnackbar(message) },
83-
onError = { message -> actions.showErrorSnackbar(message) }
84-
)
85-
)
97+
ContactGroupsUpsellingBottomSheet(actions = bottomSheetActions)
8698
}
8799
}
88100
}
@@ -133,6 +145,10 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
133145
}
134146
}
135147
}
148+
ConsumableTextEffect(effect = state.upsellingInProgress) { message ->
149+
snackbarHostState.snackbarHostState.currentSnackbarData?.dismiss()
150+
snackbarHostState.showSnackbar(ProtonSnackbarType.NORM, message)
151+
}
136152
}
137153

138154
when (state) {
@@ -172,6 +188,12 @@ fun ContactListScreen(listActions: ContactListScreen.Actions, viewModel: Contact
172188
}
173189
}
174190
}
191+
},
192+
snackbarHost = {
193+
DismissableSnackbarHost(
194+
modifier = Modifier.testTag(CommonTestTags.SnackbarHost),
195+
protonSnackbarHostState = snackbarHostState
196+
)
175197
}
176198
)
177199
}
@@ -189,9 +211,7 @@ object ContactListScreen {
189211
val onNewGroupClick: () -> Unit,
190212
val openImportContact: () -> Unit,
191213
val onSubscriptionUpgradeRequired: (String) -> Unit,
192-
val exitWithErrorMessage: (String) -> Unit,
193-
val showNormSnackbar: (String) -> Unit,
194-
val showErrorSnackbar: (String) -> Unit
214+
val exitWithErrorMessage: (String) -> Unit
195215
) {
196216

197217
companion object {
@@ -206,9 +226,7 @@ object ContactListScreen {
206226
openImportContact = {},
207227
onNewGroupClick = {},
208228
onSubscriptionUpgradeRequired = {},
209-
exitWithErrorMessage = {},
210-
showNormSnackbar = {},
211-
showErrorSnackbar = {}
229+
exitWithErrorMessage = {}
212230
)
213231

214232
fun fromContactSearchActions(

mail-contact/presentation/src/test/kotlin/ch/protonmail/android/mailcontact/presentation/contactlist/ContactListViewModelTest.kt

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ import ch.protonmail.android.mailcontact.presentation.R
3838
import ch.protonmail.android.mailcontact.presentation.model.ContactGroupItemUiModelMapper
3939
import ch.protonmail.android.mailcontact.presentation.model.ContactListItemUiModelMapper
4040
import ch.protonmail.android.maillabel.presentation.getHexStringFromColor
41+
import ch.protonmail.android.mailupselling.domain.model.UserUpgradeState
4142
import ch.protonmail.android.mailupselling.presentation.model.BottomSheetVisibilityEffect
42-
import ch.protonmail.android.mailupselling.domain.usecase.featureflags.IsUpsellingContactGroupsEnabled
4343
import ch.protonmail.android.mailupselling.presentation.usecase.ObserveUpsellingVisibility
4444
import ch.protonmail.android.test.utils.rule.MainDispatcherRule
4545
import ch.protonmail.android.testdata.user.UserIdTestData
@@ -112,8 +112,9 @@ class ContactListViewModelTest {
112112
private val observeUpsellingVisibilityMock = mockk<ObserveUpsellingVisibility> {
113113
every { this@mockk(any()) } returns flowOf(false)
114114
}
115-
private val isUpsellingContactGroupsEnabledMock = mockk<IsUpsellingContactGroupsEnabled> {
116-
every { this@mockk() } returns true
115+
116+
private val userUpgradeState = mockk<UserUpgradeState> {
117+
every { this@mockk.isUserPendingUpgrade } returns false
117118
}
118119

119120
private val reducer = ContactListReducer()
@@ -134,6 +135,7 @@ class ContactListViewModelTest {
134135
contactGroupItemUiModelMapper,
135136
isContactGroupsCrudEnabledMock,
136137
observeUpsellingVisibilityMock,
138+
userUpgradeState,
137139
isContactSearchEnabledMock,
138140
observePrimaryUserId
139141
)
@@ -303,6 +305,38 @@ class ContactListViewModelTest {
303305
}
304306
}
305307

308+
@Test
309+
fun `when user subscription upgrade is pending, emit the upselling in progress event`() = runTest {
310+
// Given
311+
expectContactsData()
312+
coEvery { observeUpsellingVisibilityMock(any()) } returns flowOf(false)
313+
every { userUpgradeState.isUserPendingUpgrade } returns true
314+
315+
// When
316+
contactListViewModel.state.test {
317+
// Then
318+
skipItems(1)
319+
320+
contactListViewModel.submit(ContactListViewAction.OnNewContactGroupClick)
321+
322+
val actual = awaitItem()
323+
val expected = ContactListState.Loaded.Data(
324+
contacts = contactListItemUiModelMapper.toContactListItemUiModel(
325+
listOf(defaultTestContact)
326+
),
327+
contactGroups = contactGroupItemUiModelMapper.toContactGroupItemUiModel(
328+
listOf(defaultTestContact), listOf(defaultTestContactGroupLabel)
329+
),
330+
isContactGroupsCrudEnabled = true,
331+
isContactGroupsUpsellingVisible = false,
332+
isContactSearchEnabled = true,
333+
upsellingInProgress = Effect.of(TextUiModel(R.string.upselling_snackbar_upgrade_in_progress))
334+
)
335+
336+
assertEquals(expected, actual)
337+
}
338+
}
339+
306340
@Test
307341
fun `given contact list, when action open bottom sheet, then emits open state`() = runTest {
308342
// Given

0 commit comments

Comments
 (0)