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:
Trey t
2025-11-09 18:29:29 -06:00
parent e6dc54017b
commit fdcc2a2e16
19 changed files with 2196 additions and 4 deletions

View File

@@ -19,6 +19,18 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep link for password reset -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="mycrib"
android:host="reset-password" />
</intent-filter>
</activity>
</application>

View File

@@ -1,10 +1,15 @@
package com.example.mycrib
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.tooling.preview.Preview
import com.mycrib.storage.TokenManager
import com.mycrib.storage.TokenStorage
@@ -12,6 +17,8 @@ import com.mycrib.storage.TaskCacheManager
import com.mycrib.storage.TaskCacheStorage
class MainActivity : ComponentActivity() {
private var deepLinkResetToken by mutableStateOf<String?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
@@ -22,8 +29,33 @@ class MainActivity : ComponentActivity() {
// Initialize TaskCacheStorage for offline task caching
TaskCacheStorage.initialize(TaskCacheManager.getInstance(applicationContext))
// Handle deep link from intent
handleDeepLink(intent)
setContent {
App()
App(
deepLinkResetToken = deepLinkResetToken,
onClearDeepLinkToken = {
deepLinkResetToken = null
}
)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleDeepLink(intent)
}
private fun handleDeepLink(intent: Intent?) {
val data: Uri? = intent?.data
if (data != null && data.scheme == "mycrib" && data.host == "reset-password") {
// Extract token from query parameter
val token = data.getQueryParameter("token")
if (token != null) {
deepLinkResetToken = token
println("Deep link received with token: $token")
}
}
}
}
@@ -31,5 +63,5 @@ class MainActivity : ComponentActivity() {
@Preview
@Composable
fun AppAndroidPreview() {
App()
App(deepLinkResetToken = null)
}