Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.

Commit 850fc3b

Browse files
authored
Merge pull request #80 from Automattic/adam/GRA-601
Store UserSharePreferences in local storage
2 parents 63b410f + 9a2f82e commit 850fc3b

File tree

16 files changed

+266
-110
lines changed

16 files changed

+266
-110
lines changed

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareEvent.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ package com.gravatar.app.homeUi.presentation.home.share
22

33
internal sealed class ShareEvent {
44
data class OnEmailValueChanged(val value: String) : ShareEvent()
5-
data class OnEmailSharingChanged(val isShared: Boolean) : ShareEvent()
65
data class OnPhoneValueChanged(val value: String) : ShareEvent()
7-
data class OnPhoneSharingChanged(val isShared: Boolean) : ShareEvent()
86
data class OnUserSharePreferencesChanged(val shareFieldType: ShareFieldType) : ShareEvent()
97
data object OnAboutAppClicked : ShareEvent()
108
data object OnDismissAboutAppDialog : ShareEvent()

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareScreen.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,15 @@ internal fun ShareScreen(uiState: ShareUiState, onEvent: (ShareEvent) -> Unit) {
6060
.fillMaxWidth(),
6161
)
6262
SharePrivateContactInfo(
63-
privateContactInfo = uiState.privateContactInfo,
63+
privateContactState = uiState.privateContactState,
6464
onEmailValueChange = { onEvent(ShareEvent.OnEmailValueChanged(it)) },
65-
onEmailSwitchCheckedChange = { onEvent(ShareEvent.OnEmailSharingChanged(it)) },
65+
onEmailSwitchCheckedChange = {
66+
onEvent(ShareEvent.OnUserSharePreferencesChanged(ShareFieldType.PrivateEmail(it)))
67+
},
6668
onPhoneValueChange = { onEvent(ShareEvent.OnPhoneValueChanged(it)) },
67-
onPhoneSwitchCheckedChange = { onEvent(ShareEvent.OnPhoneSharingChanged(it)) },
69+
onPhoneSwitchCheckedChange = {
70+
onEvent(ShareEvent.OnUserSharePreferencesChanged(ShareFieldType.PrivatePhone(it)))
71+
},
6872
modifier = Modifier.padding(16.dp),
6973
)
7074
ItemDivider()

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareUiState.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
package com.gravatar.app.homeUi.presentation.home.share
22

3+
import com.gravatar.app.usercomponent.domain.model.PrivateContactInfo
34
import com.gravatar.app.usercomponent.domain.model.UserSharePreferences
45
import com.gravatar.restapi.models.Profile
56

67
internal data class ShareUiState(
78
val profile: Profile? = null,
89
val avatarUrl: String? = null,
910
val isAboutAppDialogVisible: Boolean = false,
10-
val privateContactInfo: PrivateContactInfo = PrivateContactInfo(),
11-
val userSharePreferences: UserSharePreferences = UserSharePreferences(
12-
name = true,
13-
location = true,
14-
title = true,
15-
organization = true,
16-
description = true,
17-
profileUrl = true
18-
)
11+
val privateContactInfo: PrivateContactInfo = PrivateContactInfo.Default,
12+
val userSharePreferences: UserSharePreferences = UserSharePreferences.Default
1913
) {
14+
15+
val privateContactState = PrivateContactState(
16+
emailValue = privateContactInfo.privateEmail,
17+
isEmailShared = userSharePreferences.privateEmail,
18+
phoneValue = privateContactInfo.privatePhone,
19+
isPhoneShared = userSharePreferences.privatePhone,
20+
)
21+
2022
fun copyWithUserSharePreferences(
2123
shareFieldType: ShareFieldType,
2224
): ShareUiState = this.copy(
2325
userSharePreferences = userSharePreferences.copy(
26+
privateEmail = if (shareFieldType is ShareFieldType.PrivateEmail) shareFieldType.checked else userSharePreferences.privateEmail,
27+
privatePhone = if (shareFieldType is ShareFieldType.PrivatePhone) shareFieldType.checked else userSharePreferences.privatePhone,
2428
name = if (shareFieldType is ShareFieldType.Name) shareFieldType.checked else userSharePreferences.name,
2529
location = if (shareFieldType is ShareFieldType.Location) shareFieldType.checked else userSharePreferences.location,
2630
title = if (shareFieldType is ShareFieldType.Title) shareFieldType.checked else userSharePreferences.title,
@@ -31,7 +35,7 @@ internal data class ShareUiState(
3135
)
3236
}
3337

34-
internal data class PrivateContactInfo(
38+
internal data class PrivateContactState(
3539
val emailValue: String = "",
3640
val isEmailShared: Boolean = false,
3741
val phoneValue: String = "",
@@ -64,4 +68,12 @@ internal sealed class ShareFieldType {
6468
data class ProfileUrl(
6569
override val checked: Boolean
6670
) : ShareFieldType()
71+
72+
data class PrivateEmail(
73+
override val checked: Boolean
74+
) : ShareFieldType()
75+
76+
data class PrivatePhone(
77+
override val checked: Boolean
78+
) : ShareFieldType()
6779
}

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModel.kt

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
55
import com.gravatar.app.usercomponent.domain.repository.UserRepository
66
import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrl
7+
import com.gravatar.app.usercomponent.domain.usecase.GetUserSharePreferences
8+
import com.gravatar.app.usercomponent.domain.usecase.UpdateUserSharePreferences
79
import kotlinx.coroutines.flow.MutableStateFlow
810
import kotlinx.coroutines.flow.StateFlow
911
import kotlinx.coroutines.flow.asStateFlow
1012
import kotlinx.coroutines.flow.launchIn
1113
import kotlinx.coroutines.flow.onEach
1214
import kotlinx.coroutines.flow.update
15+
import kotlinx.coroutines.launch
1316

1417
internal class ShareViewModel(
1518
private val userRepository: UserRepository,
1619
private val getAvatarUrl: GetAvatarUrl,
20+
private val getUserSharePreferences: GetUserSharePreferences,
21+
private val updateUserSharePreferences: UpdateUserSharePreferences,
1722
) : ViewModel() {
1823

1924
private val _uiState = MutableStateFlow(ShareUiState())
@@ -22,6 +27,7 @@ internal class ShareViewModel(
2227
init {
2328
collectProfile()
2429
collectAvatarUrl()
30+
collectUserSharePreferences()
2531
}
2632

2733
fun onEvent(shareEvent: ShareEvent) {
@@ -30,17 +36,7 @@ internal class ShareViewModel(
3036
_uiState.update {
3137
it.copy(
3238
privateContactInfo = it.privateContactInfo.copy(
33-
emailValue = shareEvent.value
34-
)
35-
)
36-
}
37-
}
38-
39-
is ShareEvent.OnEmailSharingChanged -> {
40-
_uiState.update {
41-
it.copy(
42-
privateContactInfo = it.privateContactInfo.copy(
43-
isEmailShared = shareEvent.isShared
39+
privateEmail = shareEvent.value
4440
)
4541
)
4642
}
@@ -50,31 +46,26 @@ internal class ShareViewModel(
5046
_uiState.update {
5147
it.copy(
5248
privateContactInfo = it.privateContactInfo.copy(
53-
phoneValue = shareEvent.value
54-
)
55-
)
56-
}
57-
}
58-
59-
is ShareEvent.OnPhoneSharingChanged -> {
60-
_uiState.update {
61-
it.copy(
62-
privateContactInfo = it.privateContactInfo.copy(
63-
isPhoneShared = shareEvent.isShared
49+
privatePhone = shareEvent.value
6450
)
6551
)
6652
}
6753
}
6854

6955
is ShareEvent.OnAboutAppClicked -> showAboutAppDialog()
7056
is ShareEvent.OnDismissAboutAppDialog -> hideAboutAppDialog()
71-
is ShareEvent.OnUserSharePreferencesChanged -> updateUserSharePreferences(shareEvent.shareFieldType)
57+
is ShareEvent.OnUserSharePreferencesChanged -> handleUserSharePreferencesChange(shareEvent.shareFieldType)
7258
}
7359
}
7460

75-
private fun updateUserSharePreferences(shareFieldType: ShareFieldType) {
76-
_uiState.update { currentState ->
77-
currentState.copyWithUserSharePreferences(shareFieldType)
61+
private fun handleUserSharePreferencesChange(shareFieldType: ShareFieldType) {
62+
with(_uiState.value.copyWithUserSharePreferences(shareFieldType)) {
63+
// update the UI state with the new preferences
64+
_uiState.value = this
65+
// Save the updated preferences
66+
viewModelScope.launch {
67+
updateUserSharePreferences(this@with.userSharePreferences)
68+
}
7869
}
7970
}
8071

@@ -113,4 +104,16 @@ internal class ShareViewModel(
113104
}
114105
.launchIn(viewModelScope)
115106
}
107+
108+
private fun collectUserSharePreferences() {
109+
getUserSharePreferences()
110+
.onEach { preferences ->
111+
_uiState.update { currentState ->
112+
currentState.copy(
113+
userSharePreferences = preferences
114+
)
115+
}
116+
}
117+
.launchIn(viewModelScope)
118+
}
116119
}

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/components/SharePrivateContactInfo.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import androidx.compose.ui.text.input.KeyboardType
99
import androidx.compose.ui.tooling.preview.Preview
1010
import androidx.compose.ui.unit.dp
1111
import com.gravatar.app.homeUi.R
12-
import com.gravatar.app.homeUi.presentation.home.share.PrivateContactInfo
12+
import com.gravatar.app.homeUi.presentation.home.share.PrivateContactState
1313

1414
@Composable
1515
internal fun SharePrivateContactInfo(
16-
privateContactInfo: PrivateContactInfo,
16+
privateContactState: PrivateContactState,
1717
onEmailValueChange: (String) -> Unit,
1818
onEmailSwitchCheckedChange: (Boolean) -> Unit,
1919
onPhoneValueChange: (String) -> Unit,
@@ -29,17 +29,17 @@ internal fun SharePrivateContactInfo(
2929
)
3030
ShareEditableField(
3131
placeholder = R.string.share_tab_private_contact_email_placeholder,
32-
value = privateContactInfo.emailValue,
32+
value = privateContactState.emailValue,
3333
onValueChange = onEmailValueChange,
34-
switchChecked = privateContactInfo.isEmailShared,
34+
switchChecked = privateContactState.isEmailShared,
3535
keyboardType = KeyboardType.Email,
3636
onSwitchCheckedChange = onEmailSwitchCheckedChange,
3737
)
3838
ShareEditableField(
3939
placeholder = R.string.share_tab_private_contact_phone_number_placeholder,
40-
value = privateContactInfo.phoneValue,
40+
value = privateContactState.phoneValue,
4141
onValueChange = onPhoneValueChange,
42-
switchChecked = privateContactInfo.isPhoneShared,
42+
switchChecked = privateContactState.isPhoneShared,
4343
keyboardType = KeyboardType.Phone,
4444
onSwitchCheckedChange = onPhoneSwitchCheckedChange,
4545
)
@@ -50,7 +50,7 @@ internal fun SharePrivateContactInfo(
5050
@Composable
5151
private fun SharePrivateContactInfoPreview() {
5252
SharePrivateContactInfo(
53-
privateContactInfo = PrivateContactInfo(
53+
privateContactState = PrivateContactState(
5454
emailValue = "example@email.com",
5555
isEmailShared = true,
5656
phoneValue = "123-456-7890",

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/components/SharePublicContactInfo.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,7 @@ private fun SharePublicContactInfoPreview() {
111111
GravatarAppTheme {
112112
SharePublicContactInfo(
113113
profile = defaultProfile(hash = "hash"),
114-
userSharePreferences = UserSharePreferences(
115-
name = true,
116-
location = true,
117-
title = true,
118-
organization = true,
119-
description = true,
120-
profileUrl = true
121-
),
114+
userSharePreferences = UserSharePreferences.Default,
122115
onUserPreferenceChanged = {},
123116
)
124117
}

homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModelTest.kt

Lines changed: 54 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package com.gravatar.app.homeUi.presentation.home.share
22

33
import app.cash.turbine.test
44
import com.gravatar.app.testUtils.CoroutineTestRule
5+
import com.gravatar.app.usercomponent.domain.model.UserSharePreferences
56
import com.gravatar.app.usercomponent.domain.repository.UserRepository
67
import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrl
8+
import com.gravatar.app.usercomponent.domain.usecase.GetUserSharePreferences
9+
import com.gravatar.app.usercomponent.domain.usecase.UpdateUserSharePreferences
710
import com.gravatar.restapi.models.Profile
811
import com.gravatar.restapi.models.ProfileContactInfo
912
import io.mockk.every
@@ -32,17 +35,26 @@ class ShareViewModelTest {
3235
private val getAvatarUrl: GetAvatarUrl = object : GetAvatarUrl {
3336
override fun invoke() = avatarUrlFlow
3437
}
38+
private val getUserSharePreferences: GetUserSharePreferences = object : GetUserSharePreferences {
39+
override fun invoke() = userSharePreferencesFlow
40+
}
41+
private val updateUserSharePreferences = object : UpdateUserSharePreferences {
42+
override suspend fun invoke(userSharePreferences: UserSharePreferences) {
43+
userSharePreferencesFlow.emit(userSharePreferences)
44+
}
45+
}
3546
private val userRepository = mockk<UserRepository>()
3647

3748
private lateinit var viewModel: ShareViewModel
3849

3950
private val avatarUrlFlow: MutableSharedFlow<URL?> = MutableSharedFlow()
4051
private val profileFlow: MutableSharedFlow<Profile?> = MutableSharedFlow()
52+
private val userSharePreferencesFlow: MutableSharedFlow<UserSharePreferences> = MutableSharedFlow()
4153

4254
@Before
4355
fun setup() {
4456
every { userRepository.getProfile() } returns profileFlow
45-
viewModel = ShareViewModel(userRepository, getAvatarUrl)
57+
viewModel = ShareViewModel(userRepository, getAvatarUrl, getUserSharePreferences, updateUserSharePreferences)
4658
}
4759

4860
@Test
@@ -79,21 +91,7 @@ class ShareViewModelTest {
7991

8092
// Then
8193
viewModel.uiState.test {
82-
assertEquals(newEmailValue, awaitItem().privateContactInfo.emailValue)
83-
}
84-
}
85-
86-
@Test
87-
fun `when OnEmailSharingChanged event is triggered then isEmailShared is updated`() = runTest {
88-
// Given
89-
val isShared = true
90-
91-
// When
92-
viewModel.onEvent(ShareEvent.OnEmailSharingChanged(isShared))
93-
94-
// Then
95-
viewModel.uiState.test {
96-
assertEquals(isShared, awaitItem().privateContactInfo.isEmailShared)
94+
assertEquals(newEmailValue, awaitItem().privateContactState.emailValue)
9795
}
9896
}
9997

@@ -107,21 +105,7 @@ class ShareViewModelTest {
107105

108106
// Then
109107
viewModel.uiState.test {
110-
assertEquals(newPhoneValue, awaitItem().privateContactInfo.phoneValue)
111-
}
112-
}
113-
114-
@Test
115-
fun `when OnPhoneSharingChanged event is triggered then isPhoneShared is updated`() = runTest {
116-
// Given
117-
val isShared = true
118-
119-
// When
120-
viewModel.onEvent(ShareEvent.OnPhoneSharingChanged(isShared))
121-
122-
// Then
123-
viewModel.uiState.test {
124-
assertEquals(isShared, awaitItem().privateContactInfo.isPhoneShared)
108+
assertEquals(newPhoneValue, awaitItem().privateContactState.phoneValue)
125109
}
126110
}
127111

@@ -298,6 +282,45 @@ class ShareViewModelTest {
298282
}
299283
}
300284

285+
@Test
286+
fun `when user share preferences are emitted then uiState is updated with preferences`() = runTest {
287+
// Given
288+
val testPreferences = UserSharePreferences.Default.copy(
289+
name = false,
290+
title = false,
291+
description = false,
292+
)
293+
294+
// When
295+
userSharePreferencesFlow.emit(testPreferences)
296+
advanceUntilIdle()
297+
298+
// Then
299+
viewModel.uiState.test {
300+
val state = awaitItem()
301+
assertEquals(testPreferences, state.userSharePreferences)
302+
}
303+
}
304+
305+
@Test
306+
fun `when user share preferences are changed then updateUserSharePreferences is called`() = runTest {
307+
// Given
308+
val initialState = viewModel.uiState.value
309+
val newNamePreference = !initialState.userSharePreferences.name
310+
val shareFieldType = ShareFieldType.Name(checked = newNamePreference)
311+
312+
userSharePreferencesFlow.test {
313+
// When
314+
viewModel.onEvent(ShareEvent.OnUserSharePreferencesChanged(shareFieldType))
315+
316+
// Then
317+
val expectedSharePreferences = initialState.userSharePreferences.copy(
318+
name = newNamePreference,
319+
)
320+
assertEquals(expectedSharePreferences, awaitItem())
321+
}
322+
}
323+
301324
private fun createTestProfile() = Profile {
302325
hash = "test-hash"
303326
displayName = "Test User"

0 commit comments

Comments
 (0)