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.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
} }
android { android {
namespace = "com.example.livingai_lg" namespace = "com.example.livingai_lg"
compileSdk { compileSdk = 34
version = release(36)
}
defaultConfig { defaultConfig {
applicationId = "com.example.livingai_lg" applicationId = "com.example.livingai_lg"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -30,14 +29,15 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "1.8"
} }
buildFeatures { buildFeatures {
compose = true compose = true
buildConfig = true
} }
} }
@ -50,7 +50,20 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) 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) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -10,11 +12,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.LivingAi_Lg"> android:theme="@style/Theme.LivingAi_Lg"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.LivingAi_Lg"> android:theme="@style/Theme.LivingAi_Lg">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -1,17 +1,15 @@
package com.example.livingai_lg package com.example.livingai_lg
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.livingai_lg.ui.login.LoginScreen import androidx.navigation.navArgument
import com.example.livingai_lg.ui.login.SignUpScreen import com.example.livingai_lg.ui.login.*
import com.example.livingai_lg.ui.login.OtpScreen
import com.example.livingai_lg.ui.login.CreateProfileScreen
import com.example.livingai_lg.ui.theme.LivingAi_LgTheme import com.example.livingai_lg.ui.theme.LivingAi_LgTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -28,11 +26,32 @@ class MainActivity : ComponentActivity() {
composable("signup") { composable("signup") {
SignUpScreen(navController = navController) SignUpScreen(navController = navController)
} }
composable("otp") { composable("signin") {
OtpScreen(navController = navController) SignInScreen(navController = navController)
} }
composable("create_profile") { composable(
CreateProfileScreen(navController = navController) "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 package com.example.livingai_lg.ui.login
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -7,11 +8,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -20,10 +24,30 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.example.livingai_lg.R 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 com.example.livingai_lg.ui.theme.*
import kotlinx.coroutines.launch
@Composable @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( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -46,26 +70,26 @@ fun CreateProfileScreen(navController: NavController) {
Spacer(modifier = Modifier.height(64.dp)) 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)) 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)) 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)) 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 @Composable
fun ProfileTypeItem(text: String, icon: Int) { fun ProfileTypeItem(text: String, icon: Int, onClick: () -> Unit) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(72.dp) .height(72.dp)
.shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)) .shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp))
.background(Color.White, RoundedCornerShape(16.dp)) .background(Color.White, RoundedCornerShape(16.dp))
.clickable { /* TODO */ } .clickable(onClick = onClick)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -81,6 +105,6 @@ fun ProfileTypeItem(text: String, icon: Int) {
@Composable @Composable
fun CreateProfileScreenPreview() { fun CreateProfileScreenPreview() {
LivingAi_LgTheme { LivingAi_LgTheme {
CreateProfileScreen(rememberNavController()) CreateProfileScreen(rememberNavController(), "John Doe")
} }
} }

View File

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

View File

@ -1,24 +1,18 @@
package com.example.livingai_lg.ui.login package com.example.livingai_lg.ui.login
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.*
import androidx.compose.material3.ButtonDefaults import androidx.compose.runtime.*
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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign 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.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController 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 com.example.livingai_lg.ui.theme.*
import kotlinx.coroutines.launch
@Composable @Composable
fun OtpScreen(navController: NavController) { fun OtpScreen(navController: NavController, phoneNumber: String, name: String) {
val otp = remember { mutableStateOf("") } 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( Box(
modifier = Modifier modifier = Modifier
@ -54,36 +58,49 @@ fun OtpScreen(navController: NavController) {
Spacer(modifier = Modifier.height(32.dp)) Spacer(modifier = Modifier.height(32.dp))
Row( TextField(
modifier = Modifier.fillMaxWidth(), value = otp.value,
horizontalArrangement = Arrangement.SpaceEvenly onValueChange = { if (it.length <= 6) otp.value = it },
) { modifier = Modifier
for (i in 0..3) { .fillMaxWidth()
TextField( .height(60.dp)
value = if (otp.value.length > i) otp.value[i].toString() else "", .shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)),
onValueChange = { if (it.length <= 1) { /* TODO */ } }, shape = RoundedCornerShape(16.dp),
modifier = Modifier colors = TextFieldDefaults.colors(
.width(60.dp) focusedContainerColor = Color.White.copy(alpha = 0.9f),
.height(60.dp) unfocusedContainerColor = Color.White.copy(alpha = 0.9f),
.shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)), disabledContainerColor = Color.White.copy(alpha = 0.9f),
shape = RoundedCornerShape(16.dp), focusedIndicatorColor = Color.Transparent,
colors = TextFieldDefaults.colors( unfocusedIndicatorColor = Color.Transparent,
focusedContainerColor = Color.White.copy(alpha = 0.9f), ),
unfocusedContainerColor = Color.White.copy(alpha = 0.9f), textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center, fontSize = 24.sp),
disabledContainerColor = Color.White.copy(alpha = 0.9f), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
focusedIndicatorColor = Color.Transparent, )
unfocusedIndicatorColor = Color.Transparent,
),
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center, fontSize = 24.sp),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
}
Spacer(modifier = Modifier.height(48.dp)) Spacer(modifier = Modifier.height(48.dp))
Button( 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), shape = RoundedCornerShape(16.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFE9A00)), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFE9A00)),
modifier = Modifier modifier = Modifier
@ -101,6 +118,6 @@ fun OtpScreen(navController: NavController) {
@Composable @Composable
fun OtpScreenPreview() { fun OtpScreenPreview() {
LivingAi_LgTheme { 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,29 +1,23 @@
package com.example.livingai_lg.ui.login package com.example.livingai_lg.ui.login
import android.widget.Toast
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Phone import androidx.compose.material.icons.filled.Phone
import androidx.compose.material3.Button import androidx.compose.material3.*
import androidx.compose.material3.ButtonDefaults import androidx.compose.runtime.*
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.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDecoration 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.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController 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 com.example.livingai_lg.ui.theme.*
import kotlinx.coroutines.launch
@Composable @Composable
fun SignUpScreen(navController: NavController) { fun SignUpScreen(navController: NavController) {
val name = remember { mutableStateOf("") } val name = remember { mutableStateOf("") }
val phoneNumber = 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( Box(
modifier = Modifier modifier = Modifier
@ -48,12 +51,6 @@ fun SignUpScreen(navController: NavController) {
) )
) )
) { ) {
// Decorative elements from LoginScreen
Box(
modifier = Modifier.fillMaxSize()
) {
// ...
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -129,19 +126,45 @@ fun SignUpScreen(navController: NavController) {
focusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent,
), ),
isError = phoneNumber.value.isNotEmpty() && !isPhoneNumberValid,
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
modifier = Modifier.weight(1f).height(52.dp).shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)), modifier = Modifier.weight(1f).height(52.dp).shadow(elevation = 1.dp, shape = RoundedCornerShape(16.dp)),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
singleLine = true 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)) Spacer(modifier = Modifier.height(32.dp))
Button( 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), 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)) 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) 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)) Spacer(modifier = Modifier.height(32.dp))
Row(modifier = Modifier.align(Alignment.CenterHorizontally)) { 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(
text = "Sign up", text = "Sign up",
color = Color(0xFFE17100), 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] [versions]
agp = "8.13.1" agp = "8.2.2"
kotlin = "2.0.21" kotlin = "2.0.0"
coreKtx = "1.10.1" coreKtx = "1.13.1"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.2.1"
espressoCore = "3.5.1" espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.6.1" lifecycleRuntimeKtx = "2.8.3"
activityCompose = "1.8.0" activityCompose = "1.9.0"
composeBom = "2024.09.00" composeBom = "2024.06.00"
navigationCompose = "2.7.7"
ktor = "2.3.12"
kotlinxSerialization = "1.6.3"
securityCrypto = "1.1.0-alpha06"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", 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" }