From ae21d73ae8b05c928b00c703335dd4ca38d4118b Mon Sep 17 00:00:00 2001 From: Nirvan Jain Date: Sat, 4 Jul 2026 16:33:13 +0530 Subject: [PATCH 1/6] fixes --- .../main/java/com/tpc/nudj/repository/auth/AuthRepository.kt | 2 +- .../java/com/tpc/nudj/repository/auth/FirebaseRepository.kt | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt b/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt index a651645..3cf99aa 100644 --- a/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt +++ b/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt @@ -34,7 +34,7 @@ interface AuthRepository { * @param idToken Google ID token * @return Flow of AuthResult representing the operation result */ - suspend fun signInWithGoogle(idToken: String): Flow + suspend fun signInWithGoogle(idToken: String, role: Role): Flow /** * Creates a new user account with email and password. diff --git a/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt b/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt index 0150cf1..4a1a5c9 100644 --- a/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt +++ b/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt @@ -25,7 +25,7 @@ class FirebaseAuthRepository( val authStateListener = FirebaseAuth.AuthStateListener { auth -> val firebaseUser = auth.currentUser if (firebaseUser != null) { - CoroutineScope(Dispatchers.IO).launch { + launch(Dispatchers.IO) { val role = userRepository.fetchUserRole(firebaseUser.uid) trySend( User( @@ -86,7 +86,7 @@ class FirebaseAuthRepository( } } - override suspend fun signInWithGoogle(idToken: String): Flow = flow { + override suspend fun signInWithGoogle(idToken: String, role: Role): Flow = flow { try { emit(AuthResult.Loading) val credential = GoogleAuthProvider.getCredential(idToken, null) @@ -100,7 +100,6 @@ class FirebaseAuthRepository( emit(AuthResult.Error("Use IIITDMJ email addresses only.")) return@flow } - val role = userRepository.fetchUserRole(firebaseUser.uid) emit( AuthResult.Success( User( From d0bce8faa78fc4c84225bc52402ed55a7b2338be Mon Sep 17 00:00:00 2001 From: nehasiwach802 Date: Thu, 9 Jul 2026 15:08:55 +0530 Subject: [PATCH 2/6] changed AndroidManifest.xml --- app/src/main/AndroidManifest.xml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc7d60d..cb703d1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,10 +19,22 @@ android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode" android:theme="@style/Theme.Nudj"> - + + + + + + + + + + From f8956f52feb0fd52ea820189f7c2c8fb4e9b65ea Mon Sep 17 00:00:00 2001 From: nehasiwach802 Date: Thu, 9 Jul 2026 15:39:43 +0530 Subject: [PATCH 3/6] integrated reset password screen --- app/src/main/AndroidManifest.xml | 1 - .../main/java/com/tpc/nudj/MainActivity.kt | 50 ++++++- .../nudj/repository/auth/AuthRepository.kt | 2 + .../repository/auth/FirebaseRepository.kt | 57 ++++++-- .../com/tpc/nudj/ui/navigation/ScreenRoute.kt | 17 ++- .../EmailVerificationScreen.kt | 127 ++++++++++-------- .../forgotPassword/ForgetPasswordScreen.kt | 27 +++- .../forgotPassword/ForgotPasswordEvents.kt | 6 + .../nudj/ui/screen/auth/login/LoginScreen.kt | 5 +- .../screen/auth/reset/ResetPasswordEvents.kt | 6 + .../screen/auth/reset/ResetPasswordScreen.kt | 29 +++- .../ResetPassword/ResetPasswordViewModel.kt | 60 ++++++++- .../EmailVerificationViewModel.kt | 19 ++- .../forgotPassword/ForgotPasswordViewModel.kt | 67 ++++++++- 14 files changed, 370 insertions(+), 103 deletions(-) create mode 100644 app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgotPasswordEvents.kt create mode 100644 app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordEvents.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cb703d1..b8445ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,5 +37,4 @@ - \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/MainActivity.kt b/app/src/main/java/com/tpc/nudj/MainActivity.kt index 46c6b72..488a4b6 100644 --- a/app/src/main/java/com/tpc/nudj/MainActivity.kt +++ b/app/src/main/java/com/tpc/nudj/MainActivity.kt @@ -1,5 +1,6 @@ package com.tpc.nudj +import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -8,6 +9,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -16,6 +19,7 @@ import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import com.tpc.nudj.ui.navigation.ScreenRoute +import com.tpc.nudj.ui.navigation.VerificationPurpose import com.tpc.nudj.ui.screen.DemoScreen import com.tpc.nudj.ui.screen.auth.clubVerification.ClubVerificationScreen import com.tpc.nudj.ui.screen.auth.emailVerification.EmailVerificationScreen @@ -34,8 +38,13 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { + private var deepLinkuri by mutableStateOf(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + deepLinkuri = intent?.data enableEdgeToEdge() setContent { NudjTheme { @@ -45,6 +54,9 @@ class MainActivity : ComponentActivity() { val backStack = rememberNavBackStack(ScreenRoute.Auth.SplashScreen) LaunchedEffect(authState) { + if (deepLinkuri?.getQueryParameter("mode") == "resetPassword") { + return@LaunchedEffect + } when (val state = authState) { is AppViewModel.AuthState.Initial -> {} is AppViewModel.AuthState.Unauthenticated -> { @@ -53,7 +65,7 @@ class MainActivity : ComponentActivity() { } is AppViewModel.AuthState.EmailNotVerified -> { backStack.clear() - backStack.add(ScreenRoute.Auth.EmailVerification) + backStack.add(ScreenRoute.Auth.EmailVerification()) } is AppViewModel.AuthState.Authenticated -> { backStack.clear() @@ -70,6 +82,16 @@ class MainActivity : ComponentActivity() { } } } + LaunchedEffect(deepLinkuri) { + val uri = deepLinkuri ?: return@LaunchedEffect + val mode = uri.getQueryParameter("mode") + val oobCode = uri.getQueryParameter("oobCode") + if (mode == "resetPassword" && !oobCode.isNullOrBlank()) { + backStack.clear() + backStack.add(ScreenRoute.Auth.ResetPassword(oobCode)) + } + deepLinkuri = null + } NavDisplay( backStack = backStack, modifier = Modifier.fillMaxSize(), @@ -92,6 +114,9 @@ class MainActivity : ComponentActivity() { LoginScreen( navigateToCreateAccount ={ backStack.add(ScreenRoute.Auth.Register) + }, + navigateToForgotPassword = { + backStack.add(ScreenRoute.Auth.ForgotPassword) } ) } @@ -102,7 +127,7 @@ class MainActivity : ComponentActivity() { }, onNavigateToEmailVerification = { backStack.clear() - backStack.add(ScreenRoute.Auth.EmailVerification) + backStack.add(ScreenRoute.Auth.EmailVerification(purpose = VerificationPurpose.REGISTRATION)) }, onNavigateToUserDetailsInput = { backStack.clear() @@ -115,8 +140,10 @@ class MainActivity : ComponentActivity() { ) } - entry { + entry { route -> EmailVerificationScreen( + email = route.email, + purpose = route.purpose, onNavigateBack = {}, onNavigateToEmailVerified = { backStack.clear() @@ -126,12 +153,21 @@ class MainActivity : ComponentActivity() { } entry { - ForgetPasswordScreen( + ForgetPasswordScreen(onNavigateToEmailVerification = { email -> + backStack.add( + ScreenRoute.Auth.EmailVerification( + email = email, + purpose = VerificationPurpose.PASSWORD_RESET + ) + ) + }, + onLoginClick = {} ) } - entry { + entry {route -> ResetPasswordScreen( + oobCode = route.oobCode, onLoginClick = { backStack.add(ScreenRoute.Auth.Login) } @@ -153,10 +189,10 @@ class MainActivity : ComponentActivity() { DemoScreen(text = "Student Dashboard") } entry{ - ClubLandingScreen() + DemoScreen(text = "Club Dashboard") } entry{ - ClubVerificationScreen(onNavigationBack = {}) + DemoScreen(text = "Club Verification Dashboard") } entry { DemoScreen(text = "User Details Input") diff --git a/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt b/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt index 3181a0d..7a3ee03 100644 --- a/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt +++ b/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt @@ -68,4 +68,6 @@ interface AuthRepository { */ suspend fun signOut() suspend fun reloadAndCheckEmailVerified(): Boolean + suspend fun resetPassword(code: String , newPassword:String ): Flow + suspend fun verifyPasswordResetCode(code: String): Result } \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt b/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt index 0150cf1..5cb56f5 100644 --- a/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt +++ b/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt @@ -10,6 +10,7 @@ import com.tpc.nudj.repository.user.UserRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flow @@ -25,18 +26,22 @@ class FirebaseAuthRepository( val authStateListener = FirebaseAuth.AuthStateListener { auth -> val firebaseUser = auth.currentUser if (firebaseUser != null) { - CoroutineScope(Dispatchers.IO).launch { - val role = userRepository.fetchUserRole(firebaseUser.uid) - trySend( - User( - uid = firebaseUser.uid, - email = firebaseUser.email ?: "", - displayName = firebaseUser.displayName ?: "", - isEmailVerified = firebaseUser.isEmailVerified, - photoUrl = firebaseUser.photoUrl?.toString() ?: "", - role = role + launch(Dispatchers.IO) { + try { + val role = userRepository.fetchUserRole(firebaseUser.uid) + trySend( + User( + uid = firebaseUser.uid, + email = firebaseUser.email ?: "", + displayName = firebaseUser.displayName ?: "", + isEmailVerified = firebaseUser.isEmailVerified, + photoUrl = firebaseUser.photoUrl?.toString() ?: "", + role = role + ) ) - ) + } catch (e: Exception){ + trySendBlocking(null) + } } } else { trySend(null) @@ -86,7 +91,7 @@ class FirebaseAuthRepository( } } - override suspend fun signInWithGoogle(idToken: String): Flow = flow { + override suspend fun signInWithGoogle(idToken: String, role: Role): Flow = flow { try { emit(AuthResult.Loading) val credential = GoogleAuthProvider.getCredential(idToken, null) @@ -95,12 +100,10 @@ class FirebaseAuthRepository( if (firebaseUser != null) { if(firebaseUser.email?.contains("iiitdmj.ac.in") == false) { - firebaseAuth.currentUser?.delete()?.await() firebaseAuth.signOut() emit(AuthResult.Error("Use IIITDMJ email addresses only.")) return@flow } - val role = userRepository.fetchUserRole(firebaseUser.uid) emit( AuthResult.Success( User( @@ -181,4 +184,30 @@ class FirebaseAuthRepository( override suspend fun signOut() { firebaseAuth.signOut() } + + override suspend fun reloadAndCheckEmailVerified(): Boolean { + val firebaseUser = firebaseAuth.currentUser?: throw Exception("No user is signed in") + firebaseUser.reload().await() + return firebaseAuth.currentUser?.isEmailVerified == true + } + override suspend fun verifyPasswordResetCode( + code: String + ): Result = runCatching { + firebaseAuth.verifyPasswordResetCode(code).await() + } + + override suspend fun resetPassword(code: String, newPassword: String): Flow + = flow { + try { + emit(AuthResult.Loading) + firebaseAuth.confirmPasswordReset(code, newPassword).await() + emit(AuthResult.Success(User())) + } catch (e: Exception) { + emit( + AuthResult.Error( + e.message ?: "Unable to reset password" + ) + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt b/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt index 1e4b22d..f0ef0f4 100644 --- a/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt +++ b/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt @@ -3,6 +3,12 @@ package com.tpc.nudj.ui.navigation import androidx.navigation3.runtime.NavKey import kotlinx.serialization.Serializable +@Serializable +enum class VerificationPurpose { + REGISTRATION, + PASSWORD_RESET +} + @Serializable sealed interface ScreenRoute : NavKey { @@ -21,7 +27,10 @@ sealed interface ScreenRoute : NavKey { data object Register : Auth @Serializable - data object EmailVerification : Auth + data class EmailVerification( + val email: String = "", + val purpose: VerificationPurpose = VerificationPurpose.REGISTRATION + ) : Auth @Serializable data object EmailVerified : Auth @@ -30,7 +39,9 @@ sealed interface ScreenRoute : NavKey { data object ForgotPassword : Auth @Serializable - data object ResetPassword : Auth + data class ResetPassword( + val oobCode : String + ) : Auth } @Serializable @@ -47,4 +58,4 @@ sealed interface ScreenRoute : NavKey { @Serializable data object UserDetailsInput : App } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt index 4631e8d..043b5ca 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt @@ -29,6 +29,7 @@ import com.tpc.nudj.ui.components.LoadingIndicator import com.tpc.nudj.ui.components.NudjTopAppBar import com.tpc.nudj.ui.components.PrimaryButton import com.tpc.nudj.ui.components.TertiaryButton +import com.tpc.nudj.ui.navigation.VerificationPurpose import com.tpc.nudj.ui.theme.LocalAppColors import com.tpc.nudj.ui.theme.NudjTheme import com.tpc.nudj.viewmodels.auth.emailVerification.EmailVerificationViewModel @@ -37,13 +38,19 @@ import kotlinx.coroutines.launch @Composable fun EmailVerificationScreen( viewModel: EmailVerificationViewModel = hiltViewModel(), + email: String, + purpose: VerificationPurpose, onNavigateBack: () -> Unit, onNavigateToEmailVerified: () -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackBarHostState = remember { SnackbarHostState() } - LaunchedEffect(Unit) { - viewModel.onScreenOpened() + LaunchedEffect(email, purpose) { + viewModel.onScreenOpened( + email = email, + purpose = purpose + + ) } LaunchedEffect(Unit) { viewModel.events.collect { event -> @@ -70,7 +77,11 @@ fun EmailVerificationScreen( EmailVerificationScreenLayout( uiState = uiState, onBackClick = onNavigateBack, - onResendEmailClick = { viewModel.onResendEmailClick() }, + onResendEmailClick = { viewModel.onResendEmailClick( + email = email, + purpose = purpose + + ) }, modifier = Modifier.padding(paddingValues), onShowSnackBar = { message-> snackBarHostState.showSnackbar(message) } ) @@ -90,76 +101,76 @@ fun EmailVerificationScreenLayout( val context = LocalContext.current val scope = rememberCoroutineScope() - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - - Image( - painter = painterResource(id = R.drawable.meditating_mascot), - contentDescription = "Email Sent Illustration", - modifier = Modifier.size(280.dp) - ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + + Image( + painter = painterResource(id = R.drawable.meditating_mascot), + contentDescription = "Email Sent Illustration", + modifier = Modifier.size(280.dp) + ) - Spacer(modifier = Modifier.height(40.dp)) + Spacer(modifier = Modifier.height(40.dp)) - Text( - text = uiState.formattedTime, - style = MaterialTheme.typography.displayMedium, - color = LocalAppColors.current.primaryButtonColor - ) + Text( + text = uiState.formattedTime, + style = MaterialTheme.typography.displayMedium, + color = LocalAppColors.current.primaryButtonColor + ) - Spacer(modifier = Modifier.height(56.dp)) + Spacer(modifier = Modifier.height(56.dp)) - PrimaryButton( - text = "Check Inbox", - onClick = { - val inboxIntent = Intent(Intent.ACTION_MAIN).apply { - addCategory(Intent.CATEGORY_APP_EMAIL) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } + PrimaryButton( + text = "Check Inbox", + onClick = { + val inboxIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_EMAIL) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } - val chooser = Intent.createChooser(inboxIntent, "Open Inbox via Gmail") + val chooser = Intent.createChooser(inboxIntent, "Open Inbox via Gmail") - try { - context.startActivity(chooser) - } catch (e: Exception){ - scope.launch { - onShowSnackBar("No email app found") - } + try { + context.startActivity(chooser) + } catch (e: Exception){ + scope.launch { + onShowSnackBar("No email app found") } - }, - modifier = Modifier - .fillMaxWidth() - ) - + } + }, + modifier = Modifier + .fillMaxWidth() + ) - Spacer(modifier = Modifier.height(24.dp) ) - TertiaryButton( - text = "Resend Email", - onClick = onResendEmailClick, - enabled = uiState.isResendEnabled - ) + Spacer(modifier = Modifier.height(24.dp) ) - Spacer(modifier = Modifier.height(16.dp)) + TertiaryButton( + text = "Resend Email", + onClick = onResendEmailClick, + enabled = uiState.isResendEnabled + ) - if(!uiState.isResendEnabled){ - Text( - text = "Resend in ${uiState.timerInSeconds}s", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.padding(vertical = 8.dp) - ) - } + Spacer(modifier = Modifier.height(16.dp)) + if(!uiState.isResendEnabled){ + Text( + text = "Resend in ${uiState.timerInSeconds}s", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(vertical = 8.dp) + ) } } +} + @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -174,4 +185,4 @@ fun PreviewEmailVerificationScreen() { onShowSnackBar = {} ) } -} \ No newline at end of file + } diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgetPasswordScreen.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgetPasswordScreen.kt index 461061e..3e53a43 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgetPasswordScreen.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgetPasswordScreen.kt @@ -15,10 +15,14 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -36,14 +40,32 @@ import com.tpc.nudj.viewmodels.auth.forgotPassword.ForgotPasswordViewModel @Composable fun ForgetPasswordScreen( viewModel: ForgotPasswordViewModel = hiltViewModel(), + onNavigateToEmailVerification: (String) -> Unit, onLoginClick: () -> Unit ) { + val uiState by viewModel.forgotPasswordUiState.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + viewModel.events.collect { it -> + when (it) { + is ForgotPasswordEvents.ShowSnackBar -> { + snackbarHostState.showSnackbar(it.message) + } + + is ForgotPasswordEvents.NavigateToEmailVerification -> { + onNavigateToEmailVerification(it.email) + } + } + } + } Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, containerColor = LocalAppColors.current.background - ) { paddingValues -> - val uiState by viewModel.forgotPasswordUiState.collectAsState() + LoadingIndicator(isLoading = uiState.isLoading) { ForgetPasswordScreenLayout( @@ -58,6 +80,7 @@ fun ForgetPasswordScreen( } } + @Composable fun ForgetPasswordScreenLayout( modifier: Modifier = Modifier, diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgotPasswordEvents.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgotPasswordEvents.kt new file mode 100644 index 0000000..94a1218 --- /dev/null +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/forgotPassword/ForgotPasswordEvents.kt @@ -0,0 +1,6 @@ +package com.tpc.nudj.ui.screen.auth.forgotPassword + +sealed interface ForgotPasswordEvents{ + data class ShowSnackBar(val message: String) : ForgotPasswordEvents + data class NavigateToEmailVerification(val email: String): ForgotPasswordEvents +} \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/login/LoginScreen.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/login/LoginScreen.kt index 8bc8161..8483a3f 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/login/LoginScreen.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/login/LoginScreen.kt @@ -67,7 +67,8 @@ import com.tpc.nudj.ui.theme.LocalAppColors @Composable fun LoginScreen( viewModel: LoginViewModel = hiltViewModel(), - navigateToCreateAccount: () ->Unit + navigateToCreateAccount: () ->Unit, + navigateToForgotPassword: () -> Unit ) { val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } @@ -91,7 +92,7 @@ fun LoginScreen( onPasswordInput = { pass -> viewModel.onPasswordChange(pass) }, - onForgotPasswordClick = viewModel::onForgotPasswordClick, + onForgotPasswordClick = navigateToForgotPassword, onLoginClick = viewModel::onLoginClick, onGoogleClick = {viewModel.onGoogleClick(context)}, onPasswordVisibilityToggle = viewModel::togglePasswordVisibility, diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordEvents.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordEvents.kt new file mode 100644 index 0000000..ce00016 --- /dev/null +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordEvents.kt @@ -0,0 +1,6 @@ +package com.tpc.nudj.ui.screen.auth.reset + +sealed interface ResetPasswordEvent { + data class ShowSnackBar(val message: String) : ResetPasswordEvent + data object NavigateToLogin : ResetPasswordEvent +} \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordScreen.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordScreen.kt index 2a3e73e..946af05 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordScreen.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/reset/ResetPasswordScreen.kt @@ -15,11 +15,15 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -37,13 +41,28 @@ import com.tpc.nudj.ui.theme.NudjTheme @Composable fun ResetPasswordScreen( viewModel: ResetPasswordViewModel = hiltViewModel(), - onLoginClick :() ->Unit + onLoginClick :() ->Unit, + oobCode : String ) { + + + val uiState by viewModel.resetPasswordUiState.collectAsState() + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(Unit) { + viewModel.events.collect { event -> + when (event) { + is ResetPasswordEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(event.message) + } + ResetPasswordEvent.NavigateToLogin -> onLoginClick() + } + } + } Scaffold( - containerColor = LocalAppColors.current.background + containerColor = LocalAppColors.current.background, + snackbarHost = { SnackbarHost(snackbarHostState) } ) { paddingValues -> - - val uiState by viewModel.resetPasswordUiState.collectAsState() LoadingIndicator(isLoading = uiState.isLoading) { ResetPasswordScreenLayout( modifier = Modifier.padding(paddingValues), @@ -52,7 +71,7 @@ fun ResetPasswordScreen( onConfirmPasswordInput = viewModel::onConfirmPasswordChange, onPasswordVisibilityToggle = viewModel::togglePasswordVisibility, onConfirmPasswordVisibilityToggle = viewModel::toggleConfirmPasswordVisibility, - onSubmitClick = viewModel::onSubmitClick, + onSubmitClick = {viewModel.onSubmitClick(oobCode)}, onLoginClick = onLoginClick ) } diff --git a/app/src/main/java/com/tpc/nudj/viewmodels/auth/ResetPassword/ResetPasswordViewModel.kt b/app/src/main/java/com/tpc/nudj/viewmodels/auth/ResetPassword/ResetPasswordViewModel.kt index d3dc66f..979952e 100644 --- a/app/src/main/java/com/tpc/nudj/viewmodels/auth/ResetPassword/ResetPasswordViewModel.kt +++ b/app/src/main/java/com/tpc/nudj/viewmodels/auth/ResetPassword/ResetPasswordViewModel.kt @@ -1,6 +1,10 @@ package com.tpc.nudj.viewmodels.auth.ResetPassword import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tpc.nudj.model.AuthResult +import com.tpc.nudj.repository.auth.AuthRepository +import com.tpc.nudj.ui.screen.auth.reset.ResetPasswordEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -8,12 +12,19 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import javax.inject.Inject import com.tpc.nudj.ui.screen.auth.reset.ResetPasswordUiState +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + @HiltViewModel -class ResetPasswordViewModel @Inject constructor() : ViewModel() { +class ResetPasswordViewModel @Inject constructor( + private val authRepository: AuthRepository +) : ViewModel() { private val _resetPasswordUiState = MutableStateFlow(ResetPasswordUiState()) val resetPasswordUiState: StateFlow = _resetPasswordUiState.asStateFlow() - + private val _events = MutableSharedFlow() + val events = _events.asSharedFlow() fun onPasswordChange(password: String) { _resetPasswordUiState.update { it.copy(password = password) @@ -40,8 +51,49 @@ class ResetPasswordViewModel @Inject constructor() : ViewModel() { } } - fun onSubmitClick() { + fun onSubmitClick(oobCode: String) { + viewModelScope.launch { + if (resetPasswordUiState.value.password.isBlank()) { + _events.emit(ResetPasswordEvent.ShowSnackBar("Please Enter password")) + return@launch + } + if (resetPasswordUiState.value.confirmPassword.isBlank()) { + _events.emit(ResetPasswordEvent.ShowSnackBar("Please Enter confirm Password")) + return@launch + } + if (resetPasswordUiState.value.password.length < 6) { + _events.emit(ResetPasswordEvent.ShowSnackBar("Password must be at least 6 characters")) + return@launch + } - } + if (resetPasswordUiState.value.password != resetPasswordUiState.value.confirmPassword) { + _events.emit(ResetPasswordEvent.ShowSnackBar("Passwords do not match")) + return@launch + } + + authRepository.resetPassword(oobCode, resetPasswordUiState.value.password) + .collect { result -> + when (result) { + AuthResult.Loading -> { + _resetPasswordUiState.update { it.copy(isLoading = true) } + } + + is AuthResult.Success -> { + _resetPasswordUiState.update { it.copy(isLoading = false) } + _events.emit(ResetPasswordEvent.ShowSnackBar("Password reset successfully")) + _events.emit(ResetPasswordEvent.NavigateToLogin) + } + is AuthResult.Error -> { + _resetPasswordUiState.update { it.copy(isLoading = false) } + _events.emit(ResetPasswordEvent.ShowSnackBar(result.message)) + } + + else -> Unit + } + } + + } + + } } \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt index fdcd30b..7fef997 100644 --- a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt +++ b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.tpc.nudj.model.AuthResult import com.tpc.nudj.repository.auth.AuthRepository +import com.tpc.nudj.ui.navigation.VerificationPurpose import com.tpc.nudj.ui.screen.auth.emailVerification.EmailVerificationEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -26,12 +27,19 @@ class EmailVerificationViewModel @Inject constructor( private val _uiState = MutableStateFlow(EmailVerificationUiState()) val uiState: StateFlow = _uiState.asStateFlow() - fun onResendEmailClick() { + fun onResendEmailClick(email: String, purpose: VerificationPurpose) { viewModelScope.launch { if (!uiState.value.isResendEnabled) { return@launch } - authRepository.sendEmailVerification().collect { result -> + val emailVerificationFlow = when (purpose) { + VerificationPurpose.REGISTRATION -> + authRepository.sendEmailVerification() + + VerificationPurpose.PASSWORD_RESET -> + authRepository.sendPasswordResetEmail(email) + } + emailVerificationFlow.collect { result -> when (result) { is AuthResult.Loading -> { _uiState.update { @@ -43,7 +51,6 @@ class EmailVerificationViewModel @Inject constructor( _uiState.update { it.copy(isLoading = false) } - _events.emit( EmailVerificationEvent.showSnackBar("Verification email sent again") ) @@ -92,12 +99,14 @@ class EmailVerificationViewModel @Inject constructor( } private var hasStartedTimer = false - fun onScreenOpened() { + fun onScreenOpened(email: String, purpose: VerificationPurpose) { if (hasStartedTimer) return hasStartedTimer = true startTimer() - startCheckingEmailVerification() + if (purpose == VerificationPurpose.REGISTRATION) { + startCheckingEmailVerification() + } } private fun startCheckingEmailVerification() { viewModelScope.launch { diff --git a/app/src/main/java/com/tpc/nudj/viewmodels/auth/forgotPassword/ForgotPasswordViewModel.kt b/app/src/main/java/com/tpc/nudj/viewmodels/auth/forgotPassword/ForgotPasswordViewModel.kt index ca1a419..cb4b052 100644 --- a/app/src/main/java/com/tpc/nudj/viewmodels/auth/forgotPassword/ForgotPasswordViewModel.kt +++ b/app/src/main/java/com/tpc/nudj/viewmodels/auth/forgotPassword/ForgotPasswordViewModel.kt @@ -1,27 +1,90 @@ package com.tpc.nudj.viewmodels.auth.forgotPassword import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tpc.nudj.model.AuthResult +import com.tpc.nudj.repository.auth.AuthRepository +import com.tpc.nudj.ui.screen.auth.forgotPassword.ForgotPasswordEvents import com.tpc.nudj.ui.screen.auth.forgotPassword.ForgotPasswordUiState +import com.tpc.nudj.utils.Validator import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch @HiltViewModel -class ForgotPasswordViewModel @Inject constructor() : ViewModel() { +class ForgotPasswordViewModel @Inject constructor( + private val authRepository: AuthRepository +) : ViewModel() { private val _forgotPasswordUiState = MutableStateFlow(ForgotPasswordUiState()) val forgotPasswordUiState: StateFlow = _forgotPasswordUiState.asStateFlow() - + private val _events = MutableSharedFlow() + val events = _events.asSharedFlow() fun onEmailChange(email: String) { _forgotPasswordUiState.update { it.copy(email = email) } } fun onSendEmailClick(){ + viewModelScope.launch { + + if (forgotPasswordUiState.value.email.isBlank()) { + _events.emit( + ForgotPasswordEvents.ShowSnackBar("Please enter email") + ) + return@launch + } + Validator.isValidEmail(forgotPasswordUiState.value.email.trim()) + .onFailure { + _events.emit( + ForgotPasswordEvents.ShowSnackBar("Invalid Email") + ) + return@launch + } + authRepository.sendPasswordResetEmail(forgotPasswordUiState.value.email.trim()).collect{result -> + when(result){ + is AuthResult.Loading -> { + _forgotPasswordUiState.update{ + it.copy(isLoading = true) + } + } + + is AuthResult.Success ->{ + _forgotPasswordUiState.update { + it.copy(isLoading = false) + } + + _events.emit( + ForgotPasswordEvents.ShowSnackBar("Password reset email sent") + ) + + _events.emit( + ForgotPasswordEvents.NavigateToEmailVerification(forgotPasswordUiState.value.email.trim()) + ) + } + is AuthResult.Error -> { + _forgotPasswordUiState.update { + it.copy(isLoading = false) + } + + _events.emit( + ForgotPasswordEvents.ShowSnackBar("Failed to send reset password mail .Please try again later.") + ) + } + + else -> Unit + + } + + } + } } From 3da3f66a09b9608df3a60c3900245ca920e0b057 Mon Sep 17 00:00:00 2001 From: nehasiwach802 Date: Fri, 10 Jul 2026 19:37:58 +0530 Subject: [PATCH 4/6] fixed emailVerification and Reset password --- app/src/main/AndroidManifest.xml | 1 + .../main/java/com/tpc/nudj/MainActivity.kt | 41 ++++++++++++++++--- .../nudj/repository/auth/AuthRepository.kt | 1 + .../repository/auth/FirebaseRepository.kt | 6 +++ .../com/tpc/nudj/ui/navigation/ScreenRoute.kt | 5 ++- .../EmailVerificationScreen.kt | 6 ++- .../com/tpc/nudj/viewmodels/AppViewModel.kt | 1 + .../EmailVerificationViewModel.kt | 24 ++++++++++- 8 files changed, 74 insertions(+), 11 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b8445ea..ca99502 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ android:exported="true" android:label="@string/app_name" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode" + android:launchMode="singleTask" android:theme="@style/Theme.Nudj"> diff --git a/app/src/main/java/com/tpc/nudj/MainActivity.kt b/app/src/main/java/com/tpc/nudj/MainActivity.kt index 488a4b6..e5e7d73 100644 --- a/app/src/main/java/com/tpc/nudj/MainActivity.kt +++ b/app/src/main/java/com/tpc/nudj/MainActivity.kt @@ -1,5 +1,6 @@ package com.tpc.nudj +import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity @@ -40,6 +41,11 @@ import dagger.hilt.android.AndroidEntryPoint class MainActivity : ComponentActivity() { private var deepLinkuri by mutableStateOf(null) + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + deepLinkuri = intent.data + } override fun onCreate(savedInstanceState: Bundle?) { @@ -52,9 +58,9 @@ class MainActivity : ComponentActivity() { val authState by appViewModel.authState.collectAsState() val backStack = rememberNavBackStack(ScreenRoute.Auth.SplashScreen) - LaunchedEffect(authState) { - if (deepLinkuri?.getQueryParameter("mode") == "resetPassword") { + val deepLinkMode = deepLinkuri?.getQueryParameter("mode") + if (deepLinkMode == "resetPassword" || deepLinkMode == "verifyEmail") { return@LaunchedEffect } when (val state = authState) { @@ -86,13 +92,35 @@ class MainActivity : ComponentActivity() { val uri = deepLinkuri ?: return@LaunchedEffect val mode = uri.getQueryParameter("mode") val oobCode = uri.getQueryParameter("oobCode") - if (mode == "resetPassword" && !oobCode.isNullOrBlank()) { - backStack.clear() - backStack.add(ScreenRoute.Auth.ResetPassword(oobCode)) + when (mode) { + "resetPassword" -> { + if (!oobCode.isNullOrBlank()) { + backStack.clear() + backStack.add(ScreenRoute.Auth.ResetPassword(oobCode)) + } + } + + "verifyEmail" -> { + if (!oobCode.isNullOrBlank()) { + backStack.clear() + backStack.add( + ScreenRoute.Auth.EmailVerification( + purpose = VerificationPurpose.REGISTRATION, + oobCode = oobCode + ) + ) + } else { + backStack.clear() + backStack.add(ScreenRoute.Auth.Login) + } + } } deepLinkuri = null } - NavDisplay( + + + + NavDisplay( backStack = backStack, modifier = Modifier.fillMaxSize(), entryDecorators = listOf( @@ -144,6 +172,7 @@ class MainActivity : ComponentActivity() { EmailVerificationScreen( email = route.email, purpose = route.purpose, + oobCode = route.oobCode, onNavigateBack = {}, onNavigateToEmailVerified = { backStack.clear() diff --git a/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt b/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt index 7a3ee03..1a9aa49 100644 --- a/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt +++ b/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt @@ -68,6 +68,7 @@ interface AuthRepository { */ suspend fun signOut() suspend fun reloadAndCheckEmailVerified(): Boolean + suspend fun applyEmailVerificationCode(code: String): Result suspend fun resetPassword(code: String , newPassword:String ): Flow suspend fun verifyPasswordResetCode(code: String): Result } \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt b/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt index 5cb56f5..1896b24 100644 --- a/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt +++ b/app/src/main/java/com/tpc/nudj/repository/auth/FirebaseRepository.kt @@ -210,4 +210,10 @@ class FirebaseAuthRepository( ) } } + override suspend fun applyEmailVerificationCode( + code: String + ): Result = runCatching { + firebaseAuth.applyActionCode(code).await() + firebaseAuth.currentUser?.reload()?.await() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt b/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt index f0ef0f4..ee324b6 100644 --- a/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt +++ b/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt @@ -29,7 +29,8 @@ sealed interface ScreenRoute : NavKey { @Serializable data class EmailVerification( val email: String = "", - val purpose: VerificationPurpose = VerificationPurpose.REGISTRATION + val purpose: VerificationPurpose = VerificationPurpose.REGISTRATION, + val oobCode: String? = null ) : Auth @Serializable @@ -58,4 +59,4 @@ sealed interface ScreenRoute : NavKey { @Serializable data object UserDetailsInput : App } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt index 043b5ca..e1232b1 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt @@ -40,15 +40,17 @@ fun EmailVerificationScreen( viewModel: EmailVerificationViewModel = hiltViewModel(), email: String, purpose: VerificationPurpose, + oobCode: String? = null, onNavigateBack: () -> Unit, onNavigateToEmailVerified: () -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackBarHostState = remember { SnackbarHostState() } - LaunchedEffect(email, purpose) { + LaunchedEffect(email, purpose, oobCode) { viewModel.onScreenOpened( email = email, - purpose = purpose + purpose = purpose, + oobCode = oobCode ) } diff --git a/app/src/main/java/com/tpc/nudj/viewmodels/AppViewModel.kt b/app/src/main/java/com/tpc/nudj/viewmodels/AppViewModel.kt index 5a7e12e..d6c36dc 100644 --- a/app/src/main/java/com/tpc/nudj/viewmodels/AppViewModel.kt +++ b/app/src/main/java/com/tpc/nudj/viewmodels/AppViewModel.kt @@ -96,6 +96,7 @@ class AppViewModel @Inject constructor( } } + private fun observeAuthState() { viewModelScope.launch { authRepository.getCurrentUser().collect { user -> diff --git a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt index 7fef997..aaa6ee8 100644 --- a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt +++ b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt @@ -99,15 +99,37 @@ class EmailVerificationViewModel @Inject constructor( } private var hasStartedTimer = false - fun onScreenOpened(email: String, purpose: VerificationPurpose) { + fun onScreenOpened(email: String, purpose: VerificationPurpose, oobCode: String? = null) { if (hasStartedTimer) return hasStartedTimer = true startTimer() + if (!oobCode.isNullOrBlank()) { + applyEmailVerificationCode(oobCode) + return + } if (purpose == VerificationPurpose.REGISTRATION) { startCheckingEmailVerification() } } + private fun applyEmailVerificationCode(oobCode: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + authRepository.applyEmailVerificationCode(oobCode) + .onSuccess { + _uiState.update { it.copy(isLoading = false) } + _events.emit(EmailVerificationEvent.NavigateToEmailVerified) + } + .onFailure { + _uiState.update { it.copy(isLoading = false) } + _events.emit( + EmailVerificationEvent.showSnackBar( + it.message ?: "Unable to verify email" + ) + ) + } + } + } private fun startCheckingEmailVerification() { viewModelScope.launch { while (true) { From a8ddc938943d83fe8854792fc344ba161a6591eb Mon Sep 17 00:00:00 2001 From: nehasiwach802 Date: Sat, 11 Jul 2026 14:38:01 +0530 Subject: [PATCH 5/6] fixed the edge cases --- .../main/java/com/tpc/nudj/MainActivity.kt | 46 ++++++++++++---- .../EmailVerificationEvent.kt | 1 + .../EmailVerificationScreen.kt | 24 +++++---- .../EmailVerificationViewModel.kt | 52 ++++++++++++++----- 4 files changed, 92 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/tpc/nudj/MainActivity.kt b/app/src/main/java/com/tpc/nudj/MainActivity.kt index e5e7d73..8c0d64f 100644 --- a/app/src/main/java/com/tpc/nudj/MainActivity.kt +++ b/app/src/main/java/com/tpc/nudj/MainActivity.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -56,6 +57,9 @@ class MainActivity : ComponentActivity() { NudjTheme { val appViewModel: AppViewModel = hiltViewModel() val authState by appViewModel.authState.collectAsState() + var comingEmailVerificationCode by rememberSaveable { + mutableStateOf(null) + } val backStack = rememberNavBackStack(ScreenRoute.Auth.SplashScreen) LaunchedEffect(authState) { @@ -95,20 +99,39 @@ class MainActivity : ComponentActivity() { when (mode) { "resetPassword" -> { if (!oobCode.isNullOrBlank()) { - backStack.clear() - backStack.add(ScreenRoute.Auth.ResetPassword(oobCode)) + val currentRoute = backStack.lastOrNull() + if ( + currentRoute is ScreenRoute.Auth.EmailVerification && currentRoute.purpose == VerificationPurpose.PASSWORD_RESET + ) { + comingEmailVerificationCode = oobCode + } else { + backStack.clear() + backStack.add( + ScreenRoute.Auth.EmailVerification( + purpose = VerificationPurpose.PASSWORD_RESET, + oobCode = oobCode + ) + ) + } } } "verifyEmail" -> { if (!oobCode.isNullOrBlank()) { - backStack.clear() - backStack.add( - ScreenRoute.Auth.EmailVerification( - purpose = VerificationPurpose.REGISTRATION, - oobCode = oobCode + val currentRoute = backStack.lastOrNull() + if ( + currentRoute is ScreenRoute.Auth.EmailVerification && currentRoute.purpose == VerificationPurpose.REGISTRATION + ) { + comingEmailVerificationCode = oobCode + } else { + backStack.clear() + backStack.add( + ScreenRoute.Auth.EmailVerification( + purpose = VerificationPurpose.REGISTRATION, + oobCode = oobCode + ) ) - ) + } } else { backStack.clear() backStack.add(ScreenRoute.Auth.Login) @@ -169,14 +192,19 @@ class MainActivity : ComponentActivity() { ) } entry { route -> + val oobCode = route.oobCode ?: comingEmailVerificationCode EmailVerificationScreen( email = route.email, purpose = route.purpose, - oobCode = route.oobCode, + oobCode = oobCode, onNavigateBack = {}, onNavigateToEmailVerified = { backStack.clear() backStack.add(ScreenRoute.Auth.EmailVerified) + }, + onNavigateToResetPassword = { validOobCode -> + backStack.clear() + backStack.add(ScreenRoute.Auth.ResetPassword(validOobCode)) } ) } diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt index 0422ca4..dee4b2e 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt @@ -2,5 +2,6 @@ package com.tpc.nudj.ui.screen.auth.emailVerification sealed interface EmailVerificationEvent { data object NavigateToEmailVerified : EmailVerificationEvent + data class NavigateToResetPassword(val oobCode : String) : EmailVerificationEvent data class showSnackBar(val message: String) : EmailVerificationEvent } \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt index e1232b1..90fea0d 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt @@ -42,20 +42,13 @@ fun EmailVerificationScreen( purpose: VerificationPurpose, oobCode: String? = null, onNavigateBack: () -> Unit, - onNavigateToEmailVerified: () -> Unit + onNavigateToEmailVerified: () -> Unit, + onNavigateToResetPassword: (String) -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackBarHostState = remember { SnackbarHostState() } - LaunchedEffect(email, purpose, oobCode) { - viewModel.onScreenOpened( - email = email, - purpose = purpose, - oobCode = oobCode - - ) - } LaunchedEffect(Unit) { - viewModel.events.collect { event -> + viewModel.events.collect {event -> when (event) { is EmailVerificationEvent.showSnackBar -> { snackBarHostState.showSnackbar(event.message) @@ -64,9 +57,20 @@ fun EmailVerificationScreen( EmailVerificationEvent.NavigateToEmailVerified -> { onNavigateToEmailVerified() } + is EmailVerificationEvent.NavigateToResetPassword ->{ + onNavigateToResetPassword(event.oobCode) + } } } } + LaunchedEffect(email, purpose, oobCode) { + viewModel.onScreenOpened( + email = email, + purpose = purpose, + oobCode = oobCode + + ) + } Scaffold( snackbarHost = { SnackbarHost(hostState = snackBarHostState) diff --git a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt index aaa6ee8..3d432b6 100644 --- a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt +++ b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt @@ -2,7 +2,6 @@ package com.tpc.nudj.viewmodels.auth.emailVerification import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import com.tpc.nudj.model.AuthResult import com.tpc.nudj.repository.auth.AuthRepository import com.tpc.nudj.ui.navigation.VerificationPurpose @@ -47,6 +46,7 @@ class EmailVerificationViewModel @Inject constructor( } } + is AuthResult.Success, is AuthResult.VerificationNeeded -> { _uiState.update { it.copy(isLoading = false) @@ -99,20 +99,26 @@ class EmailVerificationViewModel @Inject constructor( } private var hasStartedTimer = false + private var lastHandledOobCode: String? = null fun onScreenOpened(email: String, purpose: VerificationPurpose, oobCode: String? = null) { - if (hasStartedTimer) return - - hasStartedTimer = true - startTimer() - if (!oobCode.isNullOrBlank()) { - applyEmailVerificationCode(oobCode) - return + if (!hasStartedTimer) { + hasStartedTimer = true + startTimer() + if (purpose == VerificationPurpose.REGISTRATION) { + startCheckingEmailVerification() + } } - if (purpose == VerificationPurpose.REGISTRATION) { - startCheckingEmailVerification() + + if (!oobCode.isNullOrBlank() && oobCode != lastHandledOobCode) { + lastHandledOobCode = oobCode + when (purpose) { + VerificationPurpose.REGISTRATION -> handleEmailVerificationLink(oobCode) + VerificationPurpose.PASSWORD_RESET -> handlePasswordResetLink(oobCode) + } } } - private fun applyEmailVerificationCode(oobCode: String) { + + private fun handleEmailVerificationLink(oobCode: String) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } authRepository.applyEmailVerificationCode(oobCode) @@ -124,12 +130,34 @@ class EmailVerificationViewModel @Inject constructor( _uiState.update { it.copy(isLoading = false) } _events.emit( EmailVerificationEvent.showSnackBar( - it.message ?: "Unable to verify email" + "Invalid link, try again later." ) ) } } } + + private fun handlePasswordResetLink(oobCode: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + authRepository.verifyPasswordResetCode(oobCode) + .onSuccess { + _uiState.update { + it.copy(isLoading = false) + } + _events.emit(EmailVerificationEvent.NavigateToResetPassword(oobCode)) + } + .onFailure { + _uiState.update { + it.copy(isLoading = false) + } + _events.emit( + EmailVerificationEvent.showSnackBar("Invalid link, try again later.") + ) + } + } + } + private fun startCheckingEmailVerification() { viewModelScope.launch { while (true) { From bfeee796d1eb078fe10c3ec68ed418a8fa1ff810 Mon Sep 17 00:00:00 2001 From: nehasiwach802 Date: Thu, 16 Jul 2026 11:47:15 +0530 Subject: [PATCH 6/6] removed the navigation from MainActivity.kt and added to EmailVerification --- .../main/java/com/tpc/nudj/MainActivity.kt | 77 ++++++++----------- .../com/tpc/nudj/ui/navigation/ScreenRoute.kt | 3 +- .../EmailVerificationEvent.kt | 6 +- .../EmailVerificationScreen.kt | 20 +++-- .../EmailVerificationViewModel.kt | 41 ++++++---- 5 files changed, 75 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/tpc/nudj/MainActivity.kt b/app/src/main/java/com/tpc/nudj/MainActivity.kt index 8c0d64f..948b0d7 100644 --- a/app/src/main/java/com/tpc/nudj/MainActivity.kt +++ b/app/src/main/java/com/tpc/nudj/MainActivity.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -57,8 +57,8 @@ class MainActivity : ComponentActivity() { NudjTheme { val appViewModel: AppViewModel = hiltViewModel() val authState by appViewModel.authState.collectAsState() - var comingEmailVerificationCode by rememberSaveable { - mutableStateOf(null) + var comingEmailActionData by remember { + mutableStateOf(null) } val backStack = rememberNavBackStack(ScreenRoute.Auth.SplashScreen) @@ -94,48 +94,25 @@ class MainActivity : ComponentActivity() { } LaunchedEffect(deepLinkuri) { val uri = deepLinkuri ?: return@LaunchedEffect - val mode = uri.getQueryParameter("mode") - val oobCode = uri.getQueryParameter("oobCode") - when (mode) { - "resetPassword" -> { - if (!oobCode.isNullOrBlank()) { - val currentRoute = backStack.lastOrNull() - if ( - currentRoute is ScreenRoute.Auth.EmailVerification && currentRoute.purpose == VerificationPurpose.PASSWORD_RESET - ) { - comingEmailVerificationCode = oobCode - } else { - backStack.clear() - backStack.add( - ScreenRoute.Auth.EmailVerification( - purpose = VerificationPurpose.PASSWORD_RESET, - oobCode = oobCode - ) - ) - } - } - } - - "verifyEmail" -> { - if (!oobCode.isNullOrBlank()) { - val currentRoute = backStack.lastOrNull() - if ( - currentRoute is ScreenRoute.Auth.EmailVerification && currentRoute.purpose == VerificationPurpose.REGISTRATION - ) { - comingEmailVerificationCode = oobCode - } else { - backStack.clear() - backStack.add( - ScreenRoute.Auth.EmailVerification( - purpose = VerificationPurpose.REGISTRATION, - oobCode = oobCode - ) - ) - } - } else { - backStack.clear() - backStack.add(ScreenRoute.Auth.Login) - } + val emailActionData = EmailActionData( + mode = uri.getQueryParameter("mode"), + oobCode = uri.getQueryParameter("oobCode") + ) + if ( + emailActionData.mode == "resetPassword" || + emailActionData.mode == "verifyEmail" + ) { + comingEmailActionData = emailActionData + if (backStack.lastOrNull() !is ScreenRoute.Auth.EmailVerification) { + backStack.clear() + backStack.add( + ScreenRoute.Auth.EmailVerification( + purpose = when (emailActionData.mode) { + "resetPassword" -> VerificationPurpose.PASSWORD_RESET + else -> VerificationPurpose.REGISTRATION + } + ) + ) } } deepLinkuri = null @@ -192,17 +169,18 @@ class MainActivity : ComponentActivity() { ) } entry { route -> - val oobCode = route.oobCode ?: comingEmailVerificationCode EmailVerificationScreen( email = route.email, purpose = route.purpose, - oobCode = oobCode, + emailActionData = comingEmailActionData ?: EmailActionData(), onNavigateBack = {}, onNavigateToEmailVerified = { + comingEmailActionData = null backStack.clear() backStack.add(ScreenRoute.Auth.EmailVerified) }, onNavigateToResetPassword = { validOobCode -> + comingEmailActionData = null backStack.clear() backStack.add(ScreenRoute.Auth.ResetPassword(validOobCode)) } @@ -211,6 +189,7 @@ class MainActivity : ComponentActivity() { entry { ForgetPasswordScreen(onNavigateToEmailVerification = { email -> + comingEmailActionData = null backStack.add( ScreenRoute.Auth.EmailVerification( email = email, @@ -260,3 +239,7 @@ class MainActivity : ComponentActivity() { } } } +data class EmailActionData( + val mode: String? = null, + val oobCode: String? = null +) diff --git a/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt b/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt index ee324b6..4b20444 100644 --- a/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt +++ b/app/src/main/java/com/tpc/nudj/ui/navigation/ScreenRoute.kt @@ -29,8 +29,7 @@ sealed interface ScreenRoute : NavKey { @Serializable data class EmailVerification( val email: String = "", - val purpose: VerificationPurpose = VerificationPurpose.REGISTRATION, - val oobCode: String? = null + val purpose: VerificationPurpose = VerificationPurpose.REGISTRATION ) : Auth @Serializable diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt index dee4b2e..2283da5 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationEvent.kt @@ -1,7 +1,7 @@ package com.tpc.nudj.ui.screen.auth.emailVerification sealed interface EmailVerificationEvent { - data object NavigateToEmailVerified : EmailVerificationEvent - data class NavigateToResetPassword(val oobCode : String) : EmailVerificationEvent - data class showSnackBar(val message: String) : EmailVerificationEvent + data class ShowSnackBar(val message: String) : EmailVerificationEvent + data object RegistrationVerificationCompleted : EmailVerificationEvent + data class NavigateToResetPassword(val oobCode: String) : EmailVerificationEvent } \ No newline at end of file diff --git a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt index 90fea0d..bf4a5e8 100644 --- a/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt +++ b/app/src/main/java/com/tpc/nudj/ui/screen/auth/emailVerification/EmailVerificationScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tpc.nudj.EmailActionData import com.tpc.nudj.R import com.tpc.nudj.ui.components.LoadingIndicator import com.tpc.nudj.ui.components.NudjTopAppBar @@ -40,7 +41,7 @@ fun EmailVerificationScreen( viewModel: EmailVerificationViewModel = hiltViewModel(), email: String, purpose: VerificationPurpose, - oobCode: String? = null, + emailActionData: EmailActionData = EmailActionData(), onNavigateBack: () -> Unit, onNavigateToEmailVerified: () -> Unit, onNavigateToResetPassword: (String) -> Unit = {} @@ -50,11 +51,11 @@ fun EmailVerificationScreen( LaunchedEffect(Unit) { viewModel.events.collect {event -> when (event) { - is EmailVerificationEvent.showSnackBar -> { + is EmailVerificationEvent.ShowSnackBar -> { snackBarHostState.showSnackbar(event.message) } - EmailVerificationEvent.NavigateToEmailVerified -> { + EmailVerificationEvent.RegistrationVerificationCompleted -> { onNavigateToEmailVerified() } is EmailVerificationEvent.NavigateToResetPassword ->{ @@ -63,11 +64,16 @@ fun EmailVerificationScreen( } } } - LaunchedEffect(email, purpose, oobCode) { + LaunchedEffect(email, purpose) { viewModel.onScreenOpened( - email = email, - purpose = purpose, - oobCode = oobCode + purpose = purpose + + ) + } + LaunchedEffect(emailActionData.mode, emailActionData.oobCode) { + viewModel.onEmailActionReceived( + mode = emailActionData.mode, + oobCode = emailActionData.oobCode ) } diff --git a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt index 3d432b6..00088a8 100644 --- a/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt +++ b/app/src/main/java/com/tpc/nudj/viewmodels/auth/emailVerification/EmailVerificationViewModel.kt @@ -52,7 +52,7 @@ class EmailVerificationViewModel @Inject constructor( it.copy(isLoading = false) } _events.emit( - EmailVerificationEvent.showSnackBar("Verification email sent again") + EmailVerificationEvent.ShowSnackBar("Verification email sent again") ) startTimer() @@ -64,7 +64,7 @@ class EmailVerificationViewModel @Inject constructor( } _events.emit( - EmailVerificationEvent.showSnackBar("Some error occurred while verifying the email. Please try again later.") + EmailVerificationEvent.ShowSnackBar("Some error occurred while verifying the email. Please try again later.") ) } @@ -100,7 +100,7 @@ class EmailVerificationViewModel @Inject constructor( } private var hasStartedTimer = false private var lastHandledOobCode: String? = null - fun onScreenOpened(email: String, purpose: VerificationPurpose, oobCode: String? = null) { + fun onScreenOpened(purpose: VerificationPurpose) { if (!hasStartedTimer) { hasStartedTimer = true startTimer() @@ -108,12 +108,27 @@ class EmailVerificationViewModel @Inject constructor( startCheckingEmailVerification() } } + } + + fun onEmailActionReceived(mode: String?, oobCode: String?) { + if (mode.isNullOrBlank() || oobCode.isNullOrBlank()) { + return + } + + if (oobCode == lastHandledOobCode) { + return + } + lastHandledOobCode = oobCode + when (mode) { + "verifyEmail" -> {handleEmailVerificationLink(oobCode) } + "resetPassword" -> {handlePasswordResetLink(oobCode)} - if (!oobCode.isNullOrBlank() && oobCode != lastHandledOobCode) { - lastHandledOobCode = oobCode - when (purpose) { - VerificationPurpose.REGISTRATION -> handleEmailVerificationLink(oobCode) - VerificationPurpose.PASSWORD_RESET -> handlePasswordResetLink(oobCode) + else -> { + viewModelScope.launch { + _events.emit( + EmailVerificationEvent.ShowSnackBar("Unsupported email link.") + ) + } } } } @@ -124,12 +139,12 @@ class EmailVerificationViewModel @Inject constructor( authRepository.applyEmailVerificationCode(oobCode) .onSuccess { _uiState.update { it.copy(isLoading = false) } - _events.emit(EmailVerificationEvent.NavigateToEmailVerified) + _events.emit(EmailVerificationEvent.RegistrationVerificationCompleted) } .onFailure { _uiState.update { it.copy(isLoading = false) } _events.emit( - EmailVerificationEvent.showSnackBar( + EmailVerificationEvent.ShowSnackBar( "Invalid link, try again later." ) ) @@ -152,7 +167,7 @@ class EmailVerificationViewModel @Inject constructor( it.copy(isLoading = false) } _events.emit( - EmailVerificationEvent.showSnackBar("Invalid link, try again later.") + EmailVerificationEvent.ShowSnackBar("Invalid link, try again later.") ) } } @@ -167,14 +182,14 @@ class EmailVerificationViewModel @Inject constructor( if (isVerified) { _events.emit( - EmailVerificationEvent.NavigateToEmailVerified + EmailVerificationEvent.RegistrationVerificationCompleted ) break } } catch (e: Exception) { _events.emit( - EmailVerificationEvent.showSnackBar("Unable to check email verification") + EmailVerificationEvent.ShowSnackBar("Unable to check email verification") ) } }