Updated Login
This commit is contained in:
parent
8d2808734a
commit
d6b099f921
|
|
@ -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>
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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?
|
||||||
|
)
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue