Correctly working with correct routes
This commit is contained in:
parent
682ed78491
commit
88161f7933
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
){
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue