Reputation: 51
I am sending phone number and userId from RegisterScreen to OtpVerificationScreen. But I can't popBackStack from OtpVerificationScreen to RegisterScreen. Please help me fix this I want user to popBackStack from OtpVerificationScreen to RegisterScreen with the previous phone number
Below is my code
class AuthViewModel(
private val authUseCase: AuthUseCase,
private val sharedPreferencesManager: SharedPreferencesManager,
) : ViewModel() {
private val _userLoginState = MutableStateFlow(UserLoginState())
val userLoginState: StateFlow<UserLoginState> = _userLoginState.asStateFlow()
private val _isLoggedIn = MutableStateFlow(sharedPreferencesManager.isLoggedIn())
val isLoggedIn: StateFlow<Boolean> = _isLoggedIn.asStateFlow()
init {
startResendOtpTimer()
}
fun onPhoneChange(phone: String) {
val phoneError = when {
phone.isEmpty() -> "Phone number is required"
phone.length != 10 -> "Invalid phone number"
else -> ""
}
_userLoginState.update {
it.copy(
phone = phone,
phoneError = phoneError
)
}
}
fun onNameChange(name: String) {
val nameError = when {
name.isEmpty() -> "Name is required"
name.length !in 3..50 -> "Invalid name"
else -> ""
}
_userLoginState.update {
it.copy(
name = name,
nameError = nameError
)
}
}
fun onSecretCodeChange(secretCode: String) {
val secretCodeError = when {
secretCode.isEmpty() -> "OTP is required"
secretCode.length != 6 -> "Invalid OTP"
else -> ""
}
_userLoginState.update {
it.copy(
secretCode = secretCode,
secretCodeError = secretCodeError
)
}
}
fun signUpUsingPhone(phone: String = _userLoginState.value.phone) {
val phoneError = _userLoginState.value.phoneError
if (phoneError.isEmpty()) {
viewModelScope.launch {
authUseCase.signUpUsingPhone(phone).collectLatest { tokenState ->
_userLoginState.update {
it.copy(
userId = (tokenState as? Resource.Success)?.data?.userId ?: "",
tokenState = tokenState
)
}
}
}
}
}
fun verifyPhone(userId: String = _userLoginState.value.userId) {
val secretCode = _userLoginState.value.secretCode
val secretCodeError = _userLoginState.value.secretCodeError
if (secretCodeError.isEmpty()) {
viewModelScope.launch {
authUseCase.verifyPhone(userId, secretCode).collect { verificationState ->
_userLoginState.update {
it.copy(
userId = userId,
sessionState = verificationState
)
}
if (verificationState is Resource.Success) {
sharedPreferencesManager.setLoggedIn(true)
_isLoggedIn.value = true
}
}
}
}
}
fun logout() {
viewModelScope.launch {
authUseCase.logout()
sharedPreferencesManager.setLoggedIn(false)
_isLoggedIn.value = false
}
}
private fun startResendOtpTimer() {
viewModelScope.launch {
_userLoginState.update { it.copy(timer = 60, isTimerRunning = true) }
while (_userLoginState.value.timer > 0) {
delay(1000)
_userLoginState.update { it.copy(timer = it.timer - 1) }
}
_userLoginState.update { it.copy(isTimerRunning = false) }
}
}
fun resendOtp(phone: String) {
if (!_userLoginState.value.isTimerRunning) {
signUpUsingPhone(phone)
startResendOtpTimer()
}
}
}
@Composable
fun RegisterScreen(
modifier: Modifier = Modifier,
authViewModel: AuthViewModel = koinViewModel(),
navigateToGetStartedScreen: () -> Unit,
navigateToOtpVerificationScreen: (User) -> Unit
) {
val userLoginState by authViewModel.userLoginState.collectAsStateWithLifecycle()
val snackbarHostState = remember {
SnackbarHostState()
}
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { innerPadding ->
Box(
modifier = modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
RegisterScreenTopAppBar(
onClick = navigateToGetStartedScreen,
navigationIcon = Icons.Filled.ArrowBack
)
Text(
text = "Phone Number",
style = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
fontFamily = poppins
),
)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "Please enter your phone number to verify your account",
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = poppins,
textAlign = TextAlign.Center,
color = LightGray
),
modifier = Modifier.width(350.dp)
)
Spacer(modifier = Modifier.height(50.dp))
Box(
modifier = Modifier.padding(horizontal = 20.dp)
) {
OutlinedTextField(
value = userLoginState.phone,
onValueChange = {
if (userLoginState.phone.length < 10) {
authViewModel.onPhoneChange(it)
}
},
modifier = Modifier
.fillMaxWidth(),
leadingIcon = {
Row(
modifier = Modifier
.wrapContentWidth()
.padding(start = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.india_icon),
contentDescription = "india icon",
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "+91",
fontSize = 16.sp,
color = Color.Black
)
Spacer(modifier = Modifier.width(5.dp))
}
},
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Phone
),
placeholder = {
Text(
text = "9999999999",
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = poppins,
color = Grey20
)
)
},
isError = false,
textStyle = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = poppins,
color = Color.Black
),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Black.copy(alpha = 0.7f),
unfocusedBorderColor = LightGray
)
)
}
Spacer(modifier = Modifier.height(30.dp))
ButtonComponent(
onClick = authViewModel::signUpUsingPhone,
text = "Continue",
icon = Icons.Default.ArrowForward,
modifier = Modifier
.height(54.dp)
.padding(horizontal = 20.dp),
enabled = userLoginState.phone.length == 10
)
}
if (userLoginState.tokenState is Resource.Loading){
Box(
modifier = Modifier
.fillMaxSize()
.background(Grey.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = Color.Blue
)
}
}
}
}
ObserveAsEvents(resource = userLoginState.tokenState) { tokenState ->
if (tokenState is Resource.Success){
scope.launch {
snackbarHostState.showSnackbar(message = "OTP sent successfully")
}
navigateToOtpVerificationScreen(
User(
phone = userLoginState.phone,
id = tokenState.data?.userId ?: ""
)
)
}else if(tokenState is Resource.Error){
scope.launch {
snackbarHostState.showSnackbar(message = "OTP failed: ${tokenState.message}")
}
}
}
}
@Composable
fun OtpVerificationScreen(
modifier: Modifier = Modifier,
navigateToRegisterScreen: () -> Unit,
phone: String,
userId: String,
authViewModel: AuthViewModel = koinViewModel(),
navigateToUserDetailsScreen: (User) -> Unit
) {
val userLoginState by authViewModel.userLoginState.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
val snackbarHostState = remember {
SnackbarHostState()
}
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
}
) { innerPadding ->
Box(
modifier = modifier
.fillMaxSize()
.padding(innerPadding)
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
RegisterScreenTopAppBar(
onClick = navigateToRegisterScreen,
navigationIcon = Icons.Filled.ArrowBack
)
Text(
text = "Verify Your Phone",
style = TextStyle(
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
fontFamily = poppins
)
)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "Please enter the 6 digit code sent to",
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = poppins,
textAlign = TextAlign.Center,
color = LightGray
),
modifier = Modifier.width(350.dp)
)
Text(
text = "+91 $phone",
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = poppins,
textAlign = TextAlign.Center,
color = PrimaryBlue
),
modifier = Modifier.width(350.dp)
)
Spacer(modifier = Modifier.height(40.dp))
OtpTextField(
otpText = userLoginState.secretCode,
onOtpTextChange = { value, _ ->
authViewModel.onSecretCodeChange(value)
}
)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = String.format(
locale = Locale.getDefault(),
format = "%02d:%02d",
userLoginState.timer / 60,
userLoginState.timer % 60,
),
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = poppins,
textAlign = TextAlign.Center,
color = LightGray
)
)
Spacer(modifier = Modifier.height(5.dp))
val annotatedText = buildAnnotatedString {
withStyle(
style = SpanStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = poppins,
color = LightGray
)
) {
append("Didn't receive OTP? ")
}
pushStringAnnotation(
tag = "RESEND",
annotation = "Resend"
)
withStyle(
style = SpanStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
fontFamily = poppins,
color = PrimaryBlue,
textDecoration = TextDecoration.Underline
)
) {
append("Resend")
}
pop()
}
Text(
text = annotatedText,
style = TextStyle(
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
fontFamily = poppins
),
modifier = Modifier
.padding(horizontal = 16.dp)
.clickable {
annotatedText.getStringAnnotations(
tag = "RESEND", start = 0, end = annotatedText.length
).firstOrNull()?.let { stringAnnotation ->
if (userLoginState.timer == 0L && stringAnnotation.item == "Resend") {
authViewModel.resendOtp(phone)
}
}
},
)
Spacer(modifier = Modifier.height(30.dp))
ButtonComponent(
onClick = {
authViewModel.verifyPhone(userId)
},
text = "Verify",
icon = Icons.Default.Check,
modifier = Modifier
.height(54.dp)
.padding(horizontal = 20.dp),
enabled = userLoginState.secretCode.isNotEmpty() && userLoginState.secretCodeError.isEmpty()
)
}
}
if (userLoginState.sessionState is Resource.Loading) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Grey.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = Color.Blue
)
}
}
}
ObserveAsEvents(resource = userLoginState.sessionState) { sessionState ->
if (sessionState is Resource.Success) {
scope.launch {
snackbarHostState.showSnackbar(message = "OTP verified successfully")
}
navigateToUserDetailsScreen(
User(
id = userId,
phone = phone
)
)
} else if (sessionState is Resource.Error) {
scope.launch {
snackbarHostState.showSnackbar(message = "OTP verification failed")
}
}
}
}
@Composable
fun AppNavHost(
authViewModel: AuthViewModel = koinViewModel(),
) {
val navController = rememberNavController()
val isLoggedIn by authViewModel.isLoggedIn.collectAsState()
NavHost(
navController = navController,
startDestination = if (isLoggedIn) Screens.HomeScreen else Screens.GetStartedScreen,
enterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(700)
)
},
exitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(700)
)
},
popEnterTransition = {
slideInHorizontally(
initialOffsetX = { fullWidth -> -fullWidth },
animationSpec = tween(700)
)
},
popExitTransition = {
slideOutHorizontally(
targetOffsetX = { fullWidth -> fullWidth },
animationSpec = tween(700)
)
}
) {
composable<Screens.GetStartedScreen> {
GetStartedScreen(
navigateToRegisterScreen = {
navController.navigate(Screens.RegisterScreen)
}
)
}
composable<Screens.RegisterScreen> {
RegisterScreen(
navigateToGetStartedScreen = {
navController.popBackStack()
},
navigateToOtpVerificationScreen = { user ->
navController.navigate(
Screens.OtpVerificationScreen(
phone = user.phone,
userId = user.id
)
)
}
)
}
composable<Screens.OtpVerificationScreen> {
val args = it.toRoute<Screens.OtpVerificationScreen>()
OtpVerificationScreen(
navigateToRegisterScreen = {
navController.popBackStack()
},
phone = args.phone,
navigateToUserDetailsScreen = { user ->
navController.navigate(
Screens.UserDetailsScreen(
name = user.phone,
)
)
},
userId = args.userId
)
BackHandler {
navController.popBackStack()
}
}
composable<Screens.UserDetailsScreen> {
val args = it.toRoute<Screens.UserDetailsScreen>()
UserDetailsScreen(
name = args.name,
navigateToHomeScreen = {
navController.navigate(Screens.HomeScreen) {
popUpTo(Screens.UserDetailsScreen) { inclusive = true }
}
}
)
}
composable<Screens.HomeScreen> {
HomeScreen()
}
}
}
I want user to popBackStack from OtpVerificationScreen to RegisterScreen with the previous phone number
Upvotes: 2
Views: 49
Reputation: 2737
First thing : You dont need to write this :
BackHandler {
navController.popBackStack()
}
because android handles it by default. If you are pushing something then on backpress it will be popped automatically.
For actual issue :
When pushing the user screen , add the data in to currentBackStackEntry :
navigateToUserDetailsScreen = { user ->
{
// put your data here
navController.currentBackStackEntry?.savedStateHandle?.set("userId", "<your user id>")
navController.navigate(
Screens.UserDetailsScreen(
name = user.phone,
))
}
},
Then inside the screen, use previousBackStackEntry (where we came from) will have that data :
// Inside UserDetailsScreen() code :
val userId:String? = null
if(navController.previousBackStackEntry?.savedStateHandle?.contains("userId")) {
userId = navController.previousBackStackEntry?.savedStateHandle?.get("userId")
}
if(userId==null) {
// Passed from previous screen
}else {
// Did not pass from previous screen
}
Upvotes: 1