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..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,13 +38,18 @@ 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 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 +75,7 @@ fun TermsOfServiceScreen( onAgreeCheckedChange = { viewModel.setAgreeCheckboxChecked(it) }, onAgreeClick = { viewModel.onAgreeButtonClicked() }, onBackClick = onNavigateUp, + onRetry = { viewModel.onRetryClicked() }, termsContent = termsContent, ) } @@ -79,6 +86,7 @@ private fun TermsOfServiceContent( onAgreeCheckedChange: (Boolean) -> Unit, onAgreeClick: () -> Unit, onBackClick: () -> Unit, + onRetry: () -> Unit, termsContent: @Composable (String) -> Unit, ) { Scaffold( @@ -113,6 +121,20 @@ private fun TermsOfServiceContent( } } } + is TosUiState.Error -> { + 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)) } + } + } } } } @@ -145,34 +167,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("Terms of service") }, + ) + } } 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..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 @@ -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() @@ -107,8 +114,12 @@ constructor( if (!e.isExpectedFailure()) { Timber.e(e, "Failed to load Terms of Service") } - _events.send(TosEvent.LoadError) - authManager.signOut() + 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/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..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 @@ -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,29 @@ 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() + } + + @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-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 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