otp page fix.

This commit is contained in:
ankitsaraf 2025-12-19 20:24:17 +05:30
parent 6b634f40b7
commit 6628daed50
7 changed files with 257 additions and 185 deletions

View File

@ -1,7 +1,9 @@
package com.example.livingai_lg.ui.screens
package com.example.livingai_lg.ui.components
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
@ -16,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.example.livingai_lg.ui.screens.FilterScreen
@Composable
fun FilterOverlay(
@ -23,29 +26,40 @@ fun FilterOverlay(
onDismiss: () -> Unit,
onSubmitClick: () -> Unit = {},
) {
if (!visible) return
BackHandler { onDismiss() }
BackHandler(enabled = visible) { onDismiss() }
Box(
modifier = Modifier
.fillMaxSize()
modifier = Modifier.fillMaxSize()
) {
// Dimmed background
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.35f))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onDismiss() }
)
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally { fullWidth -> fullWidth },
exit = slideOutHorizontally { fullWidth -> fullWidth },
modifier = Modifier.fillMaxHeight()
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.35f))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onDismiss() }
)
}
// Sliding panel
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally(
initialOffsetX = { it } // from right
),
exit = slideOutHorizontally(
targetOffsetX = { it } // to right
),
modifier = Modifier
.fillMaxHeight()
.align(Alignment.CenterEnd)
) {
FilterScreen(
onBackClick = onDismiss,

View File

@ -1,6 +1,9 @@
package com.example.livingai_lg.ui.components
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
@ -10,11 +13,16 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.example.livingai_lg.ui.models.SortField
import com.example.livingai_lg.ui.screens.FilterScreen
import com.example.livingai_lg.ui.screens.SortScreen
@Composable
@ -23,17 +31,28 @@ fun SortOverlay(
onApplyClick: (selected: List<SortField>) -> Unit,
onDismiss: () -> Unit,
) {
if (!visible) return
BackHandler(enabled = visible) { onDismiss() }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.35f))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onDismiss() } // tap outside closes
) {
Box(modifier = Modifier.fillMaxSize()) {
// Dim background
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.35f))
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onDismiss() }
)
}
// Slide-in panel from LEFT
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally(
@ -41,33 +60,29 @@ fun SortOverlay(
),
exit = slideOutHorizontally(
targetOffsetX = { -it }
)
),
modifier = Modifier.align(Alignment.CenterStart)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(0.85f) // 👈 DOES NOT cover full screen
.fillMaxHeight(0.85f)
.fillMaxWidth(0.85f)
.background(Color(0xFFF7F4EE))
) {
// Prevent click-through
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {}
) {
SortScreen(
onApplyClick = { selected ->
onApplyClick(selected)
onDismiss()
// TODO: apply sort
},
onCancelClick = onDismiss,
onBackClick = onDismiss
.clip(
RoundedCornerShape(
topEnd = 24.dp,
bottomEnd = 24.dp
)
)
}
) {
SortScreen(
onApplyClick = { selected ->
onApplyClick(selected)
onDismiss()
},
onCancelClick = onDismiss,
onBackClick = onDismiss
)
}
}
}

View File

@ -96,7 +96,7 @@ object AppScreen {
object Graph {
const val AUTH = "auth"
const val MAIN = "auth"
const val MAIN = "main"
fun auth(route: String)=
"$AUTH/$route"

View File

@ -35,6 +35,7 @@ import com.example.livingai_lg.ui.models.userProfile
import com.example.livingai_lg.R
import com.example.livingai_lg.ui.components.ActionPopup
import com.example.livingai_lg.ui.components.AddressSelectorOverlay
import com.example.livingai_lg.ui.components.FilterOverlay
import com.example.livingai_lg.ui.components.NotificationsOverlay
import com.example.livingai_lg.ui.components.SortOverlay
import com.example.livingai_lg.ui.models.sampleNotifications

View File

@ -15,7 +15,9 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.*
@ -75,7 +77,7 @@ fun FilterScreen(
Column(
modifier = Modifier
.fillMaxSize()
.fillMaxHeight()
.background(Color(0xFFF7F4EE))
) {
// Header
@ -95,7 +97,7 @@ fun FilterScreen(
) {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos,
contentDescription = "Back",
tint = Color(0xFF0A0A0A)
)

View File

@ -24,7 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowBackIos
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
@ -112,7 +114,7 @@ fun SortScreen(
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
imageVector = Icons.AutoMirrored.Filled.ArrowBackIos,
contentDescription = "Back",
tint = Color(0xFF0A0A0A)
)

View File

@ -34,8 +34,13 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.Dp
import kotlinx.coroutines.launch
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
@Composable
@ -79,158 +84,175 @@ fun OtpScreen(
fun s(v: Float) = (v * scale).dp // dp scaling
fun fs(v: Float) = (v * scale).sp // font scaling
// ---------------------------
// "Enter OTP" Title
// ---------------------------
Text(
text = "Enter OTP",
color = Color(0xFF927B5E),
fontSize = fs(20f),
fontWeight = FontWeight.Medium,
modifier = Modifier.offset(x = s(139f), y = s(279f)),
style = LocalTextStyle.current.copy(
shadow = Shadow(
color = Color.Black.copy(alpha = 0.25f),
offset = Offset(0f, s(4f).value),
blurRadius = s(4f).value
)
Column(
Modifier.fillMaxSize().padding(horizontal = 12.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// ---------------------------
// "Enter OTP" Title
// ---------------------------
Text(
text = "Enter OTP",
color = Color(0xFF927B5E),
fontSize = fs(20f),
fontWeight = FontWeight.Medium,
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
textAlign = TextAlign.Center,
style = LocalTextStyle.current.copy(
shadow = Shadow(
color = Color.Black.copy(alpha = 0.25f),
offset = Offset(0f, s(4f).value),
blurRadius = s(4f).value
)
)
)
// ---------------------------
// OTP 4-Box Input Row
// ---------------------------
// Row(
// Modifier.offset(x = s(38f), y = s(319f)),
// horizontalArrangement = Arrangement.spacedBy(s(17f))
// ) {
// repeat(4) { index ->
// OtpBox(
// index = index,
// otp = otp.value,
// onChange = { if (it.length <= 6) otp.value = it },
// scale = scale
// )
// }
// }
// ---------------------------
// OTP 4-Box Input Row
// ---------------------------
Row(
Modifier.fillMaxWidth().padding(horizontal = 12.dp),
horizontalArrangement = Arrangement.spacedBy(s(17f))
) {
OtpInputRow(
otpLength = 6,
scale = scale,
otp = otp.value,
onOtpChange = { if (it.length <= 6) otp.value = it }
)
// ---------------------------
// Continue Button
// ---------------------------
Box(
modifier = Modifier
.offset(x = s(57f), y = s(411f))
.size(s(279.25f), s(55.99f))
.shadow(
elevation = s(6f),
ambientColor = Color.Black.copy(alpha = 0.10f),
shape = RoundedCornerShape(s(16f)),
)
.shadow(
elevation = s(15f),
ambientColor = Color.Black.copy(alpha = 0.10f),
shape = RoundedCornerShape(s(16f)),
)
.background(
Brush.horizontalGradient(
listOf(Color(0xFFFD9900), Color(0xFFE17100))
),
shape = RoundedCornerShape(s(16f))
)
.clickable(
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() },
onClick = {
scope.launch {
authManager.login(phoneNumber, otp.value)
.onSuccess { response ->
if (isSignInFlow) {
// For existing users, always go to the success screen.
onSuccess()
}
// ---------------------------
// Continue Button
// ---------------------------
Box(
modifier = Modifier
.fillMaxWidth().padding(vertical = 16.dp, horizontal = 48.dp)
.size(s(279.25f), s(55.99f))
.shadow(
elevation = s(6f),
ambientColor = Color.Black.copy(alpha = 0.10f),
shape = RoundedCornerShape(s(16f)),
)
.shadow(
elevation = s(15f),
ambientColor = Color.Black.copy(alpha = 0.10f),
shape = RoundedCornerShape(s(16f)),
)
.background(
Brush.horizontalGradient(
listOf(Color(0xFFFD9900), Color(0xFFE17100))
),
shape = RoundedCornerShape(s(16f))
)
.clickable(
indication = LocalIndication.current,
interactionSource = remember { MutableInteractionSource() },
onClick = {
scope.launch {
authManager.login(phoneNumber, otp.value)
.onSuccess { response ->
if (isSignInFlow) {
// For existing users, always go to the success screen.
onSuccess()
} else {
// For new users, check if a profile needs to be created.
if (response.needsProfile) {
onCreateProfile(name)
} else {
// For new users, check if a profile needs to be created.
if (response.needsProfile) {
onCreateProfile(name)
} else {
onSuccess()
}
onSuccess()
}
}
.onFailure {
Toast.makeText(context, "Invalid or expired OTP", Toast.LENGTH_SHORT).show()
}
}
}
.onFailure {
Toast.makeText(
context,
"Invalid or expired OTP",
Toast.LENGTH_SHORT
).show()
}
}
),
contentAlignment = Alignment.Center
) {
Text(
"Continue",
color = Color.White,
fontSize = fs(16f),
fontWeight = FontWeight.Medium
)
}
}
),
contentAlignment = Alignment.Center
) {
Text(
"Continue",
color = Color.White,
fontSize = fs(16f),
fontWeight = FontWeight.Medium
)
}
}
}
}
@Composable
fun OtpInputRow(
otpLength: Int = 4,
otpLength: Int,
scale: Float,
otp: String,
onOtpChange: (String) -> Unit
) {
val focusRequesters = remember {
List(otpLength) { FocusRequester() }
}
BoxWithConstraints(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
val maxRowWidth = maxWidth
Row(horizontalArrangement = Arrangement.spacedBy((12 * scale).dp)) {
repeat(otpLength) { index ->
OtpBox(
index = index,
otp = otp,
scale = scale,
focusRequester = focusRequesters[index],
onRequestFocus = {
val firstEmpty = otp.length.coerceAtMost(otpLength - 1)
focusRequesters[firstEmpty].requestFocus()
},
onNextFocus = {
if (index + 1 < otpLength) {
focusRequesters[index + 1].requestFocus()
}
},
onPrevFocus = {
if (index - 1 >= 0) {
focusRequesters[index - 1].requestFocus()
}
},
onChange = onOtpChange
)
val spacing = (12f * scale).dp
val totalSpacing = spacing * (otpLength - 1)
val boxWidth = ((maxRowWidth - totalSpacing) / otpLength)
.coerceAtMost((66f * scale).dp)
val focusRequesters = remember {
List(otpLength) { FocusRequester() }
}
Row(
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalAlignment = Alignment.CenterVertically
) {
repeat(otpLength) { index ->
OtpBox(
index = index,
otp = otp,
scale = scale,
width = boxWidth, // 👈 fixed width
focusRequester = focusRequesters[index],
onRequestFocus = {
val firstEmpty = otp.length.coerceAtMost(otpLength - 1)
focusRequesters[firstEmpty].requestFocus()
},
onNextFocus = {
if (index + 1 < otpLength) focusRequesters[index + 1].requestFocus()
},
onPrevFocus = {
if (index - 1 >= 0) focusRequesters[index - 1].requestFocus()
},
onChange = onOtpChange
)
}
}
}
}
@Composable
private fun OtpBox(
index: Int,
otp: String,
scale: Float,
width: Dp,
focusRequester: FocusRequester,
onRequestFocus: () -> Unit,
onNextFocus: () -> Unit,
onPrevFocus: () -> Unit,
onChange: (String) -> Unit
) {
val boxW = 66f * scale
val boxH = 52f * scale
val radius = 16f * scale
@ -238,7 +260,7 @@ private fun OtpBox(
Box(
modifier = Modifier
.size(boxW.dp, boxH.dp)
.size(width, boxH.dp)
.shadow((4f * scale).dp, RoundedCornerShape(radius.dp))
.background(Color.White, RoundedCornerShape(radius.dp))
.clickable { onRequestFocus() },
@ -250,25 +272,38 @@ private fun OtpBox(
when {
// DIGIT ENTERED
new.matches(Regex("\\d")) -> {
val updated = otp.padEnd(index, ' ').toMutableList()
if (updated.size > index) updated[index] = new.first()
else updated.add(new.first())
val updated = otp.padEnd(index + 1, ' ').toMutableList()
updated[index] = new.first()
onChange(updated.joinToString("").trim())
onNextFocus()
}
// BACKSPACE
new.isEmpty() -> {
if (char.isNotEmpty()) {
val updated = otp.toMutableList()
updated.removeAt(index)
onChange(updated.joinToString(""))
} else {
onPrevFocus()
}
// 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,
@ -277,11 +312,14 @@ private fun OtpBox(
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.NumberPassword
),
singleLine = true,
modifier = Modifier
.focusRequester(focusRequester)
.align(Alignment.Center)
singleLine = true
)
}
}