Updated FIxed routes and Logout

This commit is contained in:
Chandresh Kerkar 2025-12-20 00:39:58 +05:30
parent 95c6c598a0
commit 682ed78491
12 changed files with 356 additions and 57 deletions

View File

@ -29,7 +29,7 @@ class MainActivity : ComponentActivity() {
val mainViewModel: MainViewModel = viewModel(factory = MainViewModelFactory(LocalContext.current))
val authState by mainViewModel.authState.collectAsState()
AppNavigation(authState)
AppNavigation(authState = authState, mainViewModel = mainViewModel)
// when (authState) {
// is AuthState.Unknown -> {
// Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {

View File

@ -111,6 +111,20 @@ class AuthApiClient(private val context: Context) {
client.get("users/me").body()
}
suspend fun refreshToken(): Result<RefreshResponse> = runCatching {
val refreshToken = tokenManager.getRefreshToken()
?: throw IllegalStateException("No refresh token found")
val response: RefreshResponse = client.post("auth/refresh") {
contentType(ContentType.Application.Json)
setBody(RefreshRequest(refreshToken))
}.body()
// Save the new tokens (refresh token rotates)
tokenManager.saveTokens(response.accessToken, response.refreshToken)
response
}
suspend fun logout(): Result<LogoutResponse> = runCatching {
val refreshToken = tokenManager.getRefreshToken() ?: throw IllegalStateException("No refresh token found")
val response: LogoutResponse = client.post("auth/logout") {

View File

@ -39,6 +39,10 @@ class AuthManager(
return apiClient.updateProfile(name, userType)
}
suspend fun logout(): Result<com.example.livingai_lg.api.LogoutResponse> {
return apiClient.logout()
}
private fun getDeviceId(): String {
return Settings.Secure.getString(
context.contentResolver,

View File

@ -88,8 +88,8 @@ data class User(
val id: String,
@SerialName("phone_number") val phoneNumber: String,
val name: String?,
val role: String,
@SerialName("user_type") val userType: String?,
val role: String? = null, // Optional field - can be missing from JSON, defaults to null
@SerialName("user_type") val userType: String? = null, // Optional field - can be missing from JSON, defaults to null
@SerialName("created_at") val createdAt: String? = null,
@SerialName("country_code") val countryCode: String? = null
)

View File

@ -40,27 +40,80 @@ class MainViewModel(context: Context) : ViewModel() {
checkAuthStatus()
}
/**
* Public method to refresh auth status after login/signup
* Call this after tokens are saved to update the auth state
*/
fun refreshAuthStatus() {
checkAuthStatus()
}
private fun checkAuthStatus() {
viewModelScope.launch {
if (tokenManager.getAccessToken() != null) {
_authState.value = AuthState.Authenticated
fetchUserDetails()
val accessToken = tokenManager.getAccessToken()
val refreshToken = tokenManager.getRefreshToken()
if (accessToken != null && refreshToken != null) {
// Tokens exist, validate them by fetching user details
// The Ktor Auth plugin will automatically refresh if access token is expired
validateTokens()
} else {
// No tokens, user is not authenticated
_authState.value = AuthState.Unauthenticated
}
}
}
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
authApiClient.getUserDetails()
.onSuccess { userDetails ->
// Tokens are valid, user is authenticated
_authState.value = AuthState.Authenticated
_userState.value = UserState.Success(userDetails)
}
.onFailure { error ->
// If fetching user details failed, try manual refresh
Log.d(TAG, "Failed to fetch user details, attempting token refresh: ${error.message}")
attemptTokenRefresh()
}
}
}
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.")
}
}
}
fun fetchUserDetails() {
viewModelScope.launch {
_userState.value = UserState.Loading
authApiClient.getUserDetails()
.onSuccess {
_userState.value = UserState.Success(it)
_authState.value = AuthState.Authenticated
}
.onFailure {
_userState.value = UserState.Error(it.message ?: "Unknown error")
_authState.value = AuthState.Unauthenticated
// Don't automatically set to Unauthenticated here - let the caller decide
// or try refresh if needed
}
}
}

View File

@ -33,7 +33,8 @@ fun UserLocationHeader(
user: UserProfile,
selectedAddressId: String,
modifier: Modifier = Modifier,
onOpenAddressOverlay: () -> Unit
onOpenAddressOverlay: () -> Unit,
onProfileClick: () -> Unit = {} // New callback for profile icon click
) {
Row(
modifier = modifier.wrapContentWidth(),
@ -41,12 +42,18 @@ fun UserLocationHeader(
) {
val selectedAddress = user.addresses.find { it.id == selectedAddressId } ?: user.addresses.first()
// Profile image
// Profile image - make it clickable
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(Color.Black),
.background(Color.Black)
.clickable(
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() }
) {
onProfileClick()
},
contentAlignment = Alignment.Center
) {
if (user.profileImageUrl != null) {

View File

@ -70,6 +70,8 @@ object AppScreen {
const val CHATS = "chats"
const val ACCOUNTS = "accounts"
fun chats(contact: String) =
"$CHAT/$contact"
@ -100,7 +102,7 @@ object AppScreen {
object Graph {
const val AUTH = "auth"
const val MAIN = "auth"
const val MAIN = "main"
fun auth(route: String)=
"$AUTH/$route"
@ -110,7 +112,8 @@ object Graph {
}
@Composable
fun AppNavigation(
authState: AuthState
authState: AuthState,
mainViewModel: com.example.livingai_lg.ui.MainViewModel
) {
val navController = rememberNavController()
@ -120,21 +123,39 @@ fun AppNavigation(
navController = navController,
startDestination = Graph.AUTH
) {
authNavGraph(navController)
authNavGraph(navController, mainViewModel)
mainNavGraph(navController)
}
// Navigate to MAIN graph if user is authenticated
// Handle navigation based on auth state
LaunchedEffect(authState) {
if (authState is AuthState.Authenticated) {
val currentRoute = navController.currentBackStackEntry?.destination?.route
// Only navigate if we're not already in the MAIN graph
if (currentRoute?.startsWith(Graph.MAIN) != true) {
navController.navigate(Graph.MAIN) {
// Clear back stack to prevent going back to auth screens
popUpTo(Graph.AUTH) { inclusive = true }
when (authState) {
is AuthState.Authenticated -> {
// User is authenticated, navigate to main graph
val currentRoute = navController.currentBackStackEntry?.destination?.route
// Only navigate if we're not already in the MAIN graph
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 }
}
}
}
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) {
navController.navigate(Graph.AUTH) {
// Clear back stack to prevent going back to main screens
popUpTo(0) { inclusive = true }
}
}
}
is AuthState.Unknown -> {
// Still checking auth status, stay on landing screen
// Don't navigate anywhere yet
}
}
}
// MainNavGraph(navController)

View File

@ -20,7 +20,7 @@ import com.example.livingai_lg.ui.screens.auth.OtpScreen
import com.example.livingai_lg.ui.screens.auth.SignInScreen
import com.example.livingai_lg.ui.screens.auth.SignUpScreen
fun NavGraphBuilder.authNavGraph(navController: NavController) {
fun NavGraphBuilder.authNavGraph(navController: NavController, mainViewModel: com.example.livingai_lg.ui.MainViewModel) {
navigation(
route = Graph.AUTH,
startDestination = AppScreen.LANDING
@ -67,6 +67,7 @@ fun NavGraphBuilder.authNavGraph(navController: NavController) {
OtpScreen(
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
name = backStackEntry.arguments?.getString("name") ?: "",
mainViewModel = mainViewModel,
onCreateProfile = {name ->
android.util.Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
// Navigate to main graph first without popping, then navigate to specific route
@ -92,6 +93,19 @@ fun NavGraphBuilder.authNavGraph(navController: NavController) {
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
}
},
onLanding = {
android.util.Log.d("AuthNavGraph", "Navigating to landing page for new user")
// Navigate to landing page within AUTH graph
try {
navController.navigate(AppScreen.LANDING) {
// Pop all screens up to and including the current OTP screen
popUpTo(Graph.AUTH) { inclusive = false }
launchSingleTop = true
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
}
},
onSuccess = {
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
// Navigate to main graph first without popping, then navigate to specific route
@ -136,6 +150,7 @@ fun NavGraphBuilder.authNavGraph(navController: NavController) {
signupState = backStackEntry.arguments?.getString("state"),
signupDistrict = backStackEntry.arguments?.getString("district"),
signupVillage = backStackEntry.arguments?.getString("village"),
mainViewModel = mainViewModel,
onCreateProfile = {name ->
android.util.Log.d("AuthNavGraph", "Navigating to create profile with name: $name")
// Navigate to main graph first without popping, then navigate to specific route
@ -161,6 +176,19 @@ fun NavGraphBuilder.authNavGraph(navController: NavController) {
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
}
},
onLanding = {
android.util.Log.d("AuthNavGraph", "Navigating to landing page for new user")
// Navigate to landing page within AUTH graph
try {
navController.navigate(AppScreen.LANDING) {
// Pop all screens up to and including the current OTP screen
popUpTo(Graph.AUTH) { inclusive = false }
launchSingleTop = true
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Navigation to landing error: ${e.message}", e)
}
},
onSuccess = {
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
// Navigate to main graph first without popping, then navigate to specific route

View File

@ -8,9 +8,12 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.navArgument
import com.example.livingai_lg.ui.navigation.AppScreen
import com.example.livingai_lg.ui.navigation.Graph
import com.example.farmmarketplace.ui.screens.CallsScreen
import com.example.farmmarketplace.ui.screens.ContactsScreen
import com.example.livingai_lg.ui.models.profileTypes
import com.example.livingai_lg.ui.screens.AccountsScreen
import com.example.livingai_lg.ui.screens.AnimalProfileScreen
import com.example.livingai_lg.ui.screens.BuyScreen
import com.example.livingai_lg.ui.screens.ChooseServiceScreen
@ -126,6 +129,18 @@ fun NavGraphBuilder.mainNavGraph(navController: NavController) {
onBackClick = { navController.popBackStack() })
}
composable(AppScreen.ACCOUNTS) {
AccountsScreen(
onBackClick = { navController.popBackStack() },
onLogout = {
// Navigate to auth graph after logout
navController.navigate(Graph.AUTH) {
popUpTo(0) { inclusive = true }
}
}
)
}
composable(AppScreen.CREATE_ANIMAL_LISTING) {
NewListingScreen (

View File

@ -0,0 +1,129 @@
package com.example.livingai_lg.ui.screens
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
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 kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccountsScreen(
onBackClick: () -> Unit,
onLogout: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFF7F4EE))
) {
// Top App Bar
TopAppBar(
title = {
Text(
text = "Accounts",
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = Color.Black
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = Color.Black
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFFF7F4EE)
)
)
// Content
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 24.dp)
) {
// Logout option
Surface(
modifier = Modifier
.fillMaxWidth()
.clickable {
scope.launch {
authManager.logout()
.onSuccess { _: com.example.livingai_lg.api.LogoutResponse ->
Toast.makeText(context, "Logged out successfully", Toast.LENGTH_SHORT).show()
onLogout()
}
.onFailure { error: Throwable ->
Toast.makeText(context, "Logout failed: ${error.message}", Toast.LENGTH_SHORT).show()
// Still call onLogout to navigate away even if API call fails
onLogout()
}
}
},
shape = RoundedCornerShape(12.dp),
color = Color.White,
shadowElevation = 2.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 20.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Logout,
contentDescription = "Logout",
tint = Color(0xFFE53935),
modifier = Modifier.size(24.dp)
)
Text(
text = "Log Out",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = Color(0xFFE53935)
)
}
}
}
}
}
}

View File

@ -94,6 +94,9 @@ fun BuyScreen(
user = userProfile,
onOpenAddressOverlay = { showAddressSelector = true },
selectedAddressId = selectedAddressId?:"1",
onProfileClick = {
onNavClick(AppScreen.ACCOUNTS)
}
)
// Right-side actions (notifications, etc.)

View File

@ -48,8 +48,10 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
fun OtpScreen(
phoneNumber: String,
name: String,
mainViewModel: com.example.livingai_lg.ui.MainViewModel,
onSuccess: () -> Unit = {},
onCreateProfile: (name: String) -> Unit = {},
onLanding: () -> Unit = {}, // New callback for navigating to landing page
// Optional signup data for signup flow
signupState: String? = null,
signupDistrict: String? = null,
@ -164,6 +166,8 @@ Column(
.onSuccess { verifyResponse ->
android.util.Log.d("OTPScreen", "OTP verified successfully. Calling signup API...")
// OTP verified successfully - user is now logged in
// Refresh auth status in MainViewModel so AppNavigation knows user is authenticated
mainViewModel.refreshAuthStatus()
// Now call signup API to update user with name and location
val signupRequest = SignupRequest(
name = name,
@ -174,57 +178,76 @@ Column(
)
authManager.signup(signupRequest)
.onSuccess { signupResponse ->
android.util.Log.d("OTPScreen", "Signup API response: success=${signupResponse.success}, userExists=${signupResponse.userExists}, needsProfile=${signupResponse.needsProfile}")
// Signup API response - check if successful or user exists
if (signupResponse.success || signupResponse.userExists == true) {
// Success - user is created/updated and logged in
// Check if profile needs completion
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
android.util.Log.d("OTPScreen", "Signup API response: success=${signupResponse.success}, userExists=${signupResponse.userExists}, needsProfile=${signupResponse.needsProfile}, isNewAccount=${signupResponse.isNewAccount}")
// Check if this is a new user account
val isNewUser = signupResponse.isNewAccount == true
if (isNewUser) {
// New user - navigate to landing page
android.util.Log.d("OTPScreen", "New user detected - navigating to landing page")
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()
}
onLanding()
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
}
} else {
// Signup failed but OTP was verified - user is logged in
// 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
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()
// Existing user or signup update - use existing logic
if (signupResponse.success || signupResponse.userExists == true) {
// Success - user is created/updated and logged in
// Check if profile needs completion
val needsProfile = signupResponse.needsProfile == true || verifyResponse.needsProfile
android.util.Log.d("OTPScreen", "Signup successful. needsProfile=$needsProfile, navigating...")
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()
}
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
}
} else {
// Signup failed but OTP was verified - user is logged in
// 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
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()
}
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
}
// Show warning if signup update failed
val errorMsg = signupResponse.message
if (errorMsg != null) {
Toast.makeText(context, "Profile update: $errorMsg", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
Toast.makeText(context, "Navigation error: ${e.message}", Toast.LENGTH_LONG).show()
}
// Show warning if signup update failed
val errorMsg = signupResponse.message
if (errorMsg != null) {
Toast.makeText(context, "Profile update: $errorMsg", Toast.LENGTH_SHORT).show()
}
}
}
.onFailure { signupError ->
android.util.Log.e("OTPScreen", "Signup API failed: ${signupError.message}", signupError)
// Signup API failed but OTP was verified - user is logged in
// Navigate to success anyway since verify-otp succeeded
// For new users, navigate to landing page
// For existing users, use existing logic
val needsProfile = verifyResponse.needsProfile
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$needsProfile")
try {
if (needsProfile) {
// If this is a signup flow and signup failed, treat as new user
if (isSignupFlow) {
android.util.Log.d("OTPScreen", "Signup flow failed - navigating to landing page")
onLanding()
} else if (needsProfile) {
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
onCreateProfile(name)
} else {
@ -253,6 +276,8 @@ 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
mainViewModel.refreshAuthStatus()
try {
if (isSignInFlow) {
// For existing users, always go to the success screen.