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
+
+ }
+
+ }
+ }
}