otp page fix.
This commit is contained in:
parent
6b634f40b7
commit
6628daed50
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue