Upadated COde with authentication working

This commit is contained in:
Chandresh Kerkar 2025-12-19 23:24:09 +05:30
parent 6628daed50
commit 078bc02c36
11 changed files with 961 additions and 418 deletions

View File

@ -14,6 +14,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.LivingAi_Lg"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
tools:targetApi="31">
<activity
android:name=".MainActivity"

View File

@ -2,6 +2,7 @@ package com.example.livingai_lg.api
import android.content.Context
import android.os.Build
import android.provider.Settings
import com.example.livingai_lg.BuildConfig
import io.ktor.client.*
import io.ktor.client.call.*
@ -11,6 +12,7 @@ import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
@ -28,6 +30,8 @@ class AuthApiClient(private val context: Context) {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
coerceInputValues = true // Coerce missing fields to default values
encodeDefaults = false
})
}
@ -74,15 +78,50 @@ class AuthApiClient(private val context: Context) {
}
suspend fun verifyOtp(phoneNumber: String, code: String, deviceId: String): Result<VerifyOtpResponse> = runCatching {
// Trim and validate code (ensure no whitespace, exactly 6 digits)
val trimmedCode = code.trim()
// Debug: Log the request details
android.util.Log.d("AuthApiClient", "Verify OTP Request:")
android.util.Log.d("AuthApiClient", " phone_number: $phoneNumber")
android.util.Log.d("AuthApiClient", " code (original): '$code' (length: ${code.length})")
android.util.Log.d("AuthApiClient", " code (trimmed): '$trimmedCode' (length: ${trimmedCode.length})")
android.util.Log.d("AuthApiClient", " device_id: $deviceId")
// Create request object with trimmed code
val request = VerifyOtpRequest(phoneNumber, trimmedCode, deviceId, getDeviceInfo())
val response: VerifyOtpResponse = client.post("auth/verify-otp") {
contentType(ContentType.Application.Json)
setBody(VerifyOtpRequest(phoneNumber, code, deviceId, getDeviceInfo()))
// Send code as string - backend validation requires string type and bcrypt.compare needs string
setBody(request)
}.body()
tokenManager.saveTokens(response.accessToken, response.refreshToken)
response
}
suspend fun signup(request: SignupRequest): Result<SignupResponse> = runCatching {
val response = client.post("auth/signup") {
contentType(ContentType.Application.Json)
setBody(request.copy(deviceId = getDeviceId(), deviceInfo = getDeviceInfo()))
}
// Handle both success and error responses
if (response.status.isSuccess()) {
response.body<SignupResponse>()
} else {
// Try to parse error response as SignupResponse (for 409 conflicts with user_exists flag)
try {
response.body<SignupResponse>()
} catch (e: Exception) {
// If parsing fails, throw an exception with the status and message
val errorBody = response.bodyAsText()
throw Exception("Signup failed: ${response.status} - $errorBody")
}
}
}
suspend fun updateProfile(name: String, userType: String): Result<User> = runCatching {
client.put("users/me") {
contentType(ContentType.Application.Json)
@ -115,4 +154,8 @@ class AuthApiClient(private val context: Context) {
timezone = TimeZone.getDefault().id
)
}
private fun getDeviceId(): String {
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
}
}

View File

@ -16,7 +16,22 @@ class AuthManager(
val deviceId = getDeviceId()
return apiClient.verifyOtp(phoneNumber, code, deviceId)
.onSuccess { response ->
tokenManager.saveTokens(response.accessToken, response.refreshToken)
response.accessToken?.let { accessToken ->
response.refreshToken?.let { refreshToken ->
tokenManager.saveTokens(accessToken, refreshToken)
}
}
}
}
suspend fun signup(signupRequest: SignupRequest): Result<SignupResponse> {
return apiClient.signup(signupRequest)
.onSuccess { response ->
response.accessToken?.let { accessToken ->
response.refreshToken?.let { refreshToken ->
tokenManager.saveTokens(accessToken, refreshToken)
}
}
}
}

View File

@ -23,7 +23,7 @@ data class DeviceInfo(
@Serializable
data class VerifyOtpRequest(
@SerialName("phone_number") val phoneNumber: String,
val code: String,
val code: String, // Changed from Int to String - backend expects string for bcrypt comparison
@SerialName("device_id") val deviceId: String,
@SerialName("device_info") val deviceInfo: DeviceInfo? = null
)
@ -37,6 +37,34 @@ data class VerifyOtpResponse(
)
// endregion
// region: Signup
@Serializable
data class SignupRequest(
val name: String,
@SerialName("phone_number") val phoneNumber: String,
val state: String? = null,
val district: String? = null,
@SerialName("city_village") val cityVillage: String? = null,
@SerialName("device_id") val deviceId: String? = null,
@SerialName("device_info") val deviceInfo: DeviceInfo? = null
)
@Serializable
data class SignupResponse(
val success: Boolean,
val user: User? = null,
@SerialName("access_token") val accessToken: String? = null,
@SerialName("refresh_token") val refreshToken: String? = null,
@SerialName("needs_profile") val needsProfile: Boolean? = null,
@SerialName("is_new_account") val isNewAccount: Boolean? = null,
@SerialName("is_new_device") val isNewDevice: Boolean? = null,
@SerialName("active_devices_count") val activeDevicesCount: Int? = null,
@SerialName("location_id") val locationId: String? = null,
val message: String? = null,
@SerialName("user_exists") val userExists: Boolean? = null
)
// endregion
// region: Token Refresh
@Serializable
data class RefreshRequest(@SerialName("refresh_token") val refreshToken: String)
@ -59,9 +87,11 @@ data class UpdateProfileRequest(
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 name: String? = null,
val role: String? = null, // Made nullable - backend can return null for new users
@SerialName("user_type") val userType: String? = null, // Made nullable with default - backend may not return this
@SerialName("created_at") val createdAt: String? = null,
@SerialName("country_code") val countryCode: String? = null
)
@Serializable

View File

@ -0,0 +1,192 @@
# Signup API Endpoint
## POST /auth/signup
Creates a new user account with name, phone number, and optional location information.
### Request Body
```json
{
"name": "John Doe", // Required: User's name (string, max 100 chars)
"phone_number": "+919876543210", // Required: Phone number in E.164 format
"state": "Maharashtra", // Optional: State name (string, max 100 chars)
"district": "Mumbai", // Optional: District name (string, max 100 chars)
"city_village": "Andheri", // Optional: City/Village name (string, max 150 chars)
"device_id": "device-123", // Optional: Device identifier
"device_info": { // Optional: Device information
"platform": "android",
"model": "Samsung Galaxy S21",
"os_version": "Android 13",
"app_version": "1.0.0",
"language_code": "en",
"timezone": "Asia/Kolkata"
}
}
```
### Success Response (201 Created)
```json
{
"success": true,
"user": {
"id": "uuid-here",
"phone_number": "+919876543210",
"name": "John Doe",
"country_code": "+91",
"created_at": "2024-01-15T10:30:00Z"
},
"access_token": "jwt-access-token",
"refresh_token": "jwt-refresh-token",
"needs_profile": true,
"is_new_account": true,
"is_new_device": true,
"active_devices_count": 1,
"location_id": "uuid-of-location" // null if no location provided
}
```
### Error Responses
#### 400 Bad Request - Validation Error
```json
{
"error": "name is required"
}
```
#### 409 Conflict - User Already Exists
```json
{
"success": false,
"message": "User with this phone number already exists. Please sign in instead.",
"user_exists": true
}
```
#### 403 Forbidden - IP Blocked
```json
{
"success": false,
"message": "Access denied from this location."
}
```
#### 500 Internal Server Error
```json
{
"success": false,
"message": "Internal server error"
}
```
### Features
1. **User Existence Check**: Automatically checks if a user with the phone number already exists
2. **Phone Number Encryption**: Phone numbers are encrypted before storing in database
3. **Location Creation**: If state/district/city_village provided, creates a location entry
4. **Token Issuance**: Automatically issues access and refresh tokens
5. **Device Tracking**: Records device information for security
6. **Audit Logging**: Logs signup events for security monitoring
### Example Usage
#### cURL
```bash
curl -X POST http://localhost:3000/auth/signup \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"phone_number": "+919876543210",
"state": "Maharashtra",
"district": "Mumbai",
"city_village": "Andheri",
"device_id": "android-device-123",
"device_info": {
"platform": "android",
"model": "Samsung Galaxy S21",
"os_version": "Android 13"
}
}'
```
#### JavaScript/TypeScript
```javascript
const response = await fetch('/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'John Doe',
phone_number: '+919876543210',
state: 'Maharashtra',
district: 'Mumbai',
city_village: 'Andheri',
device_id: 'android-device-123',
device_info: {
platform: 'android',
model: 'Samsung Galaxy S21',
os_version: 'Android 13'
}
})
});
const data = await response.json();
if (data.success) {
// Store tokens
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
}
```
#### Kotlin/Android
```kotlin
data class SignupRequest(
val name: String,
val phone_number: String,
val state: String? = null,
val district: String? = null,
val city_village: String? = null,
val device_id: String? = null,
val device_info: Map<String, String?>? = null
)
data class SignupResponse(
val success: Boolean,
val user: User,
val access_token: String,
val refresh_token: String,
val needs_profile: Boolean,
val is_new_account: Boolean,
val is_new_device: Boolean,
val active_devices_count: Int,
val location_id: String?
)
// Usage
val request = SignupRequest(
name = "John Doe",
phone_number = "+919876543210",
state = "Maharashtra",
district = "Mumbai",
city_village = "Andheri",
device_id = getDeviceId(),
device_info = mapOf(
"platform" to "android",
"model" to Build.MODEL,
"os_version" to Build.VERSION.RELEASE
)
)
val response = apiClient.post<SignupResponse>("/auth/signup", request)
```
### Notes
- Phone number must be in E.164 format (e.g., `+919876543210`)
- If phone number is 10 digits without `+`, it will be normalized to `+91` prefix
- Location fields are optional - user can be created without location
- If user already exists, returns 409 Conflict with `user_exists: true`
- All phone numbers are encrypted in the database for security
- Country code is automatically extracted from phone number

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -75,6 +76,9 @@ object AppScreen {
fun otp(phone: String, name: String) =
"$OTP/$phone/$name"
fun otpWithSignup(phone: String, name: String, state: String, district: String, village: String) =
"$OTP/$phone/$name/$state/$district/$village"
fun createProfile(name: String) =
"$CREATE_PROFILE/$name"
@ -109,25 +113,30 @@ fun AppNavigation(
authState: AuthState
) {
val navController = rememberNavController()
var isLoggedIn = false;
when (authState) {
is AuthState.Unauthenticated -> {isLoggedIn = false; }
is AuthState.Authenticated -> {isLoggedIn = true;}
is AuthState.Unknown -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
}
// Always start with AUTH (landing screen) - this ensures LandingScreen opens first
// We'll navigate to MAIN only if user is authenticated (handled by LaunchedEffect)
NavHost(
navController = navController,
startDestination = if (isLoggedIn) Graph.MAIN else Graph.AUTH
startDestination = Graph.AUTH
) {
authNavGraph(navController)
mainNavGraph(navController)
}
// Navigate to MAIN graph if user is authenticated
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 }
}
}
}
}
// MainNavGraph(navController)
// AuthNavGraph(navController)
// when (authState) {

View File

@ -10,6 +10,10 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
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
@ -42,8 +46,8 @@ fun NavGraphBuilder.authNavGraph(navController: NavController) {
composable(AppScreen.SIGN_UP) {
SignUpScreen(
onSignUpClick = { phone, name ->
navController.navigate(AppScreen.otp(phone,name))
onSignUpClick = { phone, name, state, district, village ->
navController.navigate(AppScreen.otpWithSignup(phone, name, state, district, village))
},
onSignInClick = {
navController.navigate(AppScreen.SIGN_IN) {
@ -63,8 +67,125 @@ fun NavGraphBuilder.authNavGraph(navController: NavController) {
OtpScreen(
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
name = backStackEntry.arguments?.getString("name") ?: "",
onCreateProfile = {name -> navController.navigate(Graph.main(AppScreen.createProfile(name)))},
onSuccess = { navController.navigate(Graph.auth(AppScreen.chooseService("1")))}
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
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
launchSingleTop = true
}
// Then navigate to the specific route after a short delay
CoroutineScope(Dispatchers.Main).launch {
delay(200)
try {
navController.navigate(AppScreen.createProfile(name)) {
// 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)
}
},
onSuccess = {
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
// Navigate to main graph first without popping, then navigate to specific route
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
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)
}
}
)
}
composable(
"${AppScreen.OTP}/{phoneNumber}/{name}/{state}/{district}/{village}",
arguments = listOf(
navArgument("phoneNumber") { type = NavType.StringType },
navArgument("name") { type = NavType.StringType },
navArgument("state") { type = NavType.StringType },
navArgument("district") { type = NavType.StringType },
navArgument("village") { type = NavType.StringType }
)
) { backStackEntry ->
OtpScreen(
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
name = backStackEntry.arguments?.getString("name") ?: "",
signupState = backStackEntry.arguments?.getString("state"),
signupDistrict = backStackEntry.arguments?.getString("district"),
signupVillage = backStackEntry.arguments?.getString("village"),
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
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
launchSingleTop = true
}
// Then navigate to the specific route after a short delay
CoroutineScope(Dispatchers.Main).launch {
delay(200)
try {
navController.navigate(AppScreen.createProfile(name)) {
// 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)
}
},
onSuccess = {
android.util.Log.d("AuthNavGraph", "Navigating to choose service")
// Navigate to main graph first without popping, then navigate to specific route
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
launchSingleTop = true
}
// Then navigate to the specific route after a short delay
CoroutineScope(Dispatchers.Main).launch {
delay(200)
try {
navController.navigate(AppScreen.chooseService("1")) {
// Now pop the AUTH graph after we're in the MAIN graph
popUpTo(Graph.AUTH) { inclusive = true }
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Secondary navigation error: ${e.message}", e)
}
}
} catch (e: Exception) {
android.util.Log.e("AuthNavGraph", "Navigation error: ${e.message}", e)
}
}
)
}
}

View File

@ -44,7 +44,7 @@ fun NavGraphBuilder.mainNavGraph(navController: NavController) {
navigation(
route = Graph.MAIN,
startDestination = AppScreen.createProfile("guest")
startDestination = AppScreen.BUY_ANIMALS
){

View File

@ -27,6 +27,7 @@ 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.SignupRequest
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
import kotlin.math.min
@ -49,6 +50,10 @@ fun OtpScreen(
name: String,
onSuccess: () -> Unit = {},
onCreateProfile: (name: String) -> Unit = {},
// Optional signup data for signup flow
signupState: String? = null,
signupDistrict: String? = null,
signupVillage: String? = null,
) {
val otp = remember { mutableStateOf("") }
val context = LocalContext.current.applicationContext
@ -57,6 +62,8 @@ fun OtpScreen(
// Flag to determine if this is a sign-in flow for an existing user.
val isSignInFlow = name == "existing_user"
// Flag to determine if this is a signup flow (has signup data)
val isSignupFlow = !isSignInFlow && (signupState != null || signupDistrict != null || signupVillage != null)
BoxWithConstraints(
modifier = Modifier
@ -150,27 +157,131 @@ Column(
interactionSource = remember { MutableInteractionSource() },
onClick = {
scope.launch {
if (isSignupFlow) {
// For signup flow: Verify OTP first, then call signup API to update user with name/location
android.util.Log.d("OTPScreen", "Signup flow: Verifying OTP...")
authManager.login(phoneNumber, otp.value)
.onSuccess { response ->
if (isSignInFlow) {
// For existing users, always go to the success screen.
onSuccess()
} else {
// For new users, check if a profile needs to be created.
if (response.needsProfile) {
.onSuccess { verifyResponse ->
android.util.Log.d("OTPScreen", "OTP verified successfully. Calling signup API...")
// OTP verified successfully - user is now logged in
// Now call signup API to update user with name and location
val signupRequest = SignupRequest(
name = name,
phoneNumber = phoneNumber,
state = signupState,
district = signupDistrict,
cityVillage = signupVillage
)
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...")
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()
}
}
.onFailure {
}
.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
val needsProfile = verifyResponse.needsProfile
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$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 about signup failure
val errorMsg = signupError.message ?: "Profile update failed"
Toast.makeText(context, "Warning: $errorMsg", Toast.LENGTH_SHORT).show()
}
}
.onFailure { error ->
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
Toast.makeText(
context,
"Invalid or expired OTP",
Toast.LENGTH_SHORT
).show()
}
} else {
// For sign-in flow: Just verify OTP and login
authManager.login(phoneNumber, otp.value)
.onSuccess { response ->
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
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()
}
}
.onFailure { error ->
android.util.Log.e("OTPScreen", "OTP verification failed: ${error.message}", error)
Toast.makeText(
context,
"Invalid or expired OTP",
Toast.LENGTH_SHORT
).show()
}
}
}
}
),

View File

@ -17,6 +17,7 @@ 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.SignupRequest
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground
import com.example.livingai_lg.ui.components.DropdownInput
@ -52,7 +53,7 @@ private fun String.isValidPhoneNumber(): Boolean {
@Composable
fun SignUpScreen(
onSignInClick: () -> Unit = {},
onSignUpClick: (phone: String, name: String) -> Unit = {_,_->}
onSignUpClick: (phone: String, name: String, state: String, district: String, village: String) -> Unit = {_,_,_,_,_->}
) {
var formData by remember { mutableStateOf(SignUpFormData()) }
@ -168,9 +169,12 @@ fun SignUpScreen(
onClick = {
val fullPhoneNumber = "+91${formData.phoneNumber}"
scope.launch {
// Request OTP first before allowing signup
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
onSignUpClick(fullPhoneNumber,formData.name)
// 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, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Allow cleartext traffic for localhost/emulator (10.0.2.2) -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">10.0.2.2</domain>
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
<!-- For production, use HTTPS only -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>