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.Locale
|
||||||
import java.util.TimeZone
|
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) {
|
class AuthApiClient(private val context: Context) {
|
||||||
|
|
||||||
private val tokenManager = TokenManager(context)
|
private val tokenManager = TokenManager(context)
|
||||||
|
|
@ -33,6 +39,7 @@ class AuthApiClient(private val context: Context) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
install(Auth) {
|
install(Auth) {
|
||||||
bearer {
|
bearer {
|
||||||
loadTokens {
|
loadTokens {
|
||||||
|
|
@ -68,6 +75,36 @@ class AuthApiClient(private val context: Context) {
|
||||||
|
|
||||||
// --- API Calls ---
|
// --- 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 {
|
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> = runCatching {
|
||||||
client.post("auth/request-otp") {
|
client.post("auth/request-otp") {
|
||||||
contentType(ContentType.Application.Json)
|
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 {
|
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)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(VerifyOtpRequest(phoneNumber, code.toInt(), deviceId, getDeviceInfo()))
|
setBody(VerifyOtpRequest(phoneNumber, code.toInt(), deviceId, getDeviceInfo()))
|
||||||
}.body()
|
}
|
||||||
|
|
||||||
tokenManager.saveTokens(response.accessToken, response.refreshToken)
|
if (response.status.isSuccess()) {
|
||||||
response
|
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 {
|
suspend fun signup(request: SignupRequest): Result<SignupResponse> = runCatching {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ class AuthManager(
|
||||||
private val apiClient: AuthApiClient,
|
private val apiClient: AuthApiClient,
|
||||||
private val tokenManager: TokenManager
|
private val tokenManager: TokenManager
|
||||||
) {
|
) {
|
||||||
|
suspend fun checkUser(phoneNumber: String): Result<CheckUserResponse> {
|
||||||
|
return apiClient.checkUser(phoneNumber)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
|
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
|
||||||
return apiClient.requestOtp(phoneNumber)
|
return apiClient.requestOtp(phoneNumber)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class TokenManager(context: Context) {
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(KEY_ACCESS_TOKEN, accessToken)
|
.putString(KEY_ACCESS_TOKEN, accessToken)
|
||||||
.putString(KEY_REFRESH_TOKEN, refreshToken)
|
.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)
|
fun getAccessToken(): String? = prefs.getString(KEY_ACCESS_TOKEN, null)
|
||||||
|
|
|
||||||
|
|
@ -118,3 +118,25 @@ data class UserDetails(
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LogoutResponse(val ok: Boolean)
|
data class LogoutResponse(val ok: Boolean)
|
||||||
// endregion
|
// 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()
|
val userState = _userState.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
// Immediately check if tokens exist (synchronous check)
|
||||||
|
val hasTokens = tokenManager.getAccessToken() != null && tokenManager.getRefreshToken() != null
|
||||||
|
if (hasTokens) {
|
||||||
|
// Tokens exist, validate them asynchronously
|
||||||
checkAuthStatus()
|
checkAuthStatus()
|
||||||
|
} else {
|
||||||
|
// No tokens, immediately set to unauthenticated
|
||||||
|
_authState.value = AuthState.Unauthenticated
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public method to refresh auth status after login/signup
|
* Public method to refresh auth status after login/signup
|
||||||
* Call this after tokens are saved to update the auth state
|
* 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() {
|
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() {
|
private fun checkAuthStatus() {
|
||||||
|
|
@ -66,41 +203,95 @@ class MainViewModel(context: Context) : ViewModel() {
|
||||||
|
|
||||||
private fun validateTokens() {
|
private fun validateTokens() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// Try to fetch user details - this will validate the access token
|
// Try to fetch user details first - Ktor's Auth plugin will auto-refresh if access token is expired
|
||||||
// If access token is expired, Ktor's Auth plugin will auto-refresh
|
|
||||||
authApiClient.getUserDetails()
|
authApiClient.getUserDetails()
|
||||||
.onSuccess { userDetails ->
|
.onSuccess { userDetails ->
|
||||||
// Tokens are valid, user is authenticated
|
// Tokens are valid, user is authenticated
|
||||||
|
Log.d(TAG, "User authenticated successfully")
|
||||||
_authState.value = AuthState.Authenticated
|
_authState.value = AuthState.Authenticated
|
||||||
_userState.value = UserState.Success(userDetails)
|
_userState.value = UserState.Success(userDetails)
|
||||||
}
|
}
|
||||||
.onFailure { error ->
|
.onFailure { error ->
|
||||||
// If fetching user details failed, try manual refresh
|
// Check if this is a network error or authentication error
|
||||||
Log.d(TAG, "Failed to fetch user details, attempting token refresh: ${error.message}")
|
val isNetworkError = error.message?.contains("Unable to resolve host", ignoreCase = true) == true
|
||||||
attemptTokenRefresh()
|
|| 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
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attemptTokenRefresh() {
|
// If fetching user details failed, try manual refresh
|
||||||
viewModelScope.launch {
|
// 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()
|
authApiClient.refreshToken()
|
||||||
.onSuccess { refreshResponse ->
|
.onSuccess { refreshResponse ->
|
||||||
// Refresh successful, tokens are valid
|
// Refresh successful, now try fetching user details again
|
||||||
Log.d(TAG, "Token refresh successful")
|
Log.d(TAG, "Token refresh successful, fetching user details")
|
||||||
|
authApiClient.getUserDetails()
|
||||||
|
.onSuccess { userDetails ->
|
||||||
_authState.value = AuthState.Authenticated
|
_authState.value = AuthState.Authenticated
|
||||||
// Fetch user details with new token
|
_userState.value = UserState.Success(userDetails)
|
||||||
fetchUserDetails()
|
|
||||||
}
|
}
|
||||||
.onFailure { error ->
|
.onFailure { fetchError ->
|
||||||
// Refresh failed, tokens are invalid - clear and logout
|
// Check if this is also a network error
|
||||||
Log.d(TAG, "Token refresh failed: ${error.message}")
|
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()
|
tokenManager.clearTokens()
|
||||||
_authState.value = AuthState.Unauthenticated
|
_authState.value = AuthState.Unauthenticated
|
||||||
_userState.value = UserState.Error("Session expired. Please sign in again.")
|
_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() {
|
||||||
|
validateTokens()
|
||||||
|
}
|
||||||
|
|
||||||
fun fetchUserDetails() {
|
fun fetchUserDetails() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.NavType
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import com.example.livingai_lg.ui.AuthState
|
import com.example.livingai_lg.ui.AuthState
|
||||||
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
|
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
|
||||||
import com.example.livingai_lg.ui.screens.BuyScreen
|
import com.example.livingai_lg.ui.screens.BuyScreen
|
||||||
|
|
@ -117,44 +118,81 @@ fun AppNavigation(
|
||||||
) {
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
// Always start with AUTH (landing screen) - this ensures LandingScreen opens first
|
// Determine start destination based on initial auth state
|
||||||
// We'll navigate to MAIN only if user is authenticated (handled by LaunchedEffect)
|
// 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(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Graph.AUTH
|
startDestination = startDestination
|
||||||
) {
|
) {
|
||||||
authNavGraph(navController, mainViewModel)
|
authNavGraph(navController, mainViewModel)
|
||||||
mainNavGraph(navController)
|
mainNavGraph(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation based on auth state
|
// Handle navigation based on auth state changes
|
||||||
LaunchedEffect(authState) {
|
LaunchedEffect(authState) {
|
||||||
|
android.util.Log.d("AppNavigation", "LaunchedEffect triggered with authState: $authState")
|
||||||
when (authState) {
|
when (authState) {
|
||||||
is AuthState.Authenticated -> {
|
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
|
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 &&
|
if (currentRoute?.startsWith(Graph.MAIN) != true &&
|
||||||
currentRoute?.startsWith(Graph.AUTH) == true) {
|
currentRoute?.startsWith(AppScreen.CHOOSE_SERVICE) != true) {
|
||||||
navController.navigate(Graph.MAIN) {
|
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
|
// Clear back stack to prevent going back to auth screens
|
||||||
popUpTo(Graph.AUTH) { inclusive = true }
|
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 -> {
|
is AuthState.Unauthenticated -> {
|
||||||
// User is not authenticated, ensure we're in auth graph (landing screen)
|
// User is not authenticated, ensure we're in auth graph (landing screen)
|
||||||
val currentRoute = navController.currentBackStackEntry?.destination?.route
|
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) {
|
navController.navigate(Graph.AUTH) {
|
||||||
// Clear back stack to prevent going back to main screens
|
// Clear back stack to prevent going back to main screens
|
||||||
popUpTo(0) { inclusive = true }
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AuthState.Unknown -> {
|
is AuthState.Unknown -> {
|
||||||
// Still checking auth status, stay on landing screen
|
// Still checking auth status
|
||||||
// Don't navigate anywhere yet
|
// 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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
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.SaleArchiveScreen
|
||||||
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
import com.example.livingai_lg.ui.screens.auth.LandingScreen
|
||||||
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
import com.example.livingai_lg.ui.screens.auth.OtpScreen
|
||||||
|
|
@ -107,25 +108,13 @@ fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: co
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
|
android.util.Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||||
// Navigate to main graph first without popping, then navigate to specific route
|
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||||
try {
|
try {
|
||||||
// Navigate to main graph (this will use its start destination)
|
|
||||||
navController.navigate(Graph.MAIN) {
|
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
|
||||||
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 }
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
}
|
launchSingleTop = true
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||||
|
|
@ -190,25 +179,13 @@ fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: co
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
|
android.util.Log.d("AuthNavGraph", "Sign-in successful - navigating to ChooseServiceScreen")
|
||||||
// Navigate to main graph first without popping, then navigate to specific route
|
// Navigate to MAIN graph which starts at ChooseServiceScreen (startDestination)
|
||||||
try {
|
try {
|
||||||
// Navigate to main graph (this will use its start destination)
|
|
||||||
navController.navigate(Graph.MAIN) {
|
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
|
||||||
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 }
|
popUpTo(Graph.AUTH) { inclusive = true }
|
||||||
}
|
launchSingleTop = true
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ fun NavGraphBuilder.mainNavGraph(navController: NavController) {
|
||||||
|
|
||||||
navigation(
|
navigation(
|
||||||
route = Graph.MAIN,
|
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.AuthManager
|
||||||
import com.example.livingai_lg.api.SignupRequest
|
import com.example.livingai_lg.api.SignupRequest
|
||||||
import com.example.livingai_lg.api.TokenManager
|
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.backgrounds.DecorativeBackground
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
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.Key
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -62,6 +64,22 @@ fun OtpScreen(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
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.
|
// Flag to determine if this is a sign-in flow for an existing user.
|
||||||
val isSignInFlow = name == "existing_user"
|
val isSignInFlow = name == "existing_user"
|
||||||
// Flag to determine if this is a signup flow (has signup data)
|
// Flag to determine if this is a signup flow (has signup data)
|
||||||
|
|
@ -131,6 +149,28 @@ Column(
|
||||||
onOtpChange = { if (it.length <= 6) otp.value = it }
|
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
|
// Continue Button
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
@ -198,13 +238,15 @@ Column(
|
||||||
// Check if profile needs completion
|
// Check if profile needs completion
|
||||||
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
|
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
|
||||||
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
|
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
|
||||||
|
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
|
||||||
|
mainViewModel.refreshAuthStatus()
|
||||||
try {
|
try {
|
||||||
if (needsProfile) {
|
if (needsProfile) {
|
||||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
onCreateProfile(name)
|
onCreateProfile(name)
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.d("OTPScreen", "Navigating to success screen")
|
// Don't manually navigate - let AppNavigation handle it
|
||||||
onSuccess()
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
|
@ -215,13 +257,15 @@ Column(
|
||||||
// Navigate to success anyway since verify-otp succeeded
|
// Navigate to success anyway since verify-otp succeeded
|
||||||
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
|
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
|
||||||
val needsProfile = verifyResponse.needsProfile
|
val needsProfile = verifyResponse.needsProfile
|
||||||
|
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
|
||||||
|
mainViewModel.refreshAuthStatus()
|
||||||
try {
|
try {
|
||||||
if (needsProfile) {
|
if (needsProfile) {
|
||||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
onCreateProfile(name)
|
onCreateProfile(name)
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.d("OTPScreen", "Navigating to success screen")
|
// Don't manually navigate - let AppNavigation handle it
|
||||||
onSuccess()
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
|
@ -242,6 +286,8 @@ Column(
|
||||||
// For existing users, use existing logic
|
// For existing users, use existing logic
|
||||||
val needsProfile = verifyResponse.needsProfile
|
val needsProfile = verifyResponse.needsProfile
|
||||||
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$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 {
|
try {
|
||||||
// If this is a signup flow and signup failed, treat as new user
|
// If this is a signup flow and signup failed, treat as new user
|
||||||
if (isSignupFlow) {
|
if (isSignupFlow) {
|
||||||
|
|
@ -251,8 +297,8 @@ Column(
|
||||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
onCreateProfile(name)
|
onCreateProfile(name)
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.d("OTPScreen", "Navigating to success screen")
|
// Don't manually navigate - let AppNavigation handle it
|
||||||
onSuccess()
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
|
@ -276,39 +322,41 @@ Column(
|
||||||
authManager.login(phoneNumber, otp.value)
|
authManager.login(phoneNumber, otp.value)
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
|
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()
|
mainViewModel.refreshAuthStatus()
|
||||||
try {
|
android.util.Log.d("OTPScreen", "Auth status refreshed - navigation will happen automatically via LaunchedEffect")
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onFailure { error ->
|
.onFailure { error ->
|
||||||
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
|
||||||
|
|
||||||
|
// 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(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
"Invalid or expired OTP",
|
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
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
),
|
),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.ui.unit.sp
|
||||||
import com.example.livingai_lg.api.AuthApiClient
|
import com.example.livingai_lg.api.AuthApiClient
|
||||||
import com.example.livingai_lg.api.AuthManager
|
import com.example.livingai_lg.api.AuthManager
|
||||||
import com.example.livingai_lg.api.TokenManager
|
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.backgrounds.DecorativeBackground
|
||||||
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
import com.example.livingai_lg.ui.components.PhoneNumberInput
|
||||||
|
|
||||||
|
|
@ -96,6 +97,11 @@ fun SignInScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
val fullPhoneNumber = "+91${phoneNumber.value}"
|
val fullPhoneNumber = "+91${phoneNumber.value}"
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
// 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)
|
authManager.requestOtp(fullPhoneNumber)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
onSignInClick(fullPhoneNumber, "existing_user")
|
onSignInClick(fullPhoneNumber, "existing_user")
|
||||||
|
|
@ -103,6 +109,23 @@ fun SignInScreen(
|
||||||
.onFailure {
|
.onFailure {
|
||||||
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
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 { 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = isValid,
|
enabled = isValid,
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,21 @@ fun SignUpScreen(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val fullPhoneNumber = "+91${formData.phoneNumber}"
|
val fullPhoneNumber = "+91${formData.phoneNumber}"
|
||||||
|
|
||||||
|
// 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
|
// Request OTP first before allowing signup
|
||||||
authManager.requestOtp(fullPhoneNumber)
|
authManager.requestOtp(fullPhoneNumber)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
|
|
@ -180,6 +195,26 @@ fun SignUpScreen(
|
||||||
Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show()
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
enabled = formData.isValid,
|
enabled = formData.isValid,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue