Navigate to BuyScreen if the user is already authenticated.
OTP input improvements.
This commit is contained in:
parent
f8882c1dcc
commit
1277ae09b6
|
|
@ -113,7 +113,7 @@ fun AppNavigation(
|
||||||
* --------------------------------------------------- */
|
* --------------------------------------------------- */
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = AppScreen.LANDING
|
startDestination = if(authState is AuthState.Authenticated) AppScreen.BUY_ANIMALS else AppScreen.LANDING
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/* ---------------- AUTH ---------------- */
|
/* ---------------- AUTH ---------------- */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.example.livingai_lg.ui.screens.auth
|
package com.example.livingai_lg.ui.screens.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -8,6 +9,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
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.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -18,6 +20,8 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Shadow
|
import androidx.compose.ui.graphics.Shadow
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
@ -43,6 +47,10 @@ import kotlinx.coroutines.launch
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,16 +67,24 @@ fun OtpScreen(
|
||||||
signupDistrict: String? = null,
|
signupDistrict: String? = null,
|
||||||
signupVillage: String? = null,
|
signupVillage: String? = null,
|
||||||
) {
|
) {
|
||||||
val otp = remember { mutableStateOf("") }
|
val otpLength = 6
|
||||||
|
val otp = remember { mutableStateOf(List<String>(otpLength) { "" }) }
|
||||||
|
var focusedIndex by remember { mutableStateOf(0) }
|
||||||
val context = LocalContext.current.applicationContext
|
val context = LocalContext.current.applicationContext
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
val authManager = remember { AuthManager(context, AuthApiClient(context), TokenManager(context)) }
|
||||||
|
|
||||||
// Countdown timer state (2 minutes = 120 seconds)
|
fun updateOtpAt(index: Int, value: String) {
|
||||||
var countdownSeconds by remember { mutableStateOf(120) }
|
otp.value = otp.value.mapIndexed { i, old ->
|
||||||
|
if (i == index) value else old
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start countdown when screen is composed
|
// Countdown timer state (1 minutes = 60 seconds)
|
||||||
LaunchedEffect(Unit) {
|
var countdownSeconds by remember { mutableStateOf(10) }
|
||||||
|
var countdownKey by remember { mutableStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(countdownKey) {
|
||||||
while (countdownSeconds > 0) {
|
while (countdownSeconds > 0) {
|
||||||
delay(1000) // Wait 1 second
|
delay(1000) // Wait 1 second
|
||||||
countdownSeconds--
|
countdownSeconds--
|
||||||
|
|
@ -142,34 +158,68 @@ Column(
|
||||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
Modifier.fillMaxWidth().padding(horizontal = 12.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(s(17f))
|
horizontalArrangement = Arrangement.spacedBy(s(17f))
|
||||||
) {
|
) {
|
||||||
OtpInputRow(
|
OTPInputTextFields(
|
||||||
otpLength = 6,
|
otpLength = otpLength,
|
||||||
scale = scale,
|
otpValues = otp.value,
|
||||||
otp = otp.value,
|
onUpdateOtpValuesByIndex = { index, value ->
|
||||||
onOtpChange = { if (it.length <= 6) otp.value = it }
|
updateOtpAt(index,value)
|
||||||
|
},
|
||||||
|
onOtpInputComplete = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Countdown Timer
|
// Countdown Timer
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
Text(
|
if (countdownSeconds > 0) {
|
||||||
text = countdownText,
|
Text(
|
||||||
color = Color(0xFF927B5E),
|
text = countdownText,
|
||||||
fontSize = fs(16f),
|
color = Color(0xFF927B5E),
|
||||||
fontWeight = FontWeight.Medium,
|
fontSize = fs(16f),
|
||||||
modifier = Modifier
|
fontWeight = FontWeight.Medium,
|
||||||
.fillMaxWidth()
|
modifier = Modifier
|
||||||
.padding(vertical = 16.dp),
|
.fillMaxWidth()
|
||||||
textAlign = TextAlign.Center,
|
.padding(vertical = 16.dp),
|
||||||
style = LocalTextStyle.current.copy(
|
textAlign = TextAlign.Center,
|
||||||
shadow = Shadow(
|
style = LocalTextStyle.current.copy(
|
||||||
color = Color.Black.copy(alpha = 0.15f),
|
shadow = Shadow(
|
||||||
offset = Offset(0f, s(2f).value),
|
color = Color.Black.copy(alpha = 0.15f),
|
||||||
blurRadius = s(2f).value
|
offset = Offset(0f, s(2f).value),
|
||||||
|
blurRadius = s(2f).value
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "Resend OTP",
|
||||||
|
color = Color(0xFFE17100),
|
||||||
|
fontSize = fs(16f),
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
.clickable {
|
||||||
|
// 🔹 Stub for now
|
||||||
|
scope.launch {
|
||||||
|
|
||||||
|
authManager.requestOtp(phoneNumber)
|
||||||
|
.onSuccess {
|
||||||
|
// OTP sent successfully, navigate to OTP screen with signup data
|
||||||
|
// Pass signup form data through the callback
|
||||||
|
Toast.makeText(context, "Resent OTP",Toast.LENGTH_LONG).show()
|
||||||
|
countdownSeconds = 10
|
||||||
|
countdownKey++
|
||||||
|
}
|
||||||
|
.onFailure {
|
||||||
|
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||||
|
Log.e("OtpScreen", "Failed to send OTP ${it.message}", it)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Continue Button
|
// Continue Button
|
||||||
|
|
@ -199,14 +249,20 @@ Column(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
val otpString = otp.value.joinToString("") { it?.toString() ?: "" }
|
||||||
|
|
||||||
|
if (otpString.length < otpLength) {
|
||||||
|
Toast.makeText(context, "Please enter full OTP", Toast.LENGTH_SHORT).show()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
if (isSignupFlow) {
|
if (isSignupFlow) {
|
||||||
// For signup flow: Verify OTP first, then call signup API to update user with name/location
|
// For signup flow: Verify OTP first, then call signup API to update user with name/location
|
||||||
android.util.Log.d("OTPScreen", "Signup flow: Verifying OTP...")
|
android.util.Log.d("OTPScreen", "Signup flow: Verifying OTP...")
|
||||||
authManager.login(phoneNumber, otp.value)
|
authManager.login(phoneNumber, otpString)
|
||||||
.onSuccess { verifyResponse ->
|
.onSuccess { verifyResponse ->
|
||||||
android.util.Log.d("OTPScreen", "OTP verified successfully. Calling signup API...")
|
android.util.Log.d("OTPScreen", "OTP verified successfully. Calling signup API...")
|
||||||
// OTP verified successfully - user is now logged in
|
// OTP verified successfully - user is now logged in
|
||||||
// Refresh auth status in MainViewModel so AppNavigation knows user is authenticated
|
|
||||||
mainViewModel.refreshAuthStatus()
|
mainViewModel.refreshAuthStatus()
|
||||||
// Now call signup API to update user with name and location
|
// Now call signup API to update user with name and location
|
||||||
val signupRequest = SignupRequest(
|
val signupRequest = SignupRequest(
|
||||||
|
|
@ -245,8 +301,7 @@ Column(
|
||||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
onCreateProfile(name)
|
onCreateProfile(name)
|
||||||
} else {
|
} else {
|
||||||
// Don't manually navigate - let AppNavigation handle it
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
|
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -258,7 +313,7 @@ Column(
|
||||||
// Navigate to success anyway since verify-otp succeeded
|
// Navigate to success anyway since verify-otp succeeded
|
||||||
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
|
android.util.Log.d("OTPScreen", "Signup API returned false, but OTP verified. Navigating anyway...")
|
||||||
val needsProfile = verifyResponse.needsProfile
|
val needsProfile = verifyResponse.needsProfile
|
||||||
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
|
// Refresh auth status
|
||||||
mainViewModel.refreshAuthStatus()
|
mainViewModel.refreshAuthStatus()
|
||||||
try {
|
try {
|
||||||
if (needsProfile) {
|
if (needsProfile) {
|
||||||
|
|
@ -266,7 +321,7 @@ Column(
|
||||||
onCreateProfile(name)
|
onCreateProfile(name)
|
||||||
} else {
|
} else {
|
||||||
// Don't manually navigate - let AppNavigation handle it
|
// Don't manually navigate - let AppNavigation handle it
|
||||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||||
onSuccess()
|
onSuccess()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
@ -288,7 +343,7 @@ Column(
|
||||||
// For existing users, use existing logic
|
// For existing users, use existing logic
|
||||||
val needsProfile = verifyResponse.needsProfile
|
val needsProfile = verifyResponse.needsProfile
|
||||||
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$needsProfile")
|
android.util.Log.d("OTPScreen", "Navigating despite signup failure. needsProfile=$needsProfile")
|
||||||
// Refresh auth status - this will trigger navigation via AppNavigation's LaunchedEffect
|
// Refresh auth status
|
||||||
mainViewModel.refreshAuthStatus()
|
mainViewModel.refreshAuthStatus()
|
||||||
try {
|
try {
|
||||||
// If this is a signup flow and signup failed, treat as new user
|
// If this is a signup flow and signup failed, treat as new user
|
||||||
|
|
@ -299,8 +354,8 @@ Column(
|
||||||
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
android.util.Log.d("OTPScreen", "Navigating to create profile screen with name: $name")
|
||||||
onCreateProfile(name)
|
onCreateProfile(name)
|
||||||
} else {
|
} else {
|
||||||
// Don't manually navigate - let AppNavigation handle it
|
android.util.Log.d("OTPScreen", "Signup successful - auth state updated")
|
||||||
android.util.Log.d("OTPScreen", "Signup successful - auth state updated, navigation will happen automatically")
|
onSuccess()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
android.util.Log.e("OTPScreen", "Navigation error: ${e.message}", e)
|
||||||
|
|
@ -321,7 +376,7 @@ Column(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For sign-in flow: Just verify OTP and login
|
// For sign-in flow: Just verify OTP and login
|
||||||
authManager.login(phoneNumber, otp.value)
|
authManager.login(phoneNumber, otpString)
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
|
android.util.Log.d("OTPScreen", "Sign-in OTP verified. needsProfile=${response.needsProfile}")
|
||||||
// Tokens are now saved (synchronously via commit())
|
// Tokens are now saved (synchronously via commit())
|
||||||
|
|
@ -379,138 +434,111 @@ Column(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OtpInputRow(
|
fun OTPInputTextFields(
|
||||||
otpLength: Int,
|
otpLength: Int,
|
||||||
scale: Float,
|
onUpdateOtpValuesByIndex: (Int, String) -> Unit,
|
||||||
otp: String,
|
onOtpInputComplete: () -> Unit,
|
||||||
onOtpChange: (String) -> Unit
|
modifier: Modifier = Modifier,
|
||||||
|
otpValues: List<String> = List(otpLength) { "" }, // Pass this as default for future reference
|
||||||
|
isError: Boolean = false,
|
||||||
) {
|
) {
|
||||||
BoxWithConstraints(
|
val focusRequesters = List(otpLength) { FocusRequester() }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
val focusManager = LocalFocusManager.current
|
||||||
contentAlignment = Alignment.Center
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
// horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally)
|
||||||
) {
|
) {
|
||||||
val maxRowWidth = maxWidth
|
otpValues.forEachIndexed { index, value ->
|
||||||
|
OutlinedTextField(
|
||||||
val spacing = (12f * scale).dp
|
modifier = Modifier
|
||||||
val totalSpacing = spacing * (otpLength - 1)
|
.weight(1f)
|
||||||
|
// .width(64.dp)
|
||||||
val boxWidth = ((maxRowWidth - totalSpacing) / otpLength)
|
.padding(6.dp)
|
||||||
.coerceAtMost((66f * scale).dp)
|
.focusRequester(focusRequesters[index])
|
||||||
|
.background(Color.White)
|
||||||
val focusRequesters = remember {
|
.onKeyEvent { keyEvent ->
|
||||||
List(otpLength) { FocusRequester() }
|
if (keyEvent.key == Key.Backspace) {
|
||||||
}
|
if (otpValues[index].isEmpty() && index > 0) {
|
||||||
|
onUpdateOtpValuesByIndex(index, "")
|
||||||
Row(
|
focusRequesters[index - 1].requestFocus()
|
||||||
horizontalArrangement = Arrangement.spacedBy(spacing),
|
} else {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
onUpdateOtpValuesByIndex(index, "")
|
||||||
) {
|
}
|
||||||
repeat(otpLength) { index ->
|
true
|
||||||
OtpBox(
|
} else {
|
||||||
index = index,
|
false
|
||||||
otp = otp,
|
}
|
||||||
scale = scale,
|
|
||||||
width = boxWidth, // 👈 fixed width
|
|
||||||
focusRequester = focusRequesters[index],
|
|
||||||
onRequestFocus = {
|
|
||||||
val firstEmpty = otp.length.coerceAtMost(otpLength - 1)
|
|
||||||
focusRequesters[firstEmpty].requestFocus()
|
|
||||||
},
|
},
|
||||||
onNextFocus = {
|
value = value,
|
||||||
if (index + 1 < otpLength) focusRequesters[index + 1].requestFocus()
|
onValueChange = { newValue ->
|
||||||
|
// To use OTP code copied from keyboard
|
||||||
|
if (newValue.length == otpLength) {
|
||||||
|
for (i in otpValues.indices) {
|
||||||
|
onUpdateOtpValuesByIndex(
|
||||||
|
i,
|
||||||
|
if (i < newValue.length && newValue[i].isDigit()) newValue[i].toString() else ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyboardController?.hide()
|
||||||
|
onOtpInputComplete() // you should validate the otp values first for, if it is only digits or isNotEmpty
|
||||||
|
} else if (newValue.length <= 1) {
|
||||||
|
onUpdateOtpValuesByIndex(index, newValue)
|
||||||
|
if (newValue.isNotEmpty()) {
|
||||||
|
if (index < otpLength - 1) {
|
||||||
|
focusRequesters[index + 1].requestFocus()
|
||||||
|
} else {
|
||||||
|
keyboardController?.hide()
|
||||||
|
focusManager.clearFocus()
|
||||||
|
onOtpInputComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (index < otpLength - 1) focusRequesters[index + 1].requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Number,
|
||||||
|
imeAction = if (index == otpLength - 1) ImeAction.Done else ImeAction.Next
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onNext = {
|
||||||
|
if (index < otpLength - 1) {
|
||||||
|
focusRequesters[index + 1].requestFocus()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onPrevFocus = {
|
onDone = {
|
||||||
if (index - 1 >= 0) focusRequesters[index - 1].requestFocus()
|
keyboardController?.hide()
|
||||||
},
|
focusManager.clearFocus()
|
||||||
onChange = onOtpChange
|
onOtpInputComplete()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
isError = isError,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
fontSize = MaterialTheme.typography.bodyLarge.fontSize
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(value) {
|
||||||
|
if (otpValues.all { it.isNotEmpty() }) {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
onOtpInputComplete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun OtpBox(
|
|
||||||
index: Int,
|
|
||||||
otp: String,
|
|
||||||
scale: Float,
|
|
||||||
width: Dp,
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
onRequestFocus: () -> Unit,
|
|
||||||
onNextFocus: () -> Unit,
|
|
||||||
onPrevFocus: () -> Unit,
|
|
||||||
onChange: (String) -> Unit
|
|
||||||
) {
|
|
||||||
val boxH = 52f * scale
|
|
||||||
val radius = 16f * scale
|
|
||||||
|
|
||||||
val char = otp.getOrNull(index)?.toString() ?: ""
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(width, boxH.dp)
|
|
||||||
.shadow((4f * scale).dp, RoundedCornerShape(radius.dp))
|
|
||||||
.background(Color.White, RoundedCornerShape(radius.dp))
|
|
||||||
.clickable { onRequestFocus() },
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
BasicTextField(
|
|
||||||
value = char,
|
|
||||||
onValueChange = { new ->
|
|
||||||
when {
|
|
||||||
// DIGIT ENTERED
|
|
||||||
new.matches(Regex("\\d")) -> {
|
|
||||||
val updated = otp.padEnd(index + 1, ' ').toMutableList()
|
|
||||||
updated[index] = new.first()
|
|
||||||
onChange(updated.joinToString("").trim())
|
|
||||||
onNextFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
// BACKSPACE WHEN CHARACTER EXISTS
|
|
||||||
new.isEmpty() && char.isNotEmpty() -> {
|
|
||||||
val updated = otp.toMutableList()
|
|
||||||
updated.removeAt(index)
|
|
||||||
onChange(updated.joinToString(""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.onPreviewKeyEvent { event ->
|
|
||||||
if (event.type == KeyEventType.KeyDown &&
|
|
||||||
event.key == Key.Backspace &&
|
|
||||||
char.isEmpty() &&
|
|
||||||
index > 0
|
|
||||||
) {
|
|
||||||
val updated = otp.toMutableList()
|
|
||||||
updated.removeAt(index - 1) // 👈 clear previous box
|
|
||||||
onChange(updated.joinToString(""))
|
|
||||||
onPrevFocus()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textStyle = LocalTextStyle.current.copy(
|
|
||||||
fontSize = (24f * scale).sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
),
|
|
||||||
keyboardOptions = KeyboardOptions(
|
|
||||||
keyboardType = KeyboardType.NumberPassword
|
|
||||||
),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequesters.first().requestFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.example.livingai_lg.ui.screens.auth
|
package com.example.livingai_lg.ui.screens.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
|
@ -108,6 +109,7 @@ fun SignInScreen(
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show()
|
||||||
|
Log.e("SignInScreen", "Failed to send OTP ${it.message}", it)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// User doesn't exist (shouldn't happen if API works correctly)
|
// User doesn't exist (shouldn't happen if API works correctly)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue