Add password reset feature for iOS and Android with deep link support
Implemented complete password reset flow with email verification and deep linking: iOS (SwiftUI): - PasswordResetViewModel: Manages 3-step flow (request, verify, reset) with deep link support - ForgotPasswordView: Email entry screen with success/error states - VerifyResetCodeView: 6-digit code verification with resend option - ResetPasswordView: Password reset with live validation and strength indicators - PasswordResetFlow: Container managing navigation between screens - Deep link handling in iOSApp.swift for mycrib://reset-password?token=xxx - Info.plist: Added CFBundleURLTypes for deep link scheme Android (Compose): - PasswordResetViewModel: StateFlow-based state management with coroutines - ForgotPasswordScreen: Material3 email entry with auto-navigation - VerifyResetCodeScreen: Code verification with Material3 design - ResetPasswordScreen: Password reset with live validation checklist - Deep link handling in MainActivity.kt and AndroidManifest.xml - Token cleanup callback to prevent reusing expired deep link tokens - Shared ViewModel scoping across all password reset screens - Improved error handling for Django validation errors Shared: - Routes: Added ForgotPasswordRoute, VerifyResetCodeRoute, ResetPasswordRoute - AuthApi: Enhanced resetPassword with Django validation error parsing - User models: Added password reset request/response models Security features: - Deep link tokens expire after use - Proper token validation on backend - Session invalidation after password change - Password strength requirements enforced 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,13 +19,18 @@ import androidx.compose.ui.Modifier
|
||||
import com.mycrib.android.ui.screens.AddResidenceScreen
|
||||
import com.mycrib.android.ui.screens.EditResidenceScreen
|
||||
import com.mycrib.android.ui.screens.EditTaskScreen
|
||||
import com.mycrib.android.ui.screens.ForgotPasswordScreen
|
||||
import com.mycrib.android.ui.screens.HomeScreen
|
||||
import com.mycrib.android.ui.screens.LoginScreen
|
||||
import com.mycrib.android.ui.screens.RegisterScreen
|
||||
import com.mycrib.android.ui.screens.ResetPasswordScreen
|
||||
import com.mycrib.android.ui.screens.ResidenceDetailScreen
|
||||
import com.mycrib.android.ui.screens.ResidencesScreen
|
||||
import com.mycrib.android.ui.screens.TasksScreen
|
||||
import com.mycrib.android.ui.screens.VerifyEmailScreen
|
||||
import com.mycrib.android.ui.screens.VerifyResetCodeScreen
|
||||
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
|
||||
@@ -53,7 +58,10 @@ import mycrib.composeapp.generated.resources.compose_multiplatform
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun App() {
|
||||
fun App(
|
||||
deepLinkResetToken: String? = null,
|
||||
onClearDeepLinkToken: () -> Unit = {}
|
||||
) {
|
||||
var isLoggedIn by remember { mutableStateOf(TokenStorage.hasToken()) }
|
||||
var isVerified by remember { mutableStateOf(false) }
|
||||
var isCheckingAuth by remember { mutableStateOf(true) }
|
||||
@@ -105,6 +113,7 @@ fun App() {
|
||||
}
|
||||
|
||||
val startDestination = when {
|
||||
deepLinkResetToken != null -> ForgotPasswordRoute
|
||||
!isLoggedIn -> LoginRoute
|
||||
!isVerified -> VerifyEmailRoute
|
||||
else -> MainRoute
|
||||
@@ -139,6 +148,9 @@ fun App() {
|
||||
},
|
||||
onNavigateToRegister = {
|
||||
navController.navigate(RegisterRoute)
|
||||
},
|
||||
onNavigateToForgotPassword = {
|
||||
navController.navigate(ForgotPasswordRoute)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -160,6 +172,71 @@ fun App() {
|
||||
)
|
||||
}
|
||||
|
||||
composable<ForgotPasswordRoute> { backStackEntry ->
|
||||
// Create shared ViewModel for all password reset screens
|
||||
val parentEntry = remember(backStackEntry) {
|
||||
navController.getBackStackEntry<ForgotPasswordRoute>()
|
||||
}
|
||||
val passwordResetViewModel: PasswordResetViewModel = viewModel(parentEntry) {
|
||||
PasswordResetViewModel(deepLinkToken = deepLinkResetToken)
|
||||
}
|
||||
|
||||
ForgotPasswordScreen(
|
||||
onNavigateBack = {
|
||||
// Clear deep link token when navigating back to login
|
||||
onClearDeepLinkToken()
|
||||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToVerify = {
|
||||
navController.navigate(VerifyResetCodeRoute)
|
||||
},
|
||||
onNavigateToReset = {
|
||||
navController.navigate(ResetPasswordRoute)
|
||||
},
|
||||
viewModel = passwordResetViewModel
|
||||
)
|
||||
}
|
||||
|
||||
composable<VerifyResetCodeRoute> { backStackEntry ->
|
||||
// Use shared ViewModel from ForgotPasswordRoute
|
||||
val parentEntry = remember(backStackEntry) {
|
||||
navController.getBackStackEntry<ForgotPasswordRoute>()
|
||||
}
|
||||
val passwordResetViewModel: PasswordResetViewModel = viewModel(parentEntry) { PasswordResetViewModel() }
|
||||
|
||||
VerifyResetCodeScreen(
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
onNavigateToReset = {
|
||||
navController.navigate(ResetPasswordRoute)
|
||||
},
|
||||
viewModel = passwordResetViewModel
|
||||
)
|
||||
}
|
||||
|
||||
composable<ResetPasswordRoute> { backStackEntry ->
|
||||
// Use shared ViewModel from ForgotPasswordRoute
|
||||
val parentEntry = remember(backStackEntry) {
|
||||
navController.getBackStackEntry<ForgotPasswordRoute>()
|
||||
}
|
||||
val passwordResetViewModel: PasswordResetViewModel = viewModel(parentEntry) { PasswordResetViewModel() }
|
||||
|
||||
ResetPasswordScreen(
|
||||
onPasswordResetSuccess = {
|
||||
// Clear deep link token and navigate back to login after successful password reset
|
||||
onClearDeepLinkToken()
|
||||
navController.navigate(LoginRoute) {
|
||||
popUpTo<ForgotPasswordRoute> { inclusive = true }
|
||||
}
|
||||
},
|
||||
onNavigateBack = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
viewModel = passwordResetViewModel
|
||||
)
|
||||
}
|
||||
|
||||
composable<VerifyEmailRoute> {
|
||||
VerifyEmailScreen(
|
||||
onVerifySuccess = {
|
||||
|
||||
Reference in New Issue
Block a user