Correctly working with correct routes

This commit is contained in:
Chandresh Kerkar 2025-12-20 02:08:12 +05:30
parent 682ed78491
commit 88161f7933
11 changed files with 504 additions and 119 deletions

View File

@ -19,6 +19,12 @@ import kotlinx.serialization.json.Json
import java.util.Locale
import java.util.TimeZone
// Custom exception for user not found
class UserNotFoundException(
message: String,
val errorCode: String
) : Exception(message)
class AuthApiClient(private val context: Context) {
private val tokenManager = TokenManager(context)
@ -33,6 +39,7 @@ class AuthApiClient(private val context: Context) {
})
}
install(Auth) {
bearer {
loadTokens {
@ -68,6 +75,36 @@ class AuthApiClient(private val context: Context) {
// --- API Calls ---
suspend fun checkUser(phoneNumber: String): Result<CheckUserResponse> = runCatching {
val response = client.post("auth/check-user") {
contentType(ContentType.Application.Json)
setBody(CheckUserRequest(phoneNumber))
}
if (response.status.isSuccess()) {
// Success - parse as CheckUserResponse
response.body<CheckUserResponse>()
} else {
// Error - parse as ErrorResponse
val errorResponse = try {
response.body<ErrorResponse>()
} catch (e: Exception) {
// If parsing fails, create default error
ErrorResponse(
success = false,
error = "USER_NOT_FOUND",
message = "User is not registered. Please sign up to create a new account.",
userExists = false
)
}
throw UserNotFoundException(
message = errorResponse.message ?: "User is not registered. Please sign up to create a new account.",
errorCode = errorResponse.error ?: "USER_NOT_FOUND"
)
}
}
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> = runCatching {
client.post("auth/request-otp") {
contentType(ContentType.Application.Json)
@ -76,13 +113,23 @@ class AuthApiClient(private val context: Context) {
}
suspend fun verifyOtp(phoneNumber: String, code: String, deviceId: String): Result<VerifyOtpResponse> = runCatching {
val response: VerifyOtpResponse = client.post("auth/verify-otp") {
val response = client.post("auth/verify-otp") {
contentType(ContentType.Application.Json)
setBody(VerifyOtpRequest(phoneNumber, code.toInt(), deviceId, getDeviceInfo()))
}.body()
tokenManager.saveTokens(response.accessToken, response.refreshToken)
response
}
if (response.status.isSuccess()) {
val verifyResponse: VerifyOtpResponse = response.body()
tokenManager.saveTokens(verifyResponse.accessToken, verifyResponse.refreshToken)
verifyResponse
} else {
// Parse error response
val errorResponse: ErrorResponse = response.body()
throw UserNotFoundException(
message = errorResponse.message ?: "User not found",
errorCode = errorResponse.error ?: "USER_NOT_FOUND"
)
}
}
suspend fun signup(request: SignupRequest): Result<SignupResponse> = runCatching {

View File

@ -8,6 +8,10 @@ class AuthManager(
private val apiClient: AuthApiClient,
private val tokenManager: TokenManager
) {
suspend fun checkUser(phoneNumber: String): Result<CheckUserResponse> {
return apiClient.checkUser(phoneNumber)
}
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
return apiClient.requestOtp(phoneNumber)
}

View File

@ -28,7 +28,7 @@ class TokenManager(context: Context) {
prefs.edit()
.putString(KEY_ACCESS_TOKEN, accessToken)
.putString(KEY_REFRESH_TOKEN, refreshToken)
.apply()
.commit() // Use commit() instead of apply() to ensure tokens are saved synchronously
}
fun getAccessToken(): String? = prefs.getString(KEY_ACCESS_TOKEN, null)

View File

@ -118,3 +118,25 @@ data class UserDetails(
@Serializable
data class LogoutResponse(val ok: Boolean)
// endregion
// region: Error Responses
@Serializable
data class ErrorResponse(
val success: Boolean? = null,
val error: String? = null,
val message: String? = null,
@SerialName("user_exists") val userExists: Boolean? = null
)
// endregion
// region: User Check
@Serializable
data class CheckUserRequest(@SerialName("phone_number") val phoneNumber: String)
@Serializable
data class CheckUserResponse(
val success: Boolean,
val message: String? = null,
@SerialName("user_exists") val userExists: Boolean
)
// endregion

View File

@ -37,15 +37,152 @@ class MainViewModel(context: Context) : ViewModel() {
val userState = _userState.asStateFlow()
init {
checkAuthStatus()
// Immediately check if tokens exist (synchronous check)
val hasTokens = tokenManager.getAccessToken() != null && tokenManager.getRefreshToken() != null
if (hasTokens) {
// Tokens exist, validate them asynchronously
checkAuthStatus()
} else {
// No tokens, immediately set to unauthenticated
_authState.value = AuthState.Unauthenticated
}
}
/**
* Public method to refresh auth status after login/signup
* Call this after tokens are saved to update the auth state
*
* This method optimistically sets authState to Authenticated if tokens exist,
* then validates in the background. This ensures immediate navigation.
*
* IMPORTANT: After optimistic authentication, we only revert to Unauthenticated
* if there's a clear authentication failure (not network errors).
*/
fun refreshAuthStatus() {
checkAuthStatus()
viewModelScope.launch {
val accessToken = tokenManager.getAccessToken()
val refreshToken = tokenManager.getRefreshToken()
Log.d(TAG, "refreshAuthStatus: accessToken=${accessToken != null}, refreshToken=${refreshToken != null}")
if (accessToken != null && refreshToken != null) {
// Optimistically set to Authenticated for immediate navigation
// Then validate in background
Log.d(TAG, "Setting authState to Authenticated (optimistic)")
_authState.value = AuthState.Authenticated
// Validate tokens in background (this will only revert if there's a clear auth failure)
validateTokensOptimistic()
} else {
Log.d(TAG, "No tokens found, setting authState to Unauthenticated")
_authState.value = AuthState.Unauthenticated
}
}
}
/**
* Validates tokens after optimistic authentication.
* Only reverts authState if there's a clear authentication failure (not network errors).
* This prevents users from being logged out due to temporary network issues.
*/
private fun validateTokensOptimistic() {
viewModelScope.launch {
// Try to fetch user details first - Ktor's Auth plugin will auto-refresh if access token is expired
authApiClient.getUserDetails()
.onSuccess { userDetails ->
// Tokens are valid, user is authenticated
Log.d(TAG, "Token validation successful - user authenticated")
_authState.value = AuthState.Authenticated
_userState.value = UserState.Success(userDetails)
}
.onFailure { error ->
// Check if this is a network error or authentication error
val isNetworkError = error.message?.contains("Unable to resolve host", ignoreCase = true) == true
|| error.message?.contains("timeout", ignoreCase = true) == true
|| error.message?.contains("network", ignoreCase = true) == true
|| error.message?.contains("connection", ignoreCase = true) == true
|| error.message?.contains("SocketTimeoutException", ignoreCase = true) == true
|| error.message?.contains("ConnectException", ignoreCase = true) == true
|| error.message?.contains("UnknownHostException", ignoreCase = true) == true
if (isNetworkError) {
// Network error - keep optimistic authentication state
// User might be offline, tokens are still valid
Log.w(TAG, "Network error during token validation (keeping optimistic auth): ${error.message}")
_userState.value = UserState.Error("Network error. Please check your connection.")
// Keep authState as Authenticated - don't revert on network errors
return@launch
}
// Check if it's a clear authentication error (401, 403, or specific auth error messages)
val isAuthError = error.message?.contains("401", ignoreCase = true) == true
|| error.message?.contains("403", ignoreCase = true) == true
|| error.message?.contains("Unauthorized", ignoreCase = true) == true
|| error.message?.contains("Forbidden", ignoreCase = true) == true
|| error.message?.contains("Invalid token", ignoreCase = true) == true
|| error.message?.contains("expired token", ignoreCase = true) == true
|| error.message?.contains("Invalid refresh token", ignoreCase = true) == true
if (isAuthError) {
// Clear authentication error - try refresh as last resort
Log.d(TAG, "Authentication error detected, attempting token refresh: ${error.message}")
authApiClient.refreshToken()
.onSuccess { refreshResponse ->
// Refresh successful, now try fetching user details again
Log.d(TAG, "Token refresh successful, fetching user details")
authApiClient.getUserDetails()
.onSuccess { userDetails ->
_authState.value = AuthState.Authenticated
_userState.value = UserState.Success(userDetails)
}
.onFailure { fetchError ->
// Even after refresh, fetching failed - check if network error
val isFetchNetworkError = fetchError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|| fetchError.message?.contains("timeout", ignoreCase = true) == true
|| error.message?.contains("network", ignoreCase = true) == true
if (isFetchNetworkError) {
// Network error - keep optimistic auth
Log.w(TAG, "Network error after refresh (keeping optimistic auth): ${fetchError.message}")
_userState.value = UserState.Error("Network error. Please check your connection.")
// Keep authState as Authenticated
} else {
// Clear auth failure - revert to unauthenticated
Log.e(TAG, "Authentication failed after refresh: ${fetchError.message}")
tokenManager.clearTokens()
_authState.value = AuthState.Unauthenticated
_userState.value = UserState.Error("Session expired. Please sign in again.")
}
}
}
.onFailure { refreshError ->
// Check if refresh failed due to network or auth
val isRefreshNetworkError = refreshError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|| refreshError.message?.contains("timeout", ignoreCase = true) == true
|| refreshError.message?.contains("network", ignoreCase = true) == true
if (isRefreshNetworkError) {
// Network error - keep optimistic auth
Log.w(TAG, "Network error during refresh (keeping optimistic auth): ${refreshError.message}")
_userState.value = UserState.Error("Network error. Please check your connection.")
// Keep authState as Authenticated
} else {
// Clear auth failure - revert to unauthenticated
Log.e(TAG, "Token refresh failed (reverting auth): ${refreshError.message}")
tokenManager.clearTokens()
_authState.value = AuthState.Unauthenticated
_userState.value = UserState.Error("Session expired. Please sign in again.")
}
}
} else {
// Unknown error - be conservative and keep optimistic auth
// Don't log out user for unknown errors
Log.w(TAG, "Unknown error during validation (keeping optimistic auth): ${error.message}")
_userState.value = UserState.Error("Validation error: ${error.message}")
// Keep authState as Authenticated
}
}
}
}
private fun checkAuthStatus() {
@ -66,40 +203,94 @@ class MainViewModel(context: Context) : ViewModel() {
private fun validateTokens() {
viewModelScope.launch {
// Try to fetch user details - this will validate the access token
// If access token is expired, Ktor's Auth plugin will auto-refresh
// Try to fetch user details first - Ktor's Auth plugin will auto-refresh if access token is expired
authApiClient.getUserDetails()
.onSuccess { userDetails ->
// Tokens are valid, user is authenticated
Log.d(TAG, "User authenticated successfully")
_authState.value = AuthState.Authenticated
_userState.value = UserState.Success(userDetails)
}
.onFailure { error ->
// Check if this is a network error or authentication error
val isNetworkError = error.message?.contains("Unable to resolve host", ignoreCase = true) == true
|| error.message?.contains("timeout", ignoreCase = true) == true
|| error.message?.contains("network", ignoreCase = true) == true
|| error.message?.contains("connection", ignoreCase = true) == true
if (isNetworkError) {
// Network error - don't clear tokens, don't change auth state
// User might be offline, tokens are still valid
// Keep current auth state (Unknown) so navigation doesn't change
Log.w(TAG, "Network error during token validation: ${error.message}")
_userState.value = UserState.Error("Network error. Please check your connection.")
// Don't change auth state - keep it as Unknown so we can retry later
// Don't clear tokens - they might still be valid
return@launch
}
// If fetching user details failed, try manual refresh
Log.d(TAG, "Failed to fetch user details, attempting token refresh: ${error.message}")
attemptTokenRefresh()
// This handles cases where Ktor's auto-refresh might not have worked
Log.d(TAG, "Failed to fetch user details (${error.message}), attempting manual token refresh")
// Try manual refresh as fallback
authApiClient.refreshToken()
.onSuccess { refreshResponse ->
// Refresh successful, now try fetching user details again
Log.d(TAG, "Token refresh successful, fetching user details")
authApiClient.getUserDetails()
.onSuccess { userDetails ->
_authState.value = AuthState.Authenticated
_userState.value = UserState.Success(userDetails)
}
.onFailure { fetchError ->
// Check if this is also a network error
val isFetchNetworkError = fetchError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|| fetchError.message?.contains("timeout", ignoreCase = true) == true
|| fetchError.message?.contains("network", ignoreCase = true) == true
if (isFetchNetworkError) {
// Network error - don't clear tokens, keep auth state
Log.w(TAG, "Network error after refresh: ${fetchError.message}")
_userState.value = UserState.Error("Network error. Please check your connection.")
// Don't change auth state - keep it as Unknown
} else {
// Even after refresh, fetching user details failed - likely auth error
Log.e(TAG, "Failed to fetch user details after refresh: ${fetchError.message}")
tokenManager.clearTokens()
_authState.value = AuthState.Unauthenticated
_userState.value = UserState.Error("Session expired. Please sign in again.")
}
}
}
.onFailure { refreshError ->
// Check if refresh failed due to network or auth
val isRefreshNetworkError = refreshError.message?.contains("Unable to resolve host", ignoreCase = true) == true
|| refreshError.message?.contains("timeout", ignoreCase = true) == true
|| refreshError.message?.contains("network", ignoreCase = true) == true
if (isRefreshNetworkError) {
// Network error - don't clear tokens, keep auth state
Log.w(TAG, "Network error during token refresh: ${refreshError.message}")
_userState.value = UserState.Error("Network error. Please check your connection.")
// Don't change auth state - keep it as Unknown
} else {
// Refresh failed - tokens are invalid or expired (auth error)
Log.d(TAG, "Token refresh failed (auth error): ${refreshError.message}")
tokenManager.clearTokens()
_authState.value = AuthState.Unauthenticated
_userState.value = UserState.Error("Session expired. Please sign in again.")
}
}
}
}
}
// This method is no longer needed as validateTokens() now handles refresh directly
// Keeping it for backward compatibility but it's not used
@Deprecated("Use validateTokens() instead")
private fun attemptTokenRefresh() {
viewModelScope.launch {
authApiClient.refreshToken()
.onSuccess { refreshResponse ->
// Refresh successful, tokens are valid
Log.d(TAG, "Token refresh successful")
_authState.value = AuthState.Authenticated
// Fetch user details with new token
fetchUserDetails()
}
.onFailure { error ->
// Refresh failed, tokens are invalid - clear and logout
Log.d(TAG, "Token refresh failed: ${error.message}")
tokenManager.clearTokens()
_authState.value = AuthState.Unauthenticated
_userState.value = UserState.Error("Session expired. Please sign in again.")
}
}
validateTokens()
}
fun fetchUserDetails() {

View File

@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.NavType
import androidx.navigation.navArgument
import kotlinx.coroutines.delay
import com.example.livingai_lg.ui.AuthState
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
import com.example.livingai_lg.ui.screens.BuyScreen
@ -117,44 +118,81 @@ fun AppNavigation(
) {
val navController = rememberNavController()
// Always start with AUTH (landing screen) - this ensures LandingScreen opens first
// We'll navigate to MAIN only if user is authenticated (handled by LaunchedEffect)
// Determine start destination based on initial auth state
// This prevents showing landing screen when user is already logged in
val startDestination = remember(authState) {
when (authState) {
is AuthState.Authenticated -> Graph.MAIN
is AuthState.Unauthenticated -> Graph.AUTH
is AuthState.Unknown -> Graph.AUTH // Show landing while checking
}
}
NavHost(
navController = navController,
startDestination = Graph.AUTH
startDestination = startDestination
) {
authNavGraph(navController, mainViewModel)
mainNavGraph(navController)
}
// Handle navigation based on auth state
// Handle navigation based on auth state changes
LaunchedEffect(authState) {
android.util.Log.d("AppNavigation", "LaunchedEffect triggered with authState: $authState")
when (authState) {
is AuthState.Authenticated -> {
// User is authenticated, navigate to main graph
// User is authenticated, navigate to ChooseServiceScreen
// Add a small delay to ensure NavHost graphs are fully built
delay(100)
val currentRoute = navController.currentBackStackEntry?.destination?.route
// Only navigate if we're not already in the MAIN graph
android.util.Log.d("AppNavigation", "Authenticated - currentRoute: $currentRoute")
// Only navigate if we're not already in the MAIN graph or ChooseServiceScreen
if (currentRoute?.startsWith(Graph.MAIN) != true &&
currentRoute?.startsWith(Graph.AUTH) == true) {
navController.navigate(Graph.MAIN) {
// Clear back stack to prevent going back to auth screens
popUpTo(Graph.AUTH) { inclusive = true }
currentRoute?.startsWith(AppScreen.CHOOSE_SERVICE) != true) {
android.util.Log.d("AppNavigation", "Navigating to ChooseServiceScreen (default profileId: 1)")
try {
// Navigate directly to the start destination route of MAIN graph
// This avoids the "Sequence is empty" error when navigating to Graph.MAIN
navController.navigate(AppScreen.chooseService("1")) {
// Clear back stack to prevent going back to auth screens
popUpTo(Graph.AUTH) { inclusive = true }
// Prevent multiple navigations
launchSingleTop = true
}
} catch (e: Exception) {
android.util.Log.e("AppNavigation", "Navigation error: ${e.message}", e)
// Fallback: try navigating to Graph.MAIN if direct route fails
try {
navController.navigate(Graph.MAIN) {
popUpTo(Graph.AUTH) { inclusive = true }
launchSingleTop = true
}
} catch (e2: Exception) {
android.util.Log.e("AppNavigation", "Fallback navigation also failed: ${e2.message}", e2)
}
}
} else {
android.util.Log.d("AppNavigation", "Already in MAIN graph or ChooseServiceScreen, skipping navigation")
}
}
is AuthState.Unauthenticated -> {
// User is not authenticated, ensure we're in auth graph (landing screen)
val currentRoute = navController.currentBackStackEntry?.destination?.route
if (currentRoute?.startsWith(Graph.MAIN) == true) {
if (currentRoute?.startsWith(Graph.MAIN) == true ||
currentRoute?.startsWith(Graph.AUTH) != true) {
navController.navigate(Graph.AUTH) {
// Clear back stack to prevent going back to main screens
popUpTo(0) { inclusive = true }
launchSingleTop = true
}
}
}
is AuthState.Unknown -> {
// Still checking auth status, stay on landing screen
// Don't navigate anywhere yet
// Still checking auth status
// If we're on landing screen, stay there
// If we're on main screen and checking, don't navigate yet
// This prevents flickering during token validation
}
}
}

View File

@ -14,6 +14,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
// Note: CoroutineScope, Dispatchers, delay, launch still used in onCreateProfile callbacks
import com.example.livingai_lg.ui.screens.SaleArchiveScreen
import com.example.livingai_lg.ui.screens.auth.LandingScreen
import com.example.livingai_lg.ui.screens.auth.OtpScreen
@ -107,26 +108,14 @@ fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: co
}
},
onSuccess = {
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
// Navigate to main graph first without popping, then navigate to specific route
android.util.Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
try {
// Navigate to main graph (this will use its start destination)
navController.navigate(Graph.MAIN) {
// Don't pop the AUTH graph yet - keep the graph structure
// Clear back stack to prevent going back to auth screens
popUpTo(Graph.AUTH) { inclusive = true }
launchSingleTop = true
}
// Then navigate to the specific route after a short delay
CoroutineScope(Dispatchers.Main).launch {
delay(200)
try {
navController.navigate(AppScreen.chooseService("1")) {
// Now pop the AUTH graph after we're in the MAIN graph
popUpTo(Graph.AUTH) { inclusive = true }
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
}
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
}
@ -190,26 +179,14 @@ fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: co
}
},
onSuccess = {
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
// Navigate to main graph first without popping, then navigate to specific route
android.util.Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
try {
// Navigate to main graph (this will use its start destination)
navController.navigate(Graph.MAIN) {
// Don't pop the AUTH graph yet - keep the graph structure
// Clear back stack to prevent going back to auth screens
popUpTo(Graph.AUTH) { inclusive = true }
launchSingleTop = true
}
// Then navigate to the specific route after a short delay
CoroutineScope(Dispatchers.Main).launch {
delay(200)
try {
navController.navigate(AppScreen.chooseService("1")) {
// Now pop the AUTH graph after we're in the MAIN graph
popUpTo(Graph.AUTH) { inclusive = true }
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
}
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
}

View File

@ -47,7 +47,7 @@ fun NavGraphBuilder.mainNavGraph(navController: NavController) {
navigation(
route = Graph.MAIN,
startDestination = AppScreen.BUY_ANIMALS
startDestination = AppScreen.chooseService("1") // Default to ChooseServiceScreen for authenticated users
){

View File

@ -29,6 +29,7 @@ import com.example.livingai_lg.api.AuthApiClient
import com.example.livingai_lg.api.AuthManager
import com.example.livingai_lg.api.SignupRequest
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.api.UserNotFoundException
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
import kotlin.math.min
import androidx.compose.ui.focus.FocusRequester
@ -42,6 +43,7 @@ import kotlinx.coroutines.launch
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
import kotlinx.coroutines.delay
@Composable
@ -61,6 +63,22 @@ fun OtpScreen(
val context = LocalContext.current.applicationContext
val scope = rememberCoroutineScope()
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
// Countdown timer state (2 minutes = 120 seconds)
var countdownSeconds by remember { mutableStateOf(120) }
// Start countdown when screen is composed
LaunchedEffect(Unit) {
while (countdownSeconds > 0) {
delay(1000) // Wait 1 second
countdownSeconds--
}
}
// Format countdown as MM:SS
val minutes = countdownSeconds / 60
val seconds = countdownSeconds % 60
val countdownText = String.format("%02d:%02d", minutes, seconds)
// Flag to determine if this is a sign-in flow for an existing user.
val isSignInFlow = name == "existing_user"
@ -131,6 +149,28 @@ Column(
onOtpChange = { if (it.length <= 6) otp.value = it }
)
}
// ---------------------------
// Countdown Timer
// ---------------------------
Text(
text = countdownText,
color = Color(0xFF927B5E),
fontSize = fs(16f),
fontWeight = FontWeight.Medium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
textAlign = TextAlign.Center,
style = LocalTextStyle.current.copy(
shadow = Shadow(
color = Color.Black.copy(alpha = 0.15f),
offset = Offset(0f, s(2f).value),
blurRadius = s(2f).value
)
)
)
// ---------------------------
// Continue Button
// ---------------------------
@ -198,13 +238,15 @@ Column(
// Check if profile needs completion
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
mainViewModel.refreshAuthStatus()
try {
if (needsProfile) {
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
onCreateProfile(name)
} else {
android.util.Log.d("OTPScreen", "Navigating to success screen")
onSuccess()
// Don't manually navigate - let AppNavigation handle it
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
}
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
@ -215,13 +257,15 @@ Column(
// Navigate to success anyway since verify-otp succeeded
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
val needsProfile = verifyResponse.needsProfile
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
mainViewModel.refreshAuthStatus()
try {
if (needsProfile) {
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
onCreateProfile(name)
} else {
android.util.Log.d("OTPScreen", "Navigating to success screen")
onSuccess()
// Don't manually navigate - let AppNavigation handle it
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
}
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
@ -242,6 +286,8 @@ Column(
// For existing users, use existing logic
val needsProfile = verifyResponse.needsProfile
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$needsProfile")
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
mainViewModel.refreshAuthStatus()
try {
// If this is a signup flow and signup failed, treat as new user
if (isSignupFlow) {
@ -251,8 +297,8 @@ Column(
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
onCreateProfile(name)
} else {
android.util.Log.d("OTPScreen", "Navigating to success screen")
onSuccess()
// Don't manually navigate - let AppNavigation handle it
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
}
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
@ -276,35 +322,37 @@ Column(
authManager.login(phoneNumber, otp.value)
.onSuccess { response ->
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
// Refresh auth status in MainViewModel so AppNavigation knows user is authenticated
// Tokens are now saved (synchronously via commit())
// Refresh auth status - this will optimistically set authState to Authenticated
// The LaunchedEffect in AppNavigation will automatically navigate to ChooseServiceScreen
mainViewModel.refreshAuthStatus()
try {
if (isSignInFlow) {
// For existing users, always go to the success screen.
android.util.Log.d("OTPScreen", "Existing user - navigating to success")
onSuccess()
} else {
// For new users, check if a profile needs to be created.
if (response.needsProfile) {
android.util.Log.d("OTPScreen", "New user needs profile - navigating to create profile with name: $name")
onCreateProfile(name)
} else {
android.util.Log.d("OTPScreen", "New user - navigating to success")
onSuccess()
}
}
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
}
android.util.Log.d("OTPScreen", "Auth status refreshed - navigation will happen automatically via LaunchedEffect")
}
.onFailure { error ->
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
Toast.makeText(
context,
"Invalid or expired OTP",
Toast.LENGTH_SHORT
).show()
// Check if user not found - redirect to signup
if (error is UserNotFoundException && error.errorCode == "USER_NOT_FOUND") {
android.util.Log.d("OTPScreen", "User not found - redirecting to signup")
Toast.makeText(
context,
error.message ?: "Account not found. Please sign up to create a new account.",
Toast.LENGTH_LONG
).show()
// Navigate back to landing page so user can choose signup
try {
onLanding()
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
}
} else {
// Other errors (invalid OTP, expired, etc.)
Toast.makeText(
context,
error.message ?: "Invalid or expired OTP",
Toast.LENGTH_SHORT
).show()
}
}
}
}

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.unit.sp
import com.example.livingai_lg.api.AuthApiClient
import com.example.livingai_lg.api.AuthManager
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.api.UserNotFoundException
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
import com.example.livingai_lg.ui.components.PhoneNumberInput
@ -96,12 +97,34 @@ fun SignInScreen(
onClick = {
val fullPhoneNumber = "+91${phoneNumber.value}"
scope.launch {
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
onSignInClick(fullPhoneNumber,"existing_user")
// First check if user exists before requesting OTP
authManager.checkUser(fullPhoneNumber)
.onSuccess { checkResponse ->
if (checkResponse.userExists) {
// User exists, proceed to request OTP
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
onSignInClick(fullPhoneNumber, "existing_user")
}
.onFailure {
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
}
} else {
// User doesn't exist (shouldn't happen if API works correctly)
Toast.makeText(context, "User is not registered. Please sign up.", Toast.LENGTH_LONG).show()
}
}
.onFailure {
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
.onFailure { error ->
// User not found or other error
if (error is UserNotFoundException) {
Toast.makeText(
context,
error.message ?: "User is not registered. Please sign up to create a new account.",
Toast.LENGTH_LONG
).show()
} else {
Toast.makeText(context, "Error: ${error.message}", Toast.LENGTH_LONG).show()
}
}
}
},

View File

@ -169,15 +169,50 @@ fun SignUpScreen(
onClick = {
scope.launch {
val fullPhoneNumber = "+91${formData.phoneNumber}"
// Request OTP first before allowing signup
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
// OTP sent successfully, navigate to OTP screen with signup data
// Pass signup form data through the callback
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
// First check if user already exists
authManager.checkUser(fullPhoneNumber)
.onSuccess { checkResponse ->
if (checkResponse.userExists) {
// User already registered - show message to sign in instead
Toast.makeText(
context,
"This phone number is already registered. Please sign in instead.",
Toast.LENGTH_LONG
).show()
// Optionally navigate to sign in screen
onSignInClick()
} else {
// User doesn't exist - proceed with signup
// Request OTP first before allowing signup
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
// OTP sent successfully, navigate to OTP screen with signup data
// Pass signup form data through the callback
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
}
.onFailure {
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
}
}
}
.onFailure {
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
.onFailure { checkError ->
// If check fails, it might be a network error or user doesn't exist
// Try to proceed with signup (user might not exist)
if (checkError is com.example.livingai_lg.api.UserNotFoundException) {
// User doesn't exist - proceed with signup
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
// OTP sent successfully, navigate to OTP screen with signup data
onSignUpClick(fullPhoneNumber, formData.name, formData.state, formData.district, formData.village)
}
.onFailure {
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
}
} else {
// Other error (network, etc.)
Toast.makeText(context, "Unable to verify phone number: ${checkError.message}", Toast.LENGTH_LONG).show()
}
}
}
},