diff --git a/crashlogging/src/main/java/com/gravatar/crashlogging/GravatarCrashLoggingDataProvider.kt b/crashlogging/src/main/java/com/gravatar/crashlogging/GravatarCrashLoggingDataProvider.kt index 8fbf2f91..49c2de39 100644 --- a/crashlogging/src/main/java/com/gravatar/crashlogging/GravatarCrashLoggingDataProvider.kt +++ b/crashlogging/src/main/java/com/gravatar/crashlogging/GravatarCrashLoggingDataProvider.kt @@ -7,12 +7,16 @@ import com.automattic.android.tracks.crashlogging.ExtraKnownKey import com.automattic.android.tracks.crashlogging.PerformanceMonitoringConfig import com.automattic.android.tracks.crashlogging.ReleaseName import com.gravatar.app.usercomponent.domain.repository.UserRepository +import com.gravatar.app.usercomponent.domain.usecase.GetPrivacySettings import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking internal class GravatarCrashLoggingDataProvider( localeProvider: LocaleProvider, - userRepository: UserRepository + userRepository: UserRepository, + private val getPrivacySettings: GetPrivacySettings, ) : CrashLoggingDataProvider { override val applicationContextProvider = emptyFlow>() @@ -41,7 +45,7 @@ internal class GravatarCrashLoggingDataProvider( ) } - override fun crashLoggingEnabled() = false + override fun crashLoggingEnabled() = runBlocking { getPrivacySettings().firstOrNull()?.crashReportingEnabled == true } override fun extraKnownKeys() = emptyList() diff --git a/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheetTest.privacySettingsAllDisabled.png b/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheetTest.privacySettingsAllDisabled.png new file mode 100644 index 00000000..b6512d33 Binary files /dev/null and b/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheetTest.privacySettingsAllDisabled.png differ diff --git a/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheetTest.privacySettingsAllEnabled.png b/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheetTest.privacySettingsAllEnabled.png new file mode 100644 index 00000000..8d83c172 Binary files /dev/null and b/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheetTest.privacySettingsAllEnabled.png differ diff --git a/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.topbar.components.AboutAppDialogTest.aboutAppDialogVisible.png b/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.topbar.components.AboutAppDialogTest.aboutAppDialogVisible.png index e7bf2bcd..8dae241f 100644 Binary files a/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.topbar.components.AboutAppDialogTest.aboutAppDialogVisible.png and b/homeUi/screenshotTests/roborazzi/com.gravatar.app.homeUi.presentation.home.components.topbar.components.AboutAppDialogTest.aboutAppDialogVisible.png differ diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/di/HomeUiModule.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/di/HomeUiModule.kt index a7998a8d..99007290 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/di/HomeUiModule.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/di/HomeUiModule.kt @@ -4,6 +4,7 @@ import com.gravatar.app.homeUi.ImageDownloader import com.gravatar.app.homeUi.presentation.DrawableUtils import com.gravatar.app.homeUi.presentation.FileUtils import com.gravatar.app.homeUi.presentation.home.HomeViewModel +import com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsViewModel import com.gravatar.app.homeUi.presentation.home.components.topbar.TopBarPickerPopupViewModel import com.gravatar.app.homeUi.presentation.home.components.topbar.components.about.AboutAppDialogViewModel import com.gravatar.app.homeUi.presentation.home.gravatar.GravatarViewModel @@ -25,6 +26,7 @@ val homeUiModule = module { viewModelOf(::AboutAppDialogViewModel) viewModelOf(::ShareViewModel) viewModelOf(::HomeViewModel) + viewModelOf(::PrivacySettingsViewModel) includes(userComponentModule) } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingUiState.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingUiState.kt new file mode 100644 index 00000000..849d9f22 --- /dev/null +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingUiState.kt @@ -0,0 +1,10 @@ +package com.gravatar.app.homeUi.presentation.home.components.privacySetting + +import com.gravatar.app.usercomponent.domain.model.PrivacySettings + +internal data class PrivacySettingUiState( + val privacySettings: PrivacySettings = PrivacySettings( + analyticsEnabled = true, + crashReportingEnabled = true + ) +) diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsBottomSheet.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsBottomSheet.kt new file mode 100644 index 00000000..694bc7ee --- /dev/null +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsBottomSheet.kt @@ -0,0 +1,224 @@ +@file:OptIn(ExperimentalMaterial3Api::class) + +package com.gravatar.app.homeUi.presentation.home.components.privacySetting + +import android.content.Context +import android.content.Intent +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeContent +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.gravatar.app.design.theme.GravatarAppTheme +import com.gravatar.app.homeUi.R +import org.koin.androidx.compose.koinViewModel + +private const val PRIVACY_POLICY_URL = "https://automattic.com/privacy/" + +@Composable +internal fun PrivacySettingsBottomSheet( + onDismissRequest: () -> Unit +) { + val topPadding = with(LocalDensity.current) { + WindowInsets.safeContent.only(WindowInsetsSides.Top).getTop(LocalDensity.current).toDp() + } + val viewModel: PrivacySettingsViewModel = koinViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ), + containerColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.padding(top = topPadding), + dragHandle = { + Surface( + modifier = Modifier.padding(vertical = 8.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + shape = MaterialTheme.shapes.extraLarge + ) { + Box(Modifier.size(width = 32.dp, height = 4.dp)) + } + } + ) { + PrivacySettings( + uiState = uiState, + onEvent = viewModel::onEvent, + onDismissRequest = onDismissRequest, + ) + } +} + +@Composable +internal fun PrivacySettings( + uiState: PrivacySettingUiState, + onEvent: (PrivacySettingsEvent) -> Unit, + onDismissRequest: () -> Unit, +) { + val context = LocalContext.current + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceVariant) + .fillMaxWidth(), + ) { + CenterAlignedTopAppBar( + title = { + Text(text = stringResource(R.string.privacy_settings_top_bar_title)) + }, + navigationIcon = { + IconButton( + onClick = onDismissRequest + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.close_button) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + windowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp), + ) + Column( + verticalArrangement = Arrangement.spacedBy(26.dp), + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(bottom = 16.dp, top = 8.dp, start = 16.dp, end = 16.dp) + ) { + PrivacySettingsCard( + settingTitle = stringResource(R.string.privacy_settings_share_analytics_data_tittle), + settingIcon = R.drawable.ic_analytics_tracking, + settingDescription = stringResource(R.string.privacy_settings_share_analytics_data_description), + checked = uiState.privacySettings.analyticsEnabled, + onCheckedChange = { onEvent(PrivacySettingsEvent.OnAnalyticsEnabledChanged(it)) }, + extraContent = { + Text( + text = stringResource(R.string.privacy_settings_privacy_policy), + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(top = 16.dp) + .clickable { + context.openPrivacyPolicy() + } + ) + }, + modifier = Modifier.fillMaxWidth() + ) + PrivacySettingsCard( + settingTitle = stringResource(R.string.privacy_settings_share_crash_reports_title), + settingIcon = R.drawable.ic_crashlytics, + settingDescription = stringResource(R.string.privacy_settings_share_crash_reports_description), + checked = uiState.privacySettings.crashReportingEnabled, + onCheckedChange = { onEvent(PrivacySettingsEvent.OnCrashReportingEnabledChanged(it)) }, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +private fun Context.openPrivacyPolicy() { + val intent = Intent(Intent.ACTION_VIEW, PRIVACY_POLICY_URL.toUri()) + startActivity(intent) +} + +@Composable +private fun PrivacySettingsCard( + settingTitle: String, + @DrawableRes settingIcon: Int, + settingDescription: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + extraContent: @Composable ColumnScope.() -> Unit = {}, +) { + val shape = RoundedCornerShape(12.dp) + Column( + modifier = modifier + .background(MaterialTheme.colorScheme.surface, shape) + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + painter = painterResource(settingIcon), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = settingTitle, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + modifier = Modifier.weight(1f) + ) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + ) + } + HorizontalDivider(thickness = 1.dp) + Text( + text = settingDescription, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(top = 16.dp) + ) + extraContent() + } +} + +@Preview +@Composable +internal fun PrivacySettingsPreview() { + GravatarAppTheme { + PrivacySettings( + uiState = PrivacySettingUiState(), + onEvent = {}, + onDismissRequest = {} + ) + } +} diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsEvent.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsEvent.kt new file mode 100644 index 00000000..80acd080 --- /dev/null +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsEvent.kt @@ -0,0 +1,6 @@ +package com.gravatar.app.homeUi.presentation.home.components.privacySetting + +internal sealed class PrivacySettingsEvent { + data class OnAnalyticsEnabledChanged(val enabled: Boolean) : PrivacySettingsEvent() + data class OnCrashReportingEnabledChanged(val enabled: Boolean) : PrivacySettingsEvent() +} diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsViewModel.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsViewModel.kt new file mode 100644 index 00000000..9b5f2914 --- /dev/null +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsViewModel.kt @@ -0,0 +1,58 @@ +package com.gravatar.app.homeUi.presentation.home.components.privacySetting + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.gravatar.app.usercomponent.domain.model.PrivacySettings +import com.gravatar.app.usercomponent.domain.usecase.GetPrivacySettings +import com.gravatar.app.usercomponent.domain.usecase.UpdatePrivacySettings +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class PrivacySettingsViewModel( + private val getPrivacySettings: GetPrivacySettings, + private val updatePrivacySettings: UpdatePrivacySettings, +) : ViewModel() { + + private val _uiState = MutableStateFlow(PrivacySettingUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + collectPrivacySettings() + } + + fun onEvent(event: PrivacySettingsEvent) { + when (event) { + is PrivacySettingsEvent.OnAnalyticsEnabledChanged -> { + val newSettings = _uiState.value.privacySettings.copy(analyticsEnabled = event.enabled) + updateSettings(newSettings) + } + + is PrivacySettingsEvent.OnCrashReportingEnabledChanged -> { + val newSettings = _uiState.value.privacySettings.copy(crashReportingEnabled = event.enabled) + updateSettings(newSettings) + } + } + } + + private fun collectPrivacySettings() { + getPrivacySettings() + .onEach { settings -> + _uiState.update { current -> + current.copy(privacySettings = settings) + } + } + .launchIn(viewModelScope) + } + + private fun updateSettings(newSettings: PrivacySettings) { + _uiState.update { it.copy(privacySettings = newSettings) } + viewModelScope.launch { + updatePrivacySettings(newSettings) + } + } +} diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/about/AboutAppDialog.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/about/AboutAppDialog.kt index 0d15a7e8..4df2f205 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/about/AboutAppDialog.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/about/AboutAppDialog.kt @@ -46,6 +46,7 @@ import org.koin.compose.koinInject @Composable internal fun AboutAppDialog( onDismissRequest: () -> Unit, + onPrivacySettingsClicked: () -> Unit, viewModel: AboutAppDialogViewModel = koinViewModel() ) { val appVersion: AppVersion = koinInject() @@ -66,6 +67,7 @@ internal fun AboutAppDialog( AboutAppDialogContent( appVersion = appVersion.value, onDone = onDismissRequest, + onPrivacySettingsClicked = onPrivacySettingsClicked, onEvent = viewModel::onEvent, modifier = Modifier ) @@ -75,7 +77,9 @@ internal fun AboutAppDialog( onDismissRequest = { viewModel.dismissErrorMessage() } ) { Surface( - modifier = Modifier.wrapContentWidth().wrapContentHeight(), + modifier = Modifier + .wrapContentWidth() + .wrapContentHeight(), shape = MaterialTheme.shapes.large, tonalElevation = AlertDialogDefaults.TonalElevation ) { @@ -108,6 +112,7 @@ internal fun AboutAppDialogContent( appVersion: String, onDone: () -> Unit, onEvent: (AboutAppDialogEvent) -> Unit, + onPrivacySettingsClicked: () -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -161,9 +166,9 @@ internal fun AboutAppDialogContent( } ) DialogText( - text = stringResource(R.string.about_app_dialog_privacy_policy), + text = stringResource(R.string.about_app_dialog_privacy_settings), modifier = Modifier.clickable { - context.openPrivacyPolicy() + onPrivacySettingsClicked() } ) } @@ -208,8 +213,6 @@ private fun Context.openSupportPage() = openUrl("https://$SUPPORT_URL") private fun Context.openTermsOfService() = openUrlInApp(TERMS_OF_SERVICE_URL) -private fun Context.openPrivacyPolicy() = openUrlInApp(PRIVACY_POLICY_URL) - private fun Context.openUrl(url: String) { val intent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity(intent) @@ -221,7 +224,6 @@ private fun Context.openUrlInApp(url: String) = private const val SUPPORT_URL = "support.gravatar.com" private const val SUPPORT_EMAIL = "support@gravatar.com" private const val TERMS_OF_SERVICE_URL = "https://wordpress.com/tos/" -private const val PRIVACY_POLICY_URL = "https://automattic.com/privacy/" @Preview(showBackground = true) @Composable @@ -231,6 +233,7 @@ private fun AboutAppDialogContentPreview() { appVersion = "0.0.1", onDone = { }, onEvent = { _ -> }, + onPrivacySettingsClicked = { }, modifier = Modifier.fillMaxWidth() ) } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarEvent.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarEvent.kt index f9c8b179..e7e2af0a 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarEvent.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarEvent.kt @@ -16,4 +16,6 @@ sealed class GravatarEvent { data object OnDismissDeleteConfirmation : GravatarEvent() data object OnDismissAboutAppDialog : GravatarEvent() data object OnAboutAppClicked : GravatarEvent() + data object OnPrivacySettingClicked : GravatarEvent() + data object OnPrivacySettingDismissed : GravatarEvent() } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarScreen.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarScreen.kt index b673d37d..42a7ed62 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarScreen.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarScreen.kt @@ -55,6 +55,7 @@ import com.gravatar.app.homeUi.GravatarFileProvider import com.gravatar.app.homeUi.R import com.gravatar.app.homeUi.presentation.home.components.ErrorViewWithRetry import com.gravatar.app.homeUi.presentation.home.components.PermissionRationaleDialog +import com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheet import com.gravatar.app.homeUi.presentation.home.components.topbar.components.about.AboutAppDialog import com.gravatar.app.homeUi.presentation.home.gravatar.components.AvatarDeletionConfirmationDialog import com.gravatar.app.homeUi.presentation.home.gravatar.components.AvatarOption @@ -335,6 +336,12 @@ internal fun GravatarScreen( if (uiState.isAboutAppDialogVisible) { AboutAppDialog( onDismissRequest = { onEvent(GravatarEvent.OnDismissAboutAppDialog) }, + onPrivacySettingsClicked = { onEvent(GravatarEvent.OnPrivacySettingClicked) } + ) + } + if (uiState.isPrivacySettingVisible) { + PrivacySettingsBottomSheet( + onDismissRequest = { onEvent(GravatarEvent.OnPrivacySettingDismissed) } ) } } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarUiState.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarUiState.kt index b58ecd05..49a5900c 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarUiState.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarUiState.kt @@ -16,6 +16,7 @@ internal data class GravatarUiState( val failedUploadDialog: AvatarUploadFailure? = null, val confirmAvatarDeletionId: String? = null, val isAboutAppDialogVisible: Boolean = false, + val isPrivacySettingVisible: Boolean = false, ) { val avatarsUi: List? = avatars?.let { buildList { diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarViewModel.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarViewModel.kt index 434261ff..457d3da2 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarViewModel.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/gravatar/GravatarViewModel.kt @@ -70,6 +70,20 @@ internal class GravatarViewModel( GravatarEvent.OnDismissDeleteConfirmation -> dismissDeleteConfirmation() GravatarEvent.OnAboutAppClicked -> showAboutAppDialog() GravatarEvent.OnDismissAboutAppDialog -> dismissAboutAppDialog() + GravatarEvent.OnPrivacySettingClicked -> showPrivacySetting() + GravatarEvent.OnPrivacySettingDismissed -> dismissPrivacySetting() + } + } + + private fun showPrivacySetting() { + _uiState.update { currentState -> + currentState.copy(isPrivacySettingVisible = true) + } + } + + private fun dismissPrivacySetting() { + _uiState.update { currentState -> + currentState.copy(isPrivacySettingVisible = false) } } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileEvent.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileEvent.kt index b7f5ba6e..7329b71f 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileEvent.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileEvent.kt @@ -18,4 +18,8 @@ internal sealed class ProfileEvent { object OnAboutAppClicked : ProfileEvent() object OnDismissAboutAppDialog : ProfileEvent() + + object OnPrivacySettingClicked : ProfileEvent() + + object OnPrivacySettingDismissed : ProfileEvent() } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileScreen.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileScreen.kt index e4e6e64f..b2538e41 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileScreen.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileScreen.kt @@ -40,6 +40,7 @@ import com.gravatar.app.design.components.Screen import com.gravatar.app.design.components.snackbar.SnackbarType import com.gravatar.app.design.components.snackbar.showGravatarSnackbar import com.gravatar.app.homeUi.R +import com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheet import com.gravatar.app.homeUi.presentation.home.components.topbar.components.about.AboutAppDialog import com.gravatar.app.homeUi.presentation.home.profile.about.AboutInputField import com.gravatar.app.homeUi.presentation.home.profile.about.AboutSection @@ -173,6 +174,16 @@ internal fun ProfileScreen(uiState: ProfileUiState, onEvent: (ProfileEvent) -> U if (uiState.isAboutAppDialogVisible) { AboutAppDialog( onDismissRequest = { onEvent(ProfileEvent.OnDismissAboutAppDialog) }, + onPrivacySettingsClicked = { + onEvent(ProfileEvent.OnPrivacySettingClicked) + onEvent(ProfileEvent.OnDismissAboutAppDialog) + }, + ) + } + + if (uiState.isPrivacySettingsVisible) { + PrivacySettingsBottomSheet( + onDismissRequest = { onEvent(ProfileEvent.OnPrivacySettingDismissed) } ) } } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileUiState.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileUiState.kt index 5b01e761..fcb0fd6c 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileUiState.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileUiState.kt @@ -12,6 +12,7 @@ internal data class ProfileUiState( val isSavingProfile: Boolean = false, val isRefreshing: Boolean = false, val isAboutAppDialogVisible: Boolean = false, + val isPrivacySettingsVisible: Boolean = false, ) { val originalAboutFields: Set = profile?.aboutFields() ?: emptySet() diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileViewModel.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileViewModel.kt index 18790f90..3bb84256 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileViewModel.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/profile/ProfileViewModel.kt @@ -44,6 +44,8 @@ internal class ProfileViewModel( ProfileEvent.OnProfileLinkClicked -> openProfileUrl() ProfileEvent.OnAboutAppClicked -> showAboutAppDialog() ProfileEvent.OnDismissAboutAppDialog -> dismissAboutAppDialog() + ProfileEvent.OnPrivacySettingClicked -> showPrivacySettings() + ProfileEvent.OnPrivacySettingDismissed -> dismissPrivacySettings() } } @@ -160,6 +162,18 @@ internal class ProfileViewModel( currentState.copy(isAboutAppDialogVisible = false) } } + + private fun showPrivacySettings() { + _uiState.update { currentState -> + currentState.copy(isPrivacySettingsVisible = true) + } + } + + private fun dismissPrivacySettings() { + _uiState.update { currentState -> + currentState.copy(isPrivacySettingsVisible = false) + } + } } internal fun Map.updateProfileRequest() = diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareEvent.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareEvent.kt index 56216ef9..8eb0bf80 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareEvent.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareEvent.kt @@ -11,4 +11,6 @@ internal sealed class ShareEvent { data object OnShareClick : ShareEvent() data object OnExpandQrCodeClick : ShareEvent() data object OnDismissExpandedQrCode : ShareEvent() + data object OnPrivacySettingClicked : ShareEvent() + data object OnPrivacySettingDismissed : ShareEvent() } diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareScreen.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareScreen.kt index 5af34355..5cdf1255 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareScreen.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareScreen.kt @@ -31,6 +31,7 @@ import androidx.lifecycle.repeatOnLifecycle import com.gravatar.app.design.components.Screen import com.gravatar.app.design.theme.GravatarAppTheme import com.gravatar.app.homeUi.GravatarFileProvider +import com.gravatar.app.homeUi.presentation.home.components.privacySetting.PrivacySettingsBottomSheet import com.gravatar.app.homeUi.presentation.home.components.topbar.components.about.AboutAppDialog import com.gravatar.app.homeUi.presentation.home.share.components.ExpandedQrCode import com.gravatar.app.homeUi.presentation.home.share.components.ItemDivider @@ -140,7 +141,8 @@ internal fun ShareScreen(uiState: ShareUiState, onEvent: (ShareEvent) -> Unit) { AboutAppDialog( onDismissRequest = { onEvent(ShareEvent.OnDismissAboutAppDialog) - } + }, + onPrivacySettingsClicked = { onEvent(ShareEvent.OnPrivacySettingClicked) } ) } @@ -165,6 +167,12 @@ internal fun ShareScreen(uiState: ShareUiState, onEvent: (ShareEvent) -> Unit) { } ) } + + if (uiState.isPrivacySettingVisible) { + PrivacySettingsBottomSheet( + onDismissRequest = { onEvent(ShareEvent.OnPrivacySettingDismissed) } + ) + } } private fun shareVCardFile( diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareUiState.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareUiState.kt index d4770180..08d1dd00 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareUiState.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareUiState.kt @@ -14,6 +14,7 @@ internal data class ShareUiState( val userSharePreferences: UserSharePreferences = UserSharePreferences.Default, val isPrivateInformationDialogVisible: Boolean = false, val isQrCodeExpanded: Boolean = false, + val isPrivacySettingVisible: Boolean = false, private val avatarDrawable: Drawable? = null, ) { val privateContactState = PrivateContactState( diff --git a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModel.kt b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModel.kt index c3017a98..eaa181e5 100644 --- a/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModel.kt +++ b/homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/ShareViewModel.kt @@ -77,6 +77,8 @@ internal class ShareViewModel( ShareEvent.OnShareClick -> shareVCard() ShareEvent.OnExpandQrCodeClick -> expandQrCode() ShareEvent.OnDismissExpandedQrCode -> hideExpandedQrCode() + ShareEvent.OnPrivacySettingClicked -> showPrivacySetting() + ShareEvent.OnPrivacySettingDismissed -> hidePrivacySetting() } } @@ -155,6 +157,18 @@ internal class ShareViewModel( } } + private fun showPrivacySetting() { + _uiState.update { currentState -> + currentState.copy(isPrivacySettingVisible = true) + } + } + + private fun hidePrivacySetting() { + _uiState.update { currentState -> + currentState.copy(isPrivacySettingVisible = false) + } + } + private fun collectAvatarUrl() { getAvatarUrl() .onEach { avatarUrl -> diff --git a/homeUi/src/main/res/drawable/ic_analytics_tracking.xml b/homeUi/src/main/res/drawable/ic_analytics_tracking.xml new file mode 100644 index 00000000..867db51e --- /dev/null +++ b/homeUi/src/main/res/drawable/ic_analytics_tracking.xml @@ -0,0 +1,9 @@ + + + diff --git a/homeUi/src/main/res/drawable/ic_crashlytics.xml b/homeUi/src/main/res/drawable/ic_crashlytics.xml new file mode 100644 index 00000000..299668c0 --- /dev/null +++ b/homeUi/src/main/res/drawable/ic_crashlytics.xml @@ -0,0 +1,9 @@ + + + diff --git a/homeUi/src/main/res/values/strings.xml b/homeUi/src/main/res/values/strings.xml index 374e9465..d0a6f139 100644 --- a/homeUi/src/main/res/values/strings.xml +++ b/homeUi/src/main/res/values/strings.xml @@ -75,7 +75,7 @@ Get help Legal Terms of Service - Privacy Policy + Privacy Settings Delete account Done Share info from your Gravatar profile. @@ -95,4 +95,10 @@ Cancel An error has occurred while deleting your account %s account + Privacy Settings + Share Analytics Data + Share information with our analytics tool about how you interact with this app and our services.\n\nThis information helps us improve our products, and offer you a better experience. + Share Crash Reports + To help us improve the app’s performance and fix occasional bugs, enable automatic crash reports + Privacy Policy diff --git a/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsBottomSheetTest.kt b/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsBottomSheetTest.kt new file mode 100644 index 00000000..6d4e3caf --- /dev/null +++ b/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsBottomSheetTest.kt @@ -0,0 +1,41 @@ +package com.gravatar.app.homeUi.presentation.home.components.privacySetting + +import com.gravatar.app.design.theme.GravatarAppTheme +import com.gravatar.app.testUtils.roborazzi.RoborazziTest +import com.gravatar.app.usercomponent.domain.model.PrivacySettings +import org.junit.Test + +class PrivacySettingsBottomSheetTest : RoborazziTest() { + + @Test + fun privacySettingsAllEnabled() = screenshotTest { + GravatarAppTheme { + PrivacySettings( + uiState = PrivacySettingUiState( + privacySettings = PrivacySettings( + analyticsEnabled = true, + crashReportingEnabled = true + ) + ), + onEvent = { }, + onDismissRequest = { }, + ) + } + } + + @Test + fun privacySettingsAllDisabled() = screenshotTest { + GravatarAppTheme { + PrivacySettings( + uiState = PrivacySettingUiState( + privacySettings = PrivacySettings( + analyticsEnabled = false, + crashReportingEnabled = false + ) + ), + onEvent = { }, + onDismissRequest = { }, + ) + } + } +} diff --git a/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsViewModelTest.kt b/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsViewModelTest.kt new file mode 100644 index 00000000..62509723 --- /dev/null +++ b/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/privacySetting/PrivacySettingsViewModelTest.kt @@ -0,0 +1,111 @@ +package com.gravatar.app.homeUi.presentation.home.components.privacySetting + +import app.cash.turbine.test +import com.gravatar.app.testUtils.CoroutineTestRule +import com.gravatar.app.usercomponent.domain.model.PrivacySettings +import com.gravatar.app.usercomponent.domain.usecase.GetPrivacySettings +import com.gravatar.app.usercomponent.domain.usecase.UpdatePrivacySettings +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class PrivacySettingsViewModelTest { + private val testDispatcher = StandardTestDispatcher() + + @get:Rule + var coroutineTestRule = CoroutineTestRule(testDispatcher) + + private lateinit var viewModel: PrivacySettingsViewModel + private lateinit var privacySettingsFlow: MutableSharedFlow + + private val getPrivacySettings: GetPrivacySettings = object : GetPrivacySettings { + override fun invoke() = privacySettingsFlow + } + private val updatePrivacySettings: UpdatePrivacySettings = mockk(relaxed = true) + + @Before + fun setup() { + privacySettingsFlow = MutableSharedFlow() + viewModel = PrivacySettingsViewModel( + getPrivacySettings = getPrivacySettings, + updatePrivacySettings = updatePrivacySettings, + ) + } + + @Test + fun `init should collect privacy settings and update ui state`() = runTest { + // Given + val emittedSettings = PrivacySettings(analyticsEnabled = false, crashReportingEnabled = true) + + // When + privacySettingsFlow.emit(emittedSettings) + advanceUntilIdle() + + // Then + viewModel.uiState.test { + assertEquals( + PrivacySettingUiState(privacySettings = emittedSettings), + awaitItem() + ) + } + } + + @Test + fun `onEvent OnAnalyticsEnabledChanged should update state and call update use case`() = runTest { + // Given: initial state is default (true, true) + + // When + viewModel.onEvent(PrivacySettingsEvent.OnAnalyticsEnabledChanged(false)) + advanceUntilIdle() + + // Then: state updated immediately + viewModel.uiState.test { + assertEquals( + PrivacySettingUiState( + privacySettings = PrivacySettings(analyticsEnabled = false, crashReportingEnabled = true) + ), + awaitItem() + ) + } + // And use case invoked with new settings + coVerify { + updatePrivacySettings.invoke( + PrivacySettings(analyticsEnabled = false, crashReportingEnabled = true) + ) + } + } + + @Test + fun `onEvent OnCrashReportingEnabledChanged should update state and call update use case`() = runTest { + // Given: initial state is default (true, true) + + // When + viewModel.onEvent(PrivacySettingsEvent.OnCrashReportingEnabledChanged(false)) + advanceUntilIdle() + + // Then: state updated immediately + viewModel.uiState.test { + assertEquals( + PrivacySettingUiState( + privacySettings = PrivacySettings(analyticsEnabled = true, crashReportingEnabled = false) + ), + awaitItem() + ) + } + // And use case invoked with new settings + coVerify { + updatePrivacySettings.invoke( + PrivacySettings(analyticsEnabled = true, crashReportingEnabled = false) + ) + } + } +} diff --git a/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/AboutAppDialogTest.kt b/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/AboutAppDialogTest.kt index d8970242..07fe9e16 100644 --- a/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/AboutAppDialogTest.kt +++ b/homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/components/topbar/components/AboutAppDialogTest.kt @@ -15,7 +15,8 @@ class AboutAppDialogTest : RoborazziTest() { AboutAppDialogContent( appVersion = "1.0.0", onDone = {}, - onEvent = { _ -> } + onEvent = { _ -> }, + onPrivacySettingsClicked = {} ) } } diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/UserPrefsStorage.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/UserPrefsStorage.kt index 7b1b5027..5cb5becb 100644 --- a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/UserPrefsStorage.kt +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/data/UserPrefsStorage.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import com.gravatar.app.foundations.DispatcherProvider import com.gravatar.app.usercomponent.di.UserPrefs +import com.gravatar.app.usercomponent.domain.model.PrivacySettings import com.gravatar.app.usercomponent.domain.model.PrivateContactInfo import com.gravatar.app.usercomponent.domain.model.UserSharePreferences import kotlinx.coroutines.flow.Flow @@ -45,10 +46,21 @@ internal interface PrivateContactInfoStorage { suspend fun savePrivateContactInfo(privateContactInfo: PrivateContactInfo) } +internal interface PrivacySettingsStorage { + fun getPrivacySettings(): Flow + + suspend fun savePrivacySettings(privacySettings: PrivacySettings) +} + /** * Convenient interface to clear all user related data in one call. */ -internal interface UserStorage : AuthTokenStorage, AvatarCacheBusterStorage, UserSharePreferencesStorage, PrivateContactInfoStorage { +internal interface UserStorage : + AuthTokenStorage, + AvatarCacheBusterStorage, + UserSharePreferencesStorage, + PrivateContactInfoStorage, + PrivacySettingsStorage { suspend fun clear() } @@ -71,6 +83,8 @@ internal class DatastoreUserPrefsStorage( private const val USER_SHARE_PRIVATE_PHONE_VALUE_KEY = "private_phone_value" private const val USER_SHARE_PRIVATE_EMAIL_VALUE_KEY = "private_email_value" private const val USER_SHARE_VERIFIED_ACCOUNTS_KEY = "verified_accounts" + private const val PRIVACY_SETTING_ANALYTICS_KEY = "privacy_setting_analytics" + private const val PRIVACY_SETTING_CRASH_REPORTING_KEY = "privacy_setting_crash_reporting" } private val tokenKey = stringPreferencesKey(AUTH_TOKEN_KEY) @@ -86,6 +100,8 @@ internal class DatastoreUserPrefsStorage( private val userSharePrivatePhoneValueKey = stringPreferencesKey(USER_SHARE_PRIVATE_PHONE_VALUE_KEY) private val userSharePrivateEmailValueKey = stringPreferencesKey(USER_SHARE_PRIVATE_EMAIL_VALUE_KEY) private val userShareVerifiedAccounts = stringPreferencesKey(USER_SHARE_VERIFIED_ACCOUNTS_KEY) + private val privacySettingAnalyticsKey = booleanPreferencesKey(PRIVACY_SETTING_ANALYTICS_KEY) + private val privacySettingCrashReportingKey = booleanPreferencesKey(PRIVACY_SETTING_CRASH_REPORTING_KEY) override suspend fun getToken(): String? { return try { @@ -196,4 +212,25 @@ internal class DatastoreUserPrefsStorage( .getOrNull() ?: emptyMap() } + + override fun getPrivacySettings(): Flow { + return dataStore.data + .map { preferences -> + PrivacySettings( + analyticsEnabled = preferences[privacySettingAnalyticsKey] ?: true, + crashReportingEnabled = preferences[privacySettingCrashReportingKey] ?: true + ) + } + .catch { emit(PrivacySettings(analyticsEnabled = true, crashReportingEnabled = true)) } + .flowOn(dispatcherProvider.io) + } + + override suspend fun savePrivacySettings(privacySettings: PrivacySettings) { + withContext(dispatcherProvider.io) { + dataStore.edit { preferences -> + preferences[privacySettingAnalyticsKey] = privacySettings.analyticsEnabled + preferences[privacySettingCrashReportingKey] = privacySettings.crashReportingEnabled + } + } + } } diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/DatastoreModule.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/DatastoreModule.kt index c7ac6f50..add749bc 100644 --- a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/DatastoreModule.kt +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/DatastoreModule.kt @@ -7,6 +7,7 @@ import androidx.datastore.preferences.preferencesDataStore import com.gravatar.app.usercomponent.data.AuthTokenStorage import com.gravatar.app.usercomponent.data.AvatarCacheBusterStorage import com.gravatar.app.usercomponent.data.DatastoreUserPrefsStorage +import com.gravatar.app.usercomponent.data.PrivacySettingsStorage import com.gravatar.app.usercomponent.data.PrivateContactInfoStorage import com.gravatar.app.usercomponent.data.UserSharePreferencesStorage import com.gravatar.app.usercomponent.data.UserStorage @@ -49,6 +50,12 @@ internal val datastoreModule = module { dispatcherProvider = get() ) } + factory { + DatastoreUserPrefsStorage( + dataStore = get(qualifier = named()), + dispatcherProvider = get() + ) + } } private val Context.userPreferencesDataStore by preferencesDataStore(name = "user-preferences") diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/UserComponentModule.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/UserComponentModule.kt index e1bc99c1..25a37faa 100644 --- a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/UserComponentModule.kt +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/di/UserComponentModule.kt @@ -21,6 +21,8 @@ import com.gravatar.app.usercomponent.domain.usecase.FetchAvatarsUseCase import com.gravatar.app.usercomponent.domain.usecase.FetchUserAvatars import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrl import com.gravatar.app.usercomponent.domain.usecase.GetAvatarUrlUseCase +import com.gravatar.app.usercomponent.domain.usecase.GetPrivacySettings +import com.gravatar.app.usercomponent.domain.usecase.GetPrivacySettingsUseCase import com.gravatar.app.usercomponent.domain.usecase.GetPrivateContactInfo import com.gravatar.app.usercomponent.domain.usecase.GetPrivateContactInfoUseCase import com.gravatar.app.usercomponent.domain.usecase.GetUserSharePreferences @@ -33,6 +35,8 @@ import com.gravatar.app.usercomponent.domain.usecase.Logout import com.gravatar.app.usercomponent.domain.usecase.LogoutUseCase import com.gravatar.app.usercomponent.domain.usecase.SelectAvatarUseCase import com.gravatar.app.usercomponent.domain.usecase.SelectUserAvatar +import com.gravatar.app.usercomponent.domain.usecase.UpdatePrivacySettings +import com.gravatar.app.usercomponent.domain.usecase.UpdatePrivacySettingsUseCase import com.gravatar.app.usercomponent.domain.usecase.UpdatePrivateContactInfo import com.gravatar.app.usercomponent.domain.usecase.UpdatePrivateContactInfoUseCase import com.gravatar.app.usercomponent.domain.usecase.UpdateUserSharePreferences @@ -63,6 +67,8 @@ val userComponentModule = module { factoryOf(::DeleteUserProfileUseCase) { bind() } factoryOf(::UserSharePreferencesOperations) { bind() } factoryOf(::PrivateContactInfoOperations) { bind() } + factoryOf(::GetPrivacySettingsUseCase) { bind() } + factoryOf(::UpdatePrivacySettingsUseCase) { bind() } factoryOf(::WordPressClient) singleOf(::InMemoryUserSessionPersistence) { bind() } includes(httpClientModule) diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/model/PrivacySettings.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/model/PrivacySettings.kt new file mode 100644 index 00000000..46f97183 --- /dev/null +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/model/PrivacySettings.kt @@ -0,0 +1,6 @@ +package com.gravatar.app.usercomponent.domain.model + +data class PrivacySettings( + val analyticsEnabled: Boolean, + val crashReportingEnabled: Boolean, +) diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/usecase/GetPrivacySettingsUseCase.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/usecase/GetPrivacySettingsUseCase.kt new file mode 100644 index 00000000..a265f755 --- /dev/null +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/usecase/GetPrivacySettingsUseCase.kt @@ -0,0 +1,20 @@ +package com.gravatar.app.usercomponent.domain.usecase + +import com.gravatar.app.usercomponent.data.PrivacySettingsStorage +import com.gravatar.app.usercomponent.domain.model.PrivacySettings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +internal class GetPrivacySettingsUseCase( + private val privacySettingsStorage: PrivacySettingsStorage +) : GetPrivacySettings { + + override fun invoke(): Flow { + return privacySettingsStorage.getPrivacySettings() + .distinctUntilChanged() + } +} + +interface GetPrivacySettings { + operator fun invoke(): Flow +} diff --git a/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/usecase/UpdatePrivacySettingsUseCase.kt b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/usecase/UpdatePrivacySettingsUseCase.kt new file mode 100644 index 00000000..e6eff1d9 --- /dev/null +++ b/userComponent/src/main/kotlin/com/gravatar/app/usercomponent/domain/usecase/UpdatePrivacySettingsUseCase.kt @@ -0,0 +1,17 @@ +package com.gravatar.app.usercomponent.domain.usecase + +import com.gravatar.app.usercomponent.data.PrivacySettingsStorage +import com.gravatar.app.usercomponent.domain.model.PrivacySettings + +internal class UpdatePrivacySettingsUseCase( + private val privacySettingsStorage: PrivacySettingsStorage +) : UpdatePrivacySettings { + + override suspend fun invoke(privacySettings: PrivacySettings) { + privacySettingsStorage.savePrivacySettings(privacySettings) + } +} + +interface UpdatePrivacySettings { + suspend operator fun invoke(privacySettings: PrivacySettings) +}