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

View File

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

View File

@ -96,7 +96,7 @@ object AppScreen {
object Graph { object Graph {
const val AUTH = "auth" const val AUTH = "auth"
const val MAIN = "auth" const val MAIN = "main"
fun auth(route: String)= fun auth(route: String)=
"$AUTH/$route" "$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.R
import com.example.livingai_lg.ui.components.ActionPopup import com.example.livingai_lg.ui.components.ActionPopup
import com.example.livingai_lg.ui.components.AddressSelectorOverlay 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.NotificationsOverlay
import com.example.livingai_lg.ui.components.SortOverlay import com.example.livingai_lg.ui.components.SortOverlay
import com.example.livingai_lg.ui.models.sampleNotifications 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.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons 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.ArrowBack
import androidx.compose.material.icons.filled.ArrowForwardIos
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -75,7 +77,7 @@ fun FilterScreen(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxHeight()
.background(Color(0xFFF7F4EE)) .background(Color(0xFFF7F4EE))
) { ) {
// Header // Header
@ -95,7 +97,7 @@ fun FilterScreen(
) { ) {
IconButton(onClick = onBackClick) { IconButton(onClick = onBackClick) {
Icon( Icon(
imageVector = Icons.Default.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowForwardIos,
contentDescription = "Back", contentDescription = "Back",
tint = Color(0xFF0A0A0A) tint = Color(0xFF0A0A0A)
) )

View File

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