From 8186f4a8ca61821353f6f976ece51b9f6b3fa469 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Mon, 24 Feb 2025 16:25:11 +0200 Subject: [PATCH 1/2] MS-882 Download privacy notice in the default language if current is not available --- .../screens/privacy/PrivacyNoticeViewModel.kt | 37 +++++++--- .../privacy/PrivacyNoticeViewModelTest.kt | 67 +++++++++++++------ 2 files changed, 75 insertions(+), 29 deletions(-) diff --git a/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt b/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt index 7a3ca96cab..9e7989d28d 100644 --- a/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt +++ b/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt @@ -14,9 +14,13 @@ import com.simprints.infra.config.store.models.PrivacyNoticeResult.FailedBecause import com.simprints.infra.config.store.models.PrivacyNoticeResult.InProgress import com.simprints.infra.config.store.models.PrivacyNoticeResult.Succeed import com.simprints.infra.config.sync.ConfigManager +import com.simprints.infra.logging.Simber import com.simprints.infra.network.ConnectivityTracker import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -44,18 +48,31 @@ internal class PrivacyNoticeViewModel @Inject constructor( } fun retrievePrivacyNotice() = viewModelScope.launch { - val deviceConfiguration = configManager.getDeviceConfiguration() - configManager - .getPrivacyNotice( - authStore.signedInProjectId, - deviceConfiguration.language, - ).map { it.toPrivacyNoticeViewState() } - .catch { - it.printStackTrace() - PrivacyNoticeState.ConsentNotAvailable - }.collect { _viewState.postValue(it) } + val projectId = authStore.signedInProjectId + val deviceLanguage = configManager.getDeviceConfiguration().language + val defaultLanguage = configManager.getProjectConfiguration().general.defaultLanguage + + attemptDownloadingLanguage(projectId, deviceLanguage) + .flatMapLatest { + if (it is PrivacyNoticeState.ConsentNotAvailable) { + Simber.i("Privacy notice in ($deviceLanguage) not available") + attemptDownloadingLanguage(projectId, defaultLanguage) + } else { + flowOf(it) + } + }.catch { + Simber.i("Notice download failed", it) + emit(PrivacyNoticeState.ConsentNotAvailable) + }.collect { + _viewState.postValue(it) + } } + private suspend fun attemptDownloadingLanguage( + projectId: String, + deviceLanguage: String, + ): Flow = configManager.getPrivacyNotice(projectId, deviceLanguage).map { it.toPrivacyNoticeViewState() } + private fun PrivacyNoticeResult.toPrivacyNoticeViewState(): PrivacyNoticeState = when (this) { is Succeed -> PrivacyNoticeState.ConsentAvailable(consent) is InProgress -> PrivacyNoticeState.DownloadInProgress diff --git a/feature/consent/src/test/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModelTest.kt b/feature/consent/src/test/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModelTest.kt index b1ce5722b7..706c139eda 100644 --- a/feature/consent/src/test/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModelTest.kt +++ b/feature/consent/src/test/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModelTest.kt @@ -22,7 +22,8 @@ import org.junit.Test internal class PrivacyNoticeViewModelTest { companion object { private const val PROJECT_ID = "projectId" - private const val LANGUAGE = "en" + private const val DEVICE_LANGUAGE = "en" + private const val DEFAULT_LANGUAGE = "fr" } @get:Rule @@ -46,7 +47,8 @@ internal class PrivacyNoticeViewModelTest { fun setUp() { MockKAnnotations.init(this, relaxed = true) - coEvery { configManager.getDeviceConfiguration() } returns DeviceConfiguration(LANGUAGE, listOf(), "") + coEvery { configManager.getDeviceConfiguration() } returns DeviceConfiguration(DEVICE_LANGUAGE, listOf(), "") + coEvery { configManager.getProjectConfiguration().general.defaultLanguage } returns DEFAULT_LANGUAGE every { authStore.signedInProjectId } returns PROJECT_ID privacyNoticeViewModel = PrivacyNoticeViewModel( @@ -58,8 +60,8 @@ internal class PrivacyNoticeViewModelTest { @Test fun `retrievePrivacyNotice should return DownloadInProgress when trying download`() = runTest { - coEvery { configManager.getPrivacyNotice(PROJECT_ID, LANGUAGE) } returns flowOf( - PrivacyNoticeResult.InProgress(LANGUAGE), + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEVICE_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEVICE_LANGUAGE), ) val privacyNoticeLiveData = privacyNoticeViewModel.viewState @@ -71,9 +73,9 @@ internal class PrivacyNoticeViewModelTest { @Test fun `retrievePrivacyNotice should return ContentAvailable when success received`() = runTest { - coEvery { configManager.getPrivacyNotice(PROJECT_ID, LANGUAGE) } returns flowOf( - PrivacyNoticeResult.InProgress(LANGUAGE), - PrivacyNoticeResult.Succeed(LANGUAGE, "some long consent"), + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEVICE_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEVICE_LANGUAGE), + PrivacyNoticeResult.Succeed(DEVICE_LANGUAGE, "some long consent"), ) val privacyNoticeLiveData = privacyNoticeViewModel.viewState @@ -84,24 +86,28 @@ internal class PrivacyNoticeViewModelTest { } @Test - fun `retrievePrivacyNotice should return ConsentNotAvailable when Failed received`() = runTest { - coEvery { configManager.getPrivacyNotice(PROJECT_ID, LANGUAGE) } returns flowOf( - PrivacyNoticeResult.InProgress(LANGUAGE), - PrivacyNoticeResult.Failed(LANGUAGE, Throwable()), + fun `retrievePrivacyNotice should attempt default language when Failed received with initial`() = runTest { + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEVICE_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEVICE_LANGUAGE), + PrivacyNoticeResult.Failed(DEVICE_LANGUAGE, Throwable()), + ) + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEFAULT_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEFAULT_LANGUAGE), + PrivacyNoticeResult.Succeed(DEFAULT_LANGUAGE, "some long consent"), ) val privacyNoticeLiveData = privacyNoticeViewModel.viewState privacyNoticeViewModel.retrievePrivacyNotice() val value = privacyNoticeLiveData.getOrAwaitValue() - Truth.assertThat(value).isInstanceOf(PrivacyNoticeState.ConsentNotAvailable::class.java) + Truth.assertThat(value).isInstanceOf(PrivacyNoticeState.ConsentAvailable::class.java) } @Test fun `retrievePrivacyNotice should return BackendMaintenance when FailedBecauseBackendMaintenance received`() = runTest { - coEvery { configManager.getPrivacyNotice(PROJECT_ID, LANGUAGE) } returns flowOf( - PrivacyNoticeResult.InProgress(LANGUAGE), - PrivacyNoticeResult.FailedBecauseBackendMaintenance(LANGUAGE, Throwable()), + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEVICE_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEVICE_LANGUAGE), + PrivacyNoticeResult.FailedBecauseBackendMaintenance(DEVICE_LANGUAGE, Throwable()), ) val privacyNoticeLiveData = privacyNoticeViewModel.viewState @@ -111,11 +117,30 @@ internal class PrivacyNoticeViewModelTest { Truth.assertThat(value).isInstanceOf(PrivacyNoticeState.BackendMaintenance::class.java) } + @Test + fun `retrievePrivacyNotice should return BackendMaintenance when FailedBecauseBackendMaintenance receivedon default language`() = + runTest { + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEVICE_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEVICE_LANGUAGE), + PrivacyNoticeResult.Failed(DEVICE_LANGUAGE, Throwable()), + ) + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEFAULT_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEFAULT_LANGUAGE), + PrivacyNoticeResult.FailedBecauseBackendMaintenance(DEVICE_LANGUAGE, Throwable()), + ) + + val privacyNoticeLiveData = privacyNoticeViewModel.viewState + privacyNoticeViewModel.retrievePrivacyNotice() + + val value = privacyNoticeLiveData.getOrAwaitValue() + Truth.assertThat(value).isInstanceOf(PrivacyNoticeState.BackendMaintenance::class.java) + } + @Test fun `retrievePrivacyNotice should return BackendMaintenance with estimation when FailedBecauseBackendMaintenance received`() = runTest { - coEvery { configManager.getPrivacyNotice(PROJECT_ID, LANGUAGE) } returns flowOf( - PrivacyNoticeResult.InProgress(LANGUAGE), - PrivacyNoticeResult.FailedBecauseBackendMaintenance(LANGUAGE, Throwable(), 1000L), + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEVICE_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress(DEVICE_LANGUAGE), + PrivacyNoticeResult.FailedBecauseBackendMaintenance(DEVICE_LANGUAGE, Throwable(), 1000L), ) val privacyNoticeLiveData = privacyNoticeViewModel.viewState @@ -129,7 +154,11 @@ internal class PrivacyNoticeViewModelTest { @Test fun `downloadPressed should retrieve notice when online`() = runTest { every { connectivityTracker.isConnected() } returns true - coEvery { configManager.getPrivacyNotice(PROJECT_ID, LANGUAGE) } returns flowOf(PrivacyNoticeResult.InProgress(LANGUAGE)) + coEvery { configManager.getPrivacyNotice(PROJECT_ID, DEVICE_LANGUAGE) } returns flowOf( + PrivacyNoticeResult.InProgress( + DEVICE_LANGUAGE, + ), + ) val privacyNoticeLiveData = privacyNoticeViewModel.viewState val showOfflineLiveData = privacyNoticeViewModel.showOffline From d5ef245b59ced5b81687fe42acd38103793a042f Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Mon, 24 Feb 2025 16:43:50 +0200 Subject: [PATCH 2/2] MS-882 Remove unnecessary suspend keyword --- .../consent/screens/privacy/PrivacyNoticeViewModel.kt | 2 +- .../simprints/infra/config/store/ConfigRepository.kt | 2 +- .../infra/config/store/ConfigRepositoryImpl.kt | 10 +++++----- .../com/simprints/infra/config/sync/ConfigManager.kt | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt b/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt index 9e7989d28d..03076a585d 100644 --- a/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt +++ b/feature/consent/src/main/java/com/simprints/feature/consent/screens/privacy/PrivacyNoticeViewModel.kt @@ -68,7 +68,7 @@ internal class PrivacyNoticeViewModel @Inject constructor( } } - private suspend fun attemptDownloadingLanguage( + private fun attemptDownloadingLanguage( projectId: String, deviceLanguage: String, ): Flow = configManager.getPrivacyNotice(projectId, deviceLanguage).map { it.toPrivacyNoticeViewState() } diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepository.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepository.kt index 0c0d97f498..0c763c2b79 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepository.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepository.kt @@ -25,7 +25,7 @@ interface ConfigRepository { suspend fun clearData() - suspend fun getPrivacyNotice( + fun getPrivacyNotice( projectId: String, language: String, ): Flow diff --git a/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepositoryImpl.kt b/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepositoryImpl.kt index 5bee1ec1c8..8f6a72f276 100644 --- a/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepositoryImpl.kt +++ b/infra/config-store/src/main/java/com/simprints/infra/config/store/ConfigRepositoryImpl.kt @@ -79,7 +79,7 @@ internal class ConfigRepositoryImpl @Inject constructor( localDataSource.deletePrivacyNotices() } - override suspend fun getPrivacyNotice( + override fun getPrivacyNotice( projectId: String, language: String, ): Flow = flow { @@ -102,14 +102,14 @@ internal class ConfigRepositoryImpl @Inject constructor( is TokenizableString.Raw -> tokenizationProcessor.encrypt( decrypted = moduleId, tokenKeyType = TokenKeyType.ModuleId, - project = project + project = project, ) is TokenizableString.Tokenized -> moduleId } - } - ) - ) + }, + ), + ), ) } diff --git a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt index 40e8287b89..7cabf1b198 100644 --- a/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt +++ b/infra/config-sync/src/main/java/com/simprints/infra/config/sync/ConfigManager.kt @@ -46,16 +46,16 @@ class ConfigManager @Inject constructor( } } - fun watchProjectConfiguration(): Flow = - configRepository.watchProjectConfiguration() - .onStart { getProjectConfiguration() } // to invoke download if empty + fun watchProjectConfiguration(): Flow = configRepository + .watchProjectConfiguration() + .onStart { getProjectConfiguration() } // to invoke download if empty suspend fun getDeviceConfiguration(): DeviceConfiguration = configRepository.getDeviceConfiguration() suspend fun updateDeviceConfiguration(update: suspend (t: DeviceConfiguration) -> DeviceConfiguration) = configRepository.updateDeviceConfiguration(update) - suspend fun getPrivacyNotice( + fun getPrivacyNotice( projectId: String, language: String, ): Flow = configRepository.getPrivacyNotice(