From 9bec0538f36a0f1b543467143efe801dd8dbcbf2 Mon Sep 17 00:00:00 2001 From: andreia Date: Thu, 18 Jun 2026 18:08:50 +0200 Subject: [PATCH 1/3] add Error to TosUiState and trigger it when the terms of service fail to load when the screen isViewOnly=true --- .../android/ui/tos/TermsOfServiceScreen.kt | 81 +++++++++++++------ .../android/ui/tos/TermsOfServiceViewModel.kt | 15 +++- .../ui/tos/TermsOfServiceScreenTest.kt | 21 +++++ .../ui/tos/TermsOfServiceViewModelTest.kt | 31 ++++++- .../composeResources/values/strings.xml | 1 + 5 files changed, 121 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt index 8b227746bd..605464cc33 100644 --- a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt @@ -41,9 +41,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.retry import org.groundplatform.android.R import org.groundplatform.android.ui.common.ExcludeFromJacocoGeneratedReport import org.groundplatform.android.ui.components.Toolbar +import org.groundplatform.ui.theme.AppTheme +import org.jetbrains.compose.resources.stringResource @Composable fun TermsOfServiceScreen( @@ -69,6 +73,7 @@ fun TermsOfServiceScreen( onAgreeCheckedChange = { viewModel.setAgreeCheckboxChecked(it) }, onAgreeClick = { viewModel.onAgreeButtonClicked() }, onBackClick = onNavigateUp, + onRetry = { viewModel.onRetryClicked() }, termsContent = termsContent, ) } @@ -79,6 +84,7 @@ private fun TermsOfServiceContent( onAgreeCheckedChange: (Boolean) -> Unit, onAgreeClick: () -> Unit, onBackClick: () -> Unit, + onRetry: () -> Unit, termsContent: @Composable (String) -> Unit, ) { Scaffold( @@ -113,6 +119,11 @@ private fun TermsOfServiceContent( } } } + is TosUiState.Error -> { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Button(onClick = onRetry) { Text(text = stringResource(Res.string.retry)) } + } + } } } } @@ -145,34 +156,56 @@ private fun AgreeSection( @Composable @ExcludeFromJacocoGeneratedReport private fun TermsOfServiceContentPreview() { - TermsOfServiceContent( - uiState = - TosUiState.Success( - tosHtml = "Sample Terms of Service content.", - agreeChecked = false, - isViewOnly = false, - ), - onAgreeCheckedChange = {}, - onAgreeClick = {}, - onBackClick = {}, - termsContent = { Text("Sample Terms") }, - ) + AppTheme { + TermsOfServiceContent( + uiState = + TosUiState.Success( + tosHtml = "Sample Terms of Service content.", + agreeChecked = false, + isViewOnly = false, + ), + onAgreeCheckedChange = {}, + onAgreeClick = {}, + onBackClick = {}, + onRetry = {}, + termsContent = { Text("Sample Terms") }, + ) + } } @Preview(showBackground = true) @Composable @ExcludeFromJacocoGeneratedReport private fun TermsOfServiceContentIsViewOnlyPreview() { - TermsOfServiceContent( - uiState = - TosUiState.Success( - tosHtml = "Sample Terms of Service content.", - agreeChecked = false, - isViewOnly = true, - ), - onAgreeCheckedChange = {}, - onAgreeClick = {}, - onBackClick = {}, - termsContent = { Text("Sample Terms") }, - ) + AppTheme { + TermsOfServiceContent( + uiState = + TosUiState.Success( + tosHtml = "Sample Terms of Service content.", + agreeChecked = false, + isViewOnly = true, + ), + onAgreeCheckedChange = {}, + onAgreeClick = {}, + onBackClick = {}, + onRetry = {}, + termsContent = { Text("Sample Terms") }, + ) + } +} + +@Preview(showBackground = true) +@Composable +@ExcludeFromJacocoGeneratedReport +private fun TermsOfServiceContentErrorPreview() { + AppTheme { + TermsOfServiceContent( + uiState = TosUiState.Error(isViewOnly = true), + onAgreeCheckedChange = {}, + onAgreeClick = {}, + onBackClick = {}, + onRetry = {}, + termsContent = { Text("Sample Terms") }, + ) + } } diff --git a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt index 2b84951800..8c9a2bd6e6 100644 --- a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt @@ -46,6 +46,8 @@ sealed interface TosUiState { val agreeChecked: Boolean, override val isViewOnly: Boolean, ) : TosUiState + + data class Error(override val isViewOnly: Boolean) : TosUiState } sealed interface TosEvent { @@ -68,7 +70,7 @@ constructor( private val _uiState = MutableStateFlow(TosUiState.Loading(isViewOnly)) val uiState: StateFlow = _uiState.asStateFlow() - private val _events = Channel() + private val _events = Channel(Channel.BUFFERED) val events = _events.receiveAsFlow() init { @@ -92,8 +94,13 @@ constructor( } } + fun onRetryClicked() { + loadTermsOfService() + } + private fun loadTermsOfService() { viewModelScope.launch { + _uiState.value = TosUiState.Loading(isViewOnly) try { val tos = termsOfServiceRepository.getTermsOfService()?.text ?: "" val flavor = CommonMarkFlavourDescriptor() @@ -108,7 +115,11 @@ constructor( Timber.e(e, "Failed to load Terms of Service") } _events.send(TosEvent.LoadError) - authManager.signOut() + if (isViewOnly) { + _uiState.value = TosUiState.Error(isViewOnly) + } else { + authManager.signOut() + } } } } diff --git a/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceScreenTest.kt b/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceScreenTest.kt index ae0e924330..cc18c9f275 100644 --- a/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceScreenTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceScreenTest.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import ground_android.core.ui.generated.resources.Res +import ground_android.core.ui.generated.resources.retry import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.groundplatform.android.R @@ -141,6 +143,25 @@ class TermsOfServiceScreenTest { assert(errorOccurred) } + @Test + fun `View-only load failure shows error with retry and recovers on retry`() = runTest { + setupViewModel(Result.failure(Exception("Failed to load")), isViewOnly = true) + setScreenContent() + + composeTestRule.waitUntil(timeoutMillis = 5000) { + viewModel.uiState.value is TosUiState.Error + } + composeTestRule.onNodeWithText(getString(Res.string.retry)).assertIsDisplayed() + + fakeRepository.termsOfService = Result.success(TEST_TOS) + composeTestRule.onNodeWithText(getString(Res.string.retry)).performClick() + + composeTestRule.waitUntil(timeoutMillis = 5000) { + viewModel.uiState.value is TosUiState.Success + } + composeTestRule.onNode(hasText(TEST_TOS_TEXT, substring = true)).assertIsDisplayed() + } + @Test fun `Toolbar is displayed`() = runTest { setupViewModel() diff --git a/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt index c0564556f3..74780cd6fe 100644 --- a/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt @@ -32,6 +32,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -102,9 +103,9 @@ class TermsOfServiceViewModelTest { } @Test - fun `Load failure triggers LoadError event and signs out`() = runTest { + fun `Load failure signs out when not view-only`() = runTest { fakeRepository.termsOfService = Result.failure(Exception("Failed to load")) - setupViewModel() + setupViewModel(isViewOnly = false) viewModel.events.test { advanceUntilIdle() @@ -115,6 +116,32 @@ class TermsOfServiceViewModelTest { verify(authManager).signOut() } + @Test + fun `Load failure shows Error state, emits LoadError, and does not sign out when view-only`() = + runTest { + fakeRepository.termsOfService = Result.failure(Exception("Failed to load")) + setupViewModel(isViewOnly = true) + + assertThat(viewModel.uiState.value).isEqualTo(TosUiState.Error(isViewOnly = true)) + verify(authManager, never()).signOut() + viewModel.events.test { + assertThat(awaitItem()).isInstanceOf(TosEvent.LoadError::class.java) + } + } + + @Test + fun `onRetryClicked reloads terms and recovers from Error state`() = runTest { + fakeRepository.termsOfService = Result.failure(Exception("Failed to load")) + setupViewModel(isViewOnly = true) + assertThat(viewModel.uiState.value).isEqualTo(TosUiState.Error(isViewOnly = true)) + + fakeRepository.termsOfService = Result.success(TEST_TOS) + viewModel.onRetryClicked() + advanceUntilIdle() + + assertSuccessState(isViewOnly = true, TERMS_TEXT) + } + companion object { private const val TERMS_TEXT = "Terms content" private val TEST_TOS = TermsOfService("1", TERMS_TEXT) diff --git a/core/ui/src/commonMain/composeResources/values/strings.xml b/core/ui/src/commonMain/composeResources/values/strings.xml index 5b19452a85..b76a400f5f 100644 --- a/core/ui/src/commonMain/composeResources/values/strings.xml +++ b/core/ui/src/commonMain/composeResources/values/strings.xml @@ -33,4 +33,5 @@ Survey Job Data collector + Retry From b56427de1bd929090982e489ff1d96940a3fba1f Mon Sep 17 00:00:00 2001 From: andreia Date: Fri, 19 Jun 2026 11:11:54 +0200 Subject: [PATCH 2/3] add strings for 'retry' in all languages --- .../org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt | 2 +- core/ui/src/commonMain/composeResources/values-es/strings.xml | 1 + core/ui/src/commonMain/composeResources/values-fr/strings.xml | 1 + core/ui/src/commonMain/composeResources/values-km/strings.xml | 1 + core/ui/src/commonMain/composeResources/values-lo/strings.xml | 1 + core/ui/src/commonMain/composeResources/values-pt/strings.xml | 1 + core/ui/src/commonMain/composeResources/values-th/strings.xml | 1 + core/ui/src/commonMain/composeResources/values-vi/strings.xml | 1 + 8 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt index 605464cc33..82b0cbb183 100644 --- a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt @@ -205,7 +205,7 @@ private fun TermsOfServiceContentErrorPreview() { onAgreeClick = {}, onBackClick = {}, onRetry = {}, - termsContent = { Text("Sample Terms") }, + termsContent = { Text("Terms of service") }, ) } } diff --git a/core/ui/src/commonMain/composeResources/values-es/strings.xml b/core/ui/src/commonMain/composeResources/values-es/strings.xml index 20c08f2b4c..b4db70e8d1 100644 --- a/core/ui/src/commonMain/composeResources/values-es/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-es/strings.xml @@ -33,4 +33,5 @@ Encuesta Trabajo Recolector de datos + Reintentar diff --git a/core/ui/src/commonMain/composeResources/values-fr/strings.xml b/core/ui/src/commonMain/composeResources/values-fr/strings.xml index faa708a94a..732628967c 100644 --- a/core/ui/src/commonMain/composeResources/values-fr/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-fr/strings.xml @@ -32,4 +32,5 @@ Enquête Mission Collecteur de données + Réessayer diff --git a/core/ui/src/commonMain/composeResources/values-km/strings.xml b/core/ui/src/commonMain/composeResources/values-km/strings.xml index 30f0818d06..bfe966fe25 100644 --- a/core/ui/src/commonMain/composeResources/values-km/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-km/strings.xml @@ -32,4 +32,5 @@ ការស្ទង់មតិ ការងារ អ្នកប្រមូលទិន្នន័យ + ព្យាយាមឡើងវិញ \ No newline at end of file diff --git a/core/ui/src/commonMain/composeResources/values-lo/strings.xml b/core/ui/src/commonMain/composeResources/values-lo/strings.xml index 8d231efd3a..05e2bc2faf 100644 --- a/core/ui/src/commonMain/composeResources/values-lo/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-lo/strings.xml @@ -32,4 +32,5 @@ ແບບສຳຫຼວດ ພາລະກິດ ຜູ້ເກັບຂໍ້ມູນ + ລອງໃໝ່ diff --git a/core/ui/src/commonMain/composeResources/values-pt/strings.xml b/core/ui/src/commonMain/composeResources/values-pt/strings.xml index fb03d932f4..d27db3972b 100644 --- a/core/ui/src/commonMain/composeResources/values-pt/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-pt/strings.xml @@ -33,4 +33,5 @@ Inquérito Tarefa Coletor de dados + Tentar novamente diff --git a/core/ui/src/commonMain/composeResources/values-th/strings.xml b/core/ui/src/commonMain/composeResources/values-th/strings.xml index 64e8abec9a..8e91f73b57 100644 --- a/core/ui/src/commonMain/composeResources/values-th/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-th/strings.xml @@ -32,4 +32,5 @@ แบบสำรวจ งาน ผู้เก็บข้อมูล + ลองใหม่ diff --git a/core/ui/src/commonMain/composeResources/values-vi/strings.xml b/core/ui/src/commonMain/composeResources/values-vi/strings.xml index a6e9eb9712..bf2a818a24 100644 --- a/core/ui/src/commonMain/composeResources/values-vi/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-vi/strings.xml @@ -32,4 +32,5 @@ Khảo sát Công việc Người thu thập dữ liệu + Thử lại From 48b881d4c1fb3c12917d38011469dafd626b8ec9 Mon Sep 17 00:00:00 2001 From: andreia Date: Fri, 19 Jun 2026 12:15:22 +0200 Subject: [PATCH 3/3] add error message above the retry button and update tests --- .../android/ui/tos/TermsOfServiceScreen.kt | 13 ++++++++++++- .../android/ui/tos/TermsOfServiceViewModel.kt | 2 +- .../android/ui/tos/TermsOfServiceViewModelTest.kt | 3 --- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt index 82b0cbb183..c9176bf8de 100644 --- a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt +++ b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceScreen.kt @@ -16,6 +16,7 @@ package org.groundplatform.android.ui.tos 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.Row @@ -37,6 +38,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -120,7 +122,16 @@ private fun TermsOfServiceContent( } } is TosUiState.Error -> { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column( + modifier = Modifier.fillMaxSize().padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(R.string.load_tos_failed), + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(16.dp)) Button(onClick = onRetry) { Text(text = stringResource(Res.string.retry)) } } } diff --git a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt index 8c9a2bd6e6..cba7b00096 100644 --- a/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt +++ b/app/src/main/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModel.kt @@ -114,10 +114,10 @@ constructor( if (!e.isExpectedFailure()) { Timber.e(e, "Failed to load Terms of Service") } - _events.send(TosEvent.LoadError) if (isViewOnly) { _uiState.value = TosUiState.Error(isViewOnly) } else { + _events.send(TosEvent.LoadError) authManager.signOut() } } diff --git a/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt b/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt index 74780cd6fe..f2a18b09c2 100644 --- a/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt +++ b/app/src/test/java/org/groundplatform/android/ui/tos/TermsOfServiceViewModelTest.kt @@ -124,9 +124,6 @@ class TermsOfServiceViewModelTest { assertThat(viewModel.uiState.value).isEqualTo(TosUiState.Error(isViewOnly = true)) verify(authManager, never()).signOut() - viewModel.events.test { - assertThat(awaitItem()).isInstanceOf(TosEvent.LoadError::class.java) - } } @Test