Updated Login

This commit is contained in:
Chandresh Kerkar 2025-11-30 01:28:23 +05:30
parent 8d2808734a
commit d6b099f921
16 changed files with 1813 additions and 106 deletions

8
.idea/markdown.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

View File

@ -2,18 +2,17 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "com.example.livingai_lg"
compileSdk {
version = release(36)
}
compileSdk = 34
defaultConfig {
applicationId = "com.example.livingai_lg"
minSdk = 24
targetSdk = 36
targetSdk = 34
versionCode = 1
versionName = "1.0"
@ -30,14 +29,15 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "1.8"
}
buildFeatures {
compose = true
buildConfig = true
}
}
@ -50,7 +50,20 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation(libs.androidx.navigation.compose)
// Ktor
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
// Kotlinx Serialization
implementation(libs.kotlinx.serialization.json)
// AndroidX Security
implementation(libs.androidx.security.crypto)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

1130
app/how_to_use_Auth.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -10,11 +12,12 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.LivingAi_Lg">
android:theme="@style/Theme.LivingAi_Lg"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.LivingAi_Lg">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -1,17 +1,15 @@
package com.example.livingai_lg
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.livingai_lg.ui.login.LoginScreen
import com.example.livingai_lg.ui.login.SignUpScreen
import com.example.livingai_lg.ui.login.OtpScreen
import com.example.livingai_lg.ui.login.CreateProfileScreen
import androidx.navigation.navArgument
import com.example.livingai_lg.ui.login.*
import com.example.livingai_lg.ui.theme.LivingAi_LgTheme
class MainActivity : ComponentActivity() {
@ -28,11 +26,32 @@ class MainActivity : ComponentActivity() {
composable("signup") {
SignUpScreen(navController = navController)
}
composable("otp") {
OtpScreen(navController = navController)
composable("signin") {
SignInScreen(navController = navController)
}
composable("create_profile") {
CreateProfileScreen(navController = navController)
composable(
"otp/{phoneNumber}/{name}",
arguments = listOf(
navArgument("phoneNumber") { type = NavType.StringType },
navArgument("name") { type = NavType.StringType })
) { backStackEntry ->
OtpScreen(
navController = navController,
phoneNumber = backStackEntry.arguments?.getString("phoneNumber") ?: "",
name = backStackEntry.arguments?.getString("name") ?: ""
)
}
composable(
"create_profile/{name}",
arguments = listOf(navArgument("name") { type = NavType.StringType })
) { backStackEntry ->
CreateProfileScreen(
navController = navController,
name = backStackEntry.arguments?.getString("name") ?: ""
)
}
composable("success") {
SuccessScreen()
}
}
}

View File

@ -0,0 +1,107 @@
package com.example.livingai_lg.api
import android.os.Build
import com.example.livingai_lg.BuildConfig
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import java.util.Locale
import java.util.TimeZone
class AuthApiClient(private val baseUrl: String) {
private val client = HttpClient {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
}
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
return try {
val response = client.post("$baseUrl/auth/request-otp") {
contentType(ContentType.Application.Json)
setBody(RequestOtpRequest(phoneNumber))
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun verifyOtp(
phoneNumber: String,
code: String,
deviceId: String
): Result<VerifyOtpResponse> {
return try {
val request = VerifyOtpRequest(phoneNumber, code, deviceId, getDeviceInfo())
val response = client.post("$baseUrl/auth/verify-otp") {
contentType(ContentType.Application.Json)
setBody(request)
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun refreshToken(refreshToken: String): Result<RefreshResponse> {
return try {
val request = RefreshRequest(refreshToken)
val response = client.post("$baseUrl/auth/refresh") {
contentType(ContentType.Application.Json)
setBody(request)
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun updateProfile(
name: String,
userType: String,
accessToken: String
): Result<UpdateProfileResponse> {
return try {
val request = UpdateProfileRequest(name, userType)
val response = client.put("$baseUrl/users/me") {
contentType(ContentType.Application.Json)
header("Authorization", "Bearer $accessToken")
setBody(request)
}
Result.success(response.body())
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun logout(refreshToken: String): Result<Unit> {
return try {
val request = RefreshRequest(refreshToken)
client.post("$baseUrl/auth/logout") {
contentType(ContentType.Application.Json)
setBody(request)
}
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
private fun getDeviceInfo(): DeviceInfo {
return DeviceInfo(
platform = "android",
model = Build.MODEL,
os_version = Build.VERSION.RELEASE,
app_version = BuildConfig.VERSION_NAME,
language_code = Locale.getDefault().toString(),
timezone = TimeZone.getDefault().id
)
}
}

View File

@ -0,0 +1,34 @@
package com.example.livingai_lg.api
import android.content.Context
import android.provider.Settings
class AuthManager(
private val context: Context,
private val apiClient: AuthApiClient,
private val tokenManager: TokenManager
) {
suspend fun requestOtp(phoneNumber: String): Result<RequestOtpResponse> {
return apiClient.requestOtp(phoneNumber)
}
suspend fun login(phoneNumber: String, code: String): Result<VerifyOtpResponse> {
val deviceId = getDeviceId()
return apiClient.verifyOtp(phoneNumber, code, deviceId)
.onSuccess { response ->
tokenManager.saveTokens(response.access_token, response.refresh_token)
}
}
suspend fun updateProfile(name: String, userType: String): Result<UpdateProfileResponse> {
val accessToken = tokenManager.getAccessToken() ?: return Result.failure(Exception("No access token found"))
return apiClient.updateProfile(name, userType, accessToken)
}
private fun getDeviceId(): String {
return Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)
}
}

View File

@ -0,0 +1,35 @@
package com.example.livingai_lg.api
import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class TokenManager(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
context,
"auth_tokens",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveTokens(accessToken: String, refreshToken: String) {
prefs.edit().apply {
putString("access_token", accessToken)
putString("refresh_token", refreshToken)
apply()
}
}
fun getAccessToken(): String? = prefs.getString("access_token", null)
fun getRefreshToken(): String? = prefs.getString("refresh_token", null)
fun clearTokens() {
prefs.edit().clear().apply()
}
}

View File

@ -0,0 +1,62 @@
package com.example.livingai_lg.api
import kotlinx.serialization.Serializable
@Serializable
data class RequestOtpRequest(val phone_number: String)
@Serializable
data class RequestOtpResponse(val ok: Boolean)
@Serializable
data class DeviceInfo(
val platform: String,
val model: String? = null,
val os_version: String? = null,
val app_version: String? = null,
val language_code: String? = null,
val timezone: String? = null
)
@Serializable
data class VerifyOtpRequest(
val phone_number: String,
val code: String,
val device_id: String,
val device_info: DeviceInfo? = null
)
@Serializable
data class User(
val id: String,
val phone_number: String,
val name: String?,
val role: String,
val user_type: String?
)
@Serializable
data class VerifyOtpResponse(
val user: User,
val access_token: String,
val refresh_token: String,
val needs_profile: Boolean
)
@Serializable
data class RefreshRequest(val refresh_token: String)
@Serializable
data class RefreshResponse(val access_token: String, val refresh_token: String)
@Serializable
data class UpdateProfileRequest(val name: String, val user_type: String)
@Serializable
data class UpdateProfileResponse(
val id: String,
val phone_number: String,
val name: String?,
val role: String,
val user_type: String?
)

View File

@ -1,5 +1,6 @@
package com.example.livingai_lg.ui.login
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -7,11 +8,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
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.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
@ -20,10 +24,30 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.example.livingai_lg.R
import com.example.livingai_lg.api.AuthApiClient
import com.example.livingai_lg.api.AuthManager
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.ui.theme.*
import kotlinx.coroutines.launch
@Composable
fun CreateProfileScreen(navController: NavController) {
fun CreateProfileScreen(navController: NavController, name: String) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val authManager = remember { AuthManager(context, AuthApiClient("http://10.0.2.2:3000"), TokenManager(context)) }
fun updateProfile(userType: String) {
scope.launch {
authManager.updateProfile(name, userType)
.onSuccess {
navController.navigate("success") { popUpTo("login") { inclusive = true } }
}
.onFailure {
Toast.makeText(context, "Failed to update profile: ${it.message}", Toast.LENGTH_LONG).show()
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
@ -46,26 +70,26 @@ fun CreateProfileScreen(navController: NavController) {
Spacer(modifier = Modifier.height(64.dp))
ProfileTypeItem(text = "I'm a Seller", icon = R.drawable.ic_seller)
ProfileTypeItem(text = "I'm a Seller", icon = R.drawable.ic_seller) { updateProfile("seller") }
Spacer(modifier = Modifier.height(16.dp))
ProfileTypeItem(text = "I'm a Buyer", icon = R.drawable.ic_buyer)
ProfileTypeItem(text = "I'm a Buyer", icon = R.drawable.ic_buyer) { updateProfile("buyer") }
Spacer(modifier = Modifier.height(16.dp))
ProfileTypeItem(text = "I'm a Service Provider", icon = R.drawable.ic_service_provider)
ProfileTypeItem(text = "I'm a Service Provider", icon = R.drawable.ic_service_provider) { updateProfile("service_provider") }
Spacer(modifier = Modifier.height(16.dp))
ProfileTypeItem(text = "I'm a Mandi Host", icon = R.drawable.ic_mandi_host)
ProfileTypeItem(text = "I'm a Mandi Host", icon = R.drawable.ic_mandi_host) { /* TODO: Add user_type for Mandi Host */ }
}
}
}
@Composable
fun ProfileTypeItem(text: String, icon: Int) {
fun ProfileTypeItem(text: String, icon: Int, onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(72.dp)
.shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp))
.background(Color.White, RoundedCornerShape(16.dp))
.clickable { /* TODO */ }
.clickable(onClick = onClick)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
@ -81,6 +105,6 @@ fun ProfileTypeItem(text: String, icon: Int) {
@Composable
fun CreateProfileScreenPreview() {
LivingAi_LgTheme {
CreateProfileScreen(rememberNavController())
CreateProfileScreen(rememberNavController(), "John Doe")
}
}

View File

@ -1,7 +1,8 @@
package com.example.livingai_lg.ui.login
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.material3.Button
@ -10,9 +11,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -24,6 +24,8 @@ import com.example.livingai_lg.ui.theme.*
@Composable
fun LoginScreen(navController: NavController) {
val context = LocalContext.current
Box(
modifier = Modifier
.fillMaxSize()
@ -33,12 +35,7 @@ fun LoginScreen(navController: NavController) {
)
)
) {
// Decorative elements
Box(
modifier = Modifier.fillMaxSize()
) {
// ... (decorative elements)
}
// Decorative elements...
Column(
modifier = Modifier
@ -99,6 +96,8 @@ fun LoginScreen(navController: NavController) {
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(48.dp))
// New User Button
Button(
onClick = { navController.navigate("signup") },
shape = RoundedCornerShape(16.dp),
@ -109,9 +108,12 @@ fun LoginScreen(navController: NavController) {
) {
Text(text = "New user? Sign up", color = DarkerBrown, fontSize = 16.sp, fontWeight = FontWeight.Medium)
}
Spacer(modifier = Modifier.height(16.dp))
// Existing User Button
Button(
onClick = { /* TODO: Handle Sign in */ },
onClick = { navController.navigate("signin") },
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = TerraCotta),
modifier = Modifier
@ -120,13 +122,18 @@ fun LoginScreen(navController: NavController) {
) {
Text(text = "Already a user? Sign in", color = DarkerBrown, fontSize = 16.sp, fontWeight = FontWeight.Medium)
}
Spacer(modifier = Modifier.height(24.dp))
// Guest Button
Text(
text = "Continue as Guest",
color = MidBrown,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.clickable { Toast.makeText(context, "Guest mode is not yet available", Toast.LENGTH_SHORT).show() }
)
Spacer(modifier = Modifier.weight(1.5f))
}
}

View File

@ -1,24 +1,18 @@
package com.example.livingai_lg.ui.login
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
@ -27,11 +21,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.example.livingai_lg.api.AuthApiClient
import com.example.livingai_lg.api.AuthManager
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.ui.theme.*
import kotlinx.coroutines.launch
@Composable
fun OtpScreen(navController: NavController) {
fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
val otp = remember { mutableStateOf("") }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val authManager = remember { AuthManager(context, AuthApiClient("http://10.0.2.2:3000"), TokenManager(context)) }
// Flag to determine if this is a sign-in flow for an existing user.
val isSignInFlow = name == "existing_user"
Box(
modifier = Modifier
@ -54,16 +58,11 @@ fun OtpScreen(navController: NavController) {
Spacer(modifier = Modifier.height(32.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
for (i in 0..3) {
TextField(
value = if (otp.value.length > i) otp.value[i].toString() else "",
onValueChange = { if (it.length <= 1) { /* TODO */ } },
value = otp.value,
onValueChange = { if (it.length <= 6) otp.value = it },
modifier = Modifier
.width(60.dp)
.fillMaxWidth()
.height(60.dp)
.shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)),
shape = RoundedCornerShape(16.dp),
@ -77,13 +76,31 @@ fun OtpScreen(navController: NavController) {
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center, fontSize = 24.sp),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
}
Spacer(modifier = Modifier.height(48.dp))
Button(
onClick = { navController.navigate("create_profile") },
onClick = {
scope.launch {
authManager.login(phoneNumber, otp.value)
.onSuccess { response ->
if (isSignInFlow) {
// For existing users, always go to the success screen.
navController.navigate("success") { popUpTo("login") { inclusive = true } }
} else {
// For new users, check if a profile needs to be created.
if (response.needs_profile) {
navController.navigate("create_profile/$name")
} else {
navController.navigate("success") { popUpTo("login") { inclusive = true } }
}
}
}
.onFailure {
Toast.makeText(context, "Invalid or expired OTP", Toast.LENGTH_SHORT).show()
}
}
},
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFE9A00)),
modifier = Modifier
@ -101,6 +118,6 @@ fun OtpScreen(navController: NavController) {
@Composable
fun OtpScreenPreview() {
LivingAi_LgTheme {
OtpScreen(rememberNavController())
OtpScreen(rememberNavController(), "+919876543210", "John Doe")
}
}

View File

@ -0,0 +1,152 @@
package com.example.livingai_lg.ui.login
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.example.livingai_lg.api.AuthApiClient
import com.example.livingai_lg.api.AuthManager
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.ui.theme.*
import kotlinx.coroutines.launch
@Composable
fun SignInScreen(navController: NavController) {
val phoneNumber = remember { mutableStateOf("") }
val isPhoneNumberValid = remember(phoneNumber.value) { phoneNumber.value.length == 10 && phoneNumber.value.all { it.isDigit() } }
val context = LocalContext.current
val scope = rememberCoroutineScope()
val authManager = remember { AuthManager(context, AuthApiClient("http://10.0.2.2:3000"), TokenManager(context)) }
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 36.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(140.dp))
Row {
Text("Farm", fontSize = 32.sp, fontWeight = FontWeight.Medium, color = Color(0xFFE17100))
Text("Market", fontSize = 32.sp, fontWeight = FontWeight.Medium, color = Color.Black)
}
Text("Welcome back!", fontSize = 16.sp, color = Color(0xFF4A5565))
Spacer(modifier = Modifier.height(128.dp))
Text(
text = "Enter Phone Number",
fontSize = 16.sp,
color = Color(0xFF364153),
fontWeight = FontWeight.Medium,
modifier = Modifier.align(Alignment.Start).padding(start = 21.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.width(65.dp)
.height(52.dp)
.shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp))
.background(color = Color.White.copy(alpha = 0.9f), shape = RoundedCornerShape(16.dp))
.border(width = 1.dp, color = Color.Black.copy(alpha = 0.07f), shape = RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) {
Text("+91", fontSize = 16.sp, color = Color(0xFF0A0A0A))
}
Spacer(modifier = Modifier.width(6.dp))
TextField(
value = phoneNumber.value,
onValueChange = { phoneNumber.value = it },
placeholder = { Text("Enter your Phone Number", color = Color(0xFF99A1AF)) },
leadingIcon = { Icon(Icons.Default.Phone, contentDescription = null, tint = Color(0xFF99A1AF)) },
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.White.copy(alpha = 0.9f),
unfocusedContainerColor = Color.White.copy(alpha = 0.9f),
disabledContainerColor = Color.White.copy(alpha = 0.9f),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
isError = phoneNumber.value.isNotEmpty() && !isPhoneNumberValid,
shape = RoundedCornerShape(16.dp),
modifier = Modifier.weight(1f).height(52.dp).shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
singleLine = true
)
}
if (phoneNumber.value.isNotEmpty() && !isPhoneNumberValid) {
Text(
text = "Please enter a valid 10-digit phone number",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
val fullPhoneNumber = "+91${phoneNumber.value}"
scope.launch {
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
// For existing user, name is not needed, so we pass a placeholder
navController.navigate("otp/$fullPhoneNumber/existing_user")
}
.onFailure {
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
}
}
},
enabled = isPhoneNumberValid,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFE9A00),
disabledContainerColor = Color(0xFFF8DDA7),
contentColor = Color.White,
disabledContentColor = Color.White.copy(alpha = 0.7f)
),
modifier = Modifier.fillMaxWidth().height(56.dp).shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp))
) {
Text("Sign In", fontSize = 16.sp, fontWeight = FontWeight.Medium)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun SignInScreenPreview() {
LivingAi_LgTheme {
SignInScreen(rememberNavController())
}
}

View File

@ -1,6 +1,6 @@
package com.example.livingai_lg.ui.login
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@ -10,20 +10,14 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDecoration
@ -32,12 +26,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import com.example.livingai_lg.api.AuthApiClient
import com.example.livingai_lg.api.AuthManager
import com.example.livingai_lg.api.TokenManager
import com.example.livingai_lg.ui.theme.*
import kotlinx.coroutines.launch
@Composable
fun SignUpScreen(navController: NavController) {
val name = remember { mutableStateOf("") }
val phoneNumber = remember { mutableStateOf("") }
val isPhoneNumberValid = remember(phoneNumber.value) { phoneNumber.value.length == 10 && phoneNumber.value.all { it.isDigit() } }
val context = LocalContext.current
val scope = rememberCoroutineScope()
// Use 10.0.2.2 to connect to host machine's localhost from emulator
val authManager = remember { AuthManager(context, AuthApiClient("http://10.0.2.2:3000"), TokenManager(context)) }
Box(
modifier = Modifier
@ -48,12 +51,6 @@ fun SignUpScreen(navController: NavController) {
)
)
) {
// Decorative elements from LoginScreen
Box(
modifier = Modifier.fillMaxSize()
) {
// ...
}
Column(
modifier = Modifier
.fillMaxSize()
@ -129,19 +126,45 @@ fun SignUpScreen(navController: NavController) {
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
isError = phoneNumber.value.isNotEmpty() && !isPhoneNumberValid,
shape = RoundedCornerShape(16.dp),
modifier = Modifier.weight(1f).height(52.dp).shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
singleLine = true
)
}
if (phoneNumber.value.isNotEmpty() && !isPhoneNumberValid) {
Text(
text = "Please enter a valid 10-digit phone number",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = { navController.navigate("otp") },
onClick = {
val fullPhoneNumber = "+91${phoneNumber.value}"
scope.launch {
authManager.requestOtp(fullPhoneNumber)
.onSuccess {
navController.navigate("otp/$fullPhoneNumber/${name.value}")
}
.onFailure {
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
}
}
},
enabled = isPhoneNumberValid,
shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFE9A00)),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFE9A00),
disabledContainerColor = Color(0xFFF8DDA7),
contentColor = Color.White,
disabledContentColor = Color.White.copy(alpha = 0.7f)
),
modifier = Modifier.fillMaxWidth().height(56.dp).shadow(elevation = 4.dp, shape = RoundedCornerShape(16.dp))
) {
Text("Sign In", color = Color.White, fontSize = 16.sp, fontWeight = FontWeight.Medium)
@ -150,7 +173,7 @@ fun SignUpScreen(navController: NavController) {
Spacer(modifier = Modifier.height(32.dp))
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
Text("Don\'t have an account? ", color = Color(0xFF4A5565), fontSize = 16.sp)
Text("Don't have an account? ", color = Color(0xFF4A5565), fontSize = 16.sp)
Text(
text = "Sign up",
color = Color(0xFFE17100),

View File

@ -0,0 +1,56 @@
package com.example.livingai_lg.ui.login
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.example.livingai_lg.ui.theme.LightCream
import com.example.livingai_lg.ui.theme.LighterCream
import com.example.livingai_lg.ui.theme.LightestGreen
import com.example.livingai_lg.ui.theme.LivingAi_LgTheme
@Composable
fun SuccessScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(LightCream, LighterCream, LightestGreen)
)
),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Success!",
fontSize = 48.sp,
fontWeight = FontWeight.Bold
)
Text(
text = "Your profile has been created.",
fontSize = 24.sp
)
}
}
}
@Preview(showBackground = true)
@Composable
fun SuccessScreenPreview() {
LivingAi_LgTheme {
SuccessScreen()
}
}

View File

@ -1,13 +1,17 @@
[versions]
agp = "8.13.1"
kotlin = "2.0.21"
coreKtx = "1.10.1"
agp = "8.2.2"
kotlin = "2.0.0"
coreKtx = "1.13.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.3"
activityCompose = "1.9.0"
composeBom = "2024.06.00"
navigationCompose = "2.7.7"
ktor = "2.3.12"
kotlinxSerialization = "1.6.3"
securityCrypto = "1.1.0-alpha06"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -24,9 +28,22 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
# Ktor
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" }
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
# Kotlinx Serialization
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
# AndroidX Security
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "securityCrypto" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }