diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthScreen.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthScreen.kt index 5a2ead2..a60e67c 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthScreen.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthScreen.kt @@ -1,6 +1,9 @@ package com.android.trisolarispms.ui.auth import androidx.activity.ComponentActivity +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -48,6 +51,10 @@ fun AuthScreen(viewModel: AuthViewModel = viewModel()) { val phoneCountries = remember { phoneCountryOptions() } val phoneCountrySearch = remember { mutableStateOf("") } val now = remember { mutableStateOf(System.currentTimeMillis()) } + val hasNetwork = remember(context, state.error) { hasInternetConnection(context) } + val noNetworkError = state.error?.contains("No internet connection", ignoreCase = true) == true + val isCheckingExistingSession = state.isLoading && !state.apiVerified && state.userId != null + val shouldHideAuthForm = !hasNetwork || noNetworkError || isCheckingExistingSession LaunchedEffect(state.resendAvailableAt) { while (state.resendAvailableAt != null) { @@ -56,6 +63,30 @@ fun AuthScreen(viewModel: AuthViewModel = viewModel()) { } } + if (shouldHideAuthForm) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.Center + ) { + if (isCheckingExistingSession) { + Text(text = "Checking session...", style = MaterialTheme.typography.headlineSmall) + Spacer(modifier = Modifier.height(8.dp)) + CircularProgressIndicator() + } else { + Text(text = "No internet connection", style = MaterialTheme.typography.headlineSmall) + Spacer(modifier = Modifier.height(8.dp)) + Text(text = "Please connect to the internet and try again.") + } + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = viewModel::retryAfterConnectivityIssue) { + Text("Retry") + } + } + return + } + Column( modifier = Modifier .fillMaxSize() @@ -200,12 +231,6 @@ fun AuthScreen(viewModel: AuthViewModel = viewModel()) { } } - if (state.userId != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text(text = "Firebase user: ${state.userId}") - Text(text = "API verified: ${state.apiVerified}") - } - if (state.noProperties) { Spacer(modifier = Modifier.height(12.dp)) Text( @@ -215,3 +240,10 @@ fun AuthScreen(viewModel: AuthViewModel = viewModel()) { } } } + +private fun hasInternetConnection(context: Context): Boolean { + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager ?: return false + val network = cm.activeNetwork ?: return false + val capabilities = cm.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) +} diff --git a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt index 41f4d78..bf73a1f 100644 --- a/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt +++ b/app/src/main/java/com/android/trisolarispms/ui/auth/AuthViewModel.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import java.net.UnknownHostException import java.util.concurrent.TimeUnit class AuthViewModel( @@ -68,6 +69,15 @@ class AuthViewModel( _state.update { it.copy(error = null) } } + fun retryAfterConnectivityIssue() { + val currentUser = auth.currentUser + if (currentUser != null) { + verifyExistingSession(currentUser.uid) + } else { + clearError() + } + } + fun reportError(message: String) { setError(message) } @@ -96,7 +106,7 @@ class AuthViewModel( } override fun onVerificationFailed(e: FirebaseException) { - setError(e.localizedMessage ?: "Verification failed") + setError(mapThrowableToMessage(e, fallback = "Verification failed")) } override fun onCodeSent( @@ -168,7 +178,7 @@ class AuthViewModel( val response = api.verifyAuth() handleVerifyResponse(userId, response) } catch (e: Exception) { - setError(e.localizedMessage ?: "Sign-in failed") + setError(mapThrowableToMessage(e, fallback = "Sign-in failed")) } } } @@ -200,7 +210,7 @@ class AuthViewModel( val response = api.verifyAuth() handleVerifyResponse(userId, response) } catch (e: Exception) { - setError(e.localizedMessage ?: "Session verify failed") + setError(mapThrowableToMessage(e, fallback = "Session verify failed")) } } } @@ -251,7 +261,7 @@ class AuthViewModel( noProperties = false, unauthorized = false, propertyRoles = emptyMap(), - error = "API verify failed: ${response.code()}" + error = mapHttpError(response.code(), "API verify failed") ) } } @@ -309,10 +319,10 @@ class AuthViewModel( ) } } else { - setError("Update failed: ${response.code()}") + setError(mapHttpError(response.code(), "Update failed")) } } catch (e: Exception) { - setError(e.localizedMessage ?: "Update failed") + setError(mapThrowableToMessage(e, fallback = "Update failed")) } } } @@ -336,4 +346,21 @@ class AuthViewModel( } } } + + private fun mapHttpError(code: Int, prefix: String): String { + return if (code >= 500) { + "Server down. Please try again." + } else { + "$prefix: $code" + } + } + + private fun mapThrowableToMessage(throwable: Throwable, fallback: String): String { + return when { + throwable is UnknownHostException -> "No internet connection." + throwable.localizedMessage?.contains("Unable to resolve host", ignoreCase = true) == true -> + "No internet connection." + else -> throwable.localizedMessage ?: fallback + } + } }