diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bc7d60d..ca99502 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,13 +17,25 @@ android:exported="true" android:label="@string/app_name" android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode" + android:launchMode="singleTask" android:theme="@style/Theme.Nudj"> - + + + + + + + + + + - \ 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..948b0d7 100644 --- a/app/src/main/java/com/tpc/nudj/MainActivity.kt +++ b/app/src/main/java/com/tpc/nudj/MainActivity.kt @@ -1,5 +1,7 @@ package com.tpc.nudj +import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -8,6 +10,9 @@ 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.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -16,6 +21,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,17 +40,33 @@ import dagger.hilt.android.AndroidEntryPoint @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?) { + super.onCreate(savedInstanceState) + deepLinkuri = intent?.data enableEdgeToEdge() setContent { NudjTheme { val appViewModel: AppViewModel = hiltViewModel() val authState by appViewModel.authState.collectAsState() + var comingEmailActionData by remember { + mutableStateOf(null) + } val backStack = rememberNavBackStack(ScreenRoute.Auth.SplashScreen) - LaunchedEffect(authState) { + val deepLinkMode = deepLinkuri?.getQueryParameter("mode") + if (deepLinkMode == "resetPassword" || deepLinkMode == "verifyEmail") { + return@LaunchedEffect + } when (val state = authState) { is AppViewModel.AuthState.Initial -> {} is AppViewModel.AuthState.Unauthenticated -> { @@ -53,7 +75,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,7 +92,35 @@ class MainActivity : ComponentActivity() { } } } - NavDisplay( + LaunchedEffect(deepLinkuri) { + val uri = deepLinkuri ?: return@LaunchedEffect + 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 + } + + + + NavDisplay( backStack = backStack, modifier = Modifier.fillMaxSize(), entryDecorators = listOf( @@ -92,6 +142,9 @@ class MainActivity : ComponentActivity() { LoginScreen( navigateToCreateAccount ={ backStack.add(ScreenRoute.Auth.Register) + }, + navigateToForgotPassword = { + backStack.add(ScreenRoute.Auth.ForgotPassword) } ) } @@ -102,7 +155,7 @@ class MainActivity : ComponentActivity() { }, onNavigateToEmailVerification = { backStack.clear() - backStack.add(ScreenRoute.Auth.EmailVerification) + backStack.add(ScreenRoute.Auth.EmailVerification(purpose = VerificationPurpose.REGISTRATION)) }, onNavigateToUserDetailsInput = { backStack.clear() @@ -115,23 +168,42 @@ class MainActivity : ComponentActivity() { ) } - entry { + entry { route -> EmailVerificationScreen( + email = route.email, + purpose = route.purpose, + 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)) } ) } entry { - ForgetPasswordScreen( + ForgetPasswordScreen(onNavigateToEmailVerification = { email -> + comingEmailActionData = null + 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 +225,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") @@ -167,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/repository/auth/AuthRepository.kt b/app/src/main/java/com/tpc/nudj/repository/auth/AuthRepository.kt index 3181a0d..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,4 +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 e6ff815..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 @@ -190,4 +190,30 @@ class FirebaseAuthRepository( 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" + ) + ) + } + } + 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 1e4b22d..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 @@ -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 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..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,6 +1,7 @@ package com.tpc.nudj.ui.screen.auth.emailVerification sealed interface EmailVerificationEvent { - data object NavigateToEmailVerified : 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 4631e8d..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,11 +24,13 @@ 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 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,27 +39,44 @@ import kotlinx.coroutines.launch @Composable fun EmailVerificationScreen( viewModel: EmailVerificationViewModel = hiltViewModel(), + email: String, + purpose: VerificationPurpose, + emailActionData: EmailActionData = EmailActionData(), onNavigateBack: () -> Unit, - onNavigateToEmailVerified: () -> Unit + onNavigateToEmailVerified: () -> Unit, + onNavigateToResetPassword: (String) -> Unit = {} ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val snackBarHostState = remember { SnackbarHostState() } LaunchedEffect(Unit) { - viewModel.onScreenOpened() - } - LaunchedEffect(Unit) { - viewModel.events.collect { event -> + viewModel.events.collect {event -> when (event) { - is EmailVerificationEvent.showSnackBar -> { + is EmailVerificationEvent.ShowSnackBar -> { snackBarHostState.showSnackbar(event.message) } - EmailVerificationEvent.NavigateToEmailVerified -> { + EmailVerificationEvent.RegistrationVerificationCompleted -> { onNavigateToEmailVerified() } + is EmailVerificationEvent.NavigateToResetPassword ->{ + onNavigateToResetPassword(event.oobCode) + } } } } + LaunchedEffect(email, purpose) { + viewModel.onScreenOpened( + purpose = purpose + + ) + } + LaunchedEffect(emailActionData.mode, emailActionData.oobCode) { + viewModel.onEmailActionReceived( + mode = emailActionData.mode, + oobCode = emailActionData.oobCode + + ) + } Scaffold( snackbarHost = { SnackbarHost(hostState = snackBarHostState) @@ -70,7 +89,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 +113,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 +197,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/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/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..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 @@ -2,9 +2,9 @@ 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 import com.tpc.nudj.ui.screen.auth.emailVerification.EmailVerificationEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -26,12 +26,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 { @@ -39,13 +46,13 @@ class EmailVerificationViewModel @Inject constructor( } } + is AuthResult.Success, is AuthResult.VerificationNeeded -> { _uiState.update { it.copy(isLoading = false) } - _events.emit( - EmailVerificationEvent.showSnackBar("Verification email sent again") + EmailVerificationEvent.ShowSnackBar("Verification email sent again") ) startTimer() @@ -57,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.") ) } @@ -92,13 +99,80 @@ class EmailVerificationViewModel @Inject constructor( } private var hasStartedTimer = false - fun onScreenOpened() { - if (hasStartedTimer) return + private var lastHandledOobCode: String? = null + fun onScreenOpened(purpose: VerificationPurpose) { + if (!hasStartedTimer) { + hasStartedTimer = true + startTimer() + if (purpose == VerificationPurpose.REGISTRATION) { + 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)} - hasStartedTimer = true - startTimer() - startCheckingEmailVerification() + else -> { + viewModelScope.launch { + _events.emit( + EmailVerificationEvent.ShowSnackBar("Unsupported email link.") + ) + } + } + } } + + private fun handleEmailVerificationLink(oobCode: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + authRepository.applyEmailVerificationCode(oobCode) + .onSuccess { + _uiState.update { it.copy(isLoading = false) } + _events.emit(EmailVerificationEvent.RegistrationVerificationCompleted) + } + .onFailure { + _uiState.update { it.copy(isLoading = false) } + _events.emit( + EmailVerificationEvent.ShowSnackBar( + "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) { @@ -108,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") ) } } 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 + + } + + } + } }