Gursimar Singh
Gursimar Singh

Reputation: 51

Issue in handling navigation in compose

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

Answers (1)

NehaK
NehaK

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

Related Questions