From eb04163b3b9d848f5091db00591fb13559d6d7e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Nov 2025 19:32:16 +0000 Subject: [PATCH] feat: implement authentication system for mobile app - Add API integration layer with axios and token management - Create Zustand auth store for state management - Implement login screen with validation - Implement signup screen with password strength indicator - Implement forgot password flow with multi-step UI - Update root layout with auth state protection - Integrate profile screen with auth store - Install AsyncStorage and expo-secure-store dependencies --- mobile/app/(auth)/forgot-password.tsx | 565 +++++++++++++++++ mobile/app/(auth)/login.tsx | 354 +++++++++++ mobile/app/(auth)/signup.tsx | 475 +++++++++++++++ mobile/app/(tabs)/profile.tsx | 40 +- mobile/app/_layout.tsx | 56 +- mobile/app/lib/api.ts | 284 +++++++++ mobile/app/lib/auth-store.ts | 220 +++++++ mobile/app/lib/types.ts | 93 +++ mobile/package-lock.json | 837 +++++++++++++++++++++++++- mobile/package.json | 2 + 10 files changed, 2909 insertions(+), 17 deletions(-) create mode 100644 mobile/app/(auth)/forgot-password.tsx create mode 100644 mobile/app/(auth)/login.tsx create mode 100644 mobile/app/(auth)/signup.tsx create mode 100644 mobile/app/lib/api.ts create mode 100644 mobile/app/lib/auth-store.ts create mode 100644 mobile/app/lib/types.ts diff --git a/mobile/app/(auth)/forgot-password.tsx b/mobile/app/(auth)/forgot-password.tsx new file mode 100644 index 0000000..0315e56 --- /dev/null +++ b/mobile/app/(auth)/forgot-password.tsx @@ -0,0 +1,565 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + ScrollView, + KeyboardAvoidingView, + Platform, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { authApi, handleApiError } from '../lib/api'; + +type Step = 'email' | 'sent' | 'reset'; + +export default function ForgotPasswordScreen() { + const router = useRouter(); + + const [step, setStep] = useState('email'); + const [email, setEmail] = useState(''); + const [resetToken, setResetToken] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [validationErrors, setValidationErrors] = useState<{ + email?: string; + resetToken?: string; + newPassword?: string; + confirmPassword?: string; + }>({}); + + // Validate email format + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Validate email step + const validateEmailStep = (): boolean => { + const errors: { email?: string } = {}; + + if (!email.trim()) { + errors.email = 'Email is required'; + } else if (!validateEmail(email)) { + errors.email = 'Please enter a valid email'; + } + + setValidationErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Validate reset step + const validateResetStep = (): boolean => { + const errors: { + resetToken?: string; + newPassword?: string; + confirmPassword?: string; + } = {}; + + if (!resetToken.trim()) { + errors.resetToken = 'Reset code is required'; + } + + if (!newPassword) { + errors.newPassword = 'New password is required'; + } else if (newPassword.length < 8) { + errors.newPassword = 'Password must be at least 8 characters'; + } + + if (!confirmPassword) { + errors.confirmPassword = 'Please confirm your password'; + } else if (newPassword !== confirmPassword) { + errors.confirmPassword = 'Passwords do not match'; + } + + setValidationErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Handle send reset email + const handleSendReset = async () => { + setError(null); + + if (!validateEmailStep()) { + return; + } + + setIsLoading(true); + + try { + await authApi.forgotPassword({ email: email.trim().toLowerCase() }); + setStep('sent'); + } catch (err) { + setError(handleApiError(err)); + } finally { + setIsLoading(false); + } + }; + + // Handle reset password + const handleResetPassword = async () => { + setError(null); + + if (!validateResetStep()) { + return; + } + + setIsLoading(true); + + try { + await authApi.resetPassword({ + token: resetToken.trim(), + newPassword, + }); + // Navigate to login with success message + router.replace('/(auth)/login'); + } catch (err) { + setError(handleApiError(err)); + } finally { + setIsLoading(false); + } + }; + + // Navigate back to login + const handleBackToLogin = () => { + router.push('/(auth)/login'); + }; + + // Render email step + const renderEmailStep = () => ( + <> + + Forgot Password + + Enter your email address and we'll send you a link to reset your password. + + + + + + Email + { + setEmail(text); + if (validationErrors.email) { + setValidationErrors((prev) => ({ ...prev, email: undefined })); + } + }} + keyboardType="email-address" + autoCapitalize="none" + autoCorrect={false} + autoComplete="email" + editable={!isLoading} + /> + {validationErrors.email && ( + {validationErrors.email} + )} + + + {error && ( + + {error} + + )} + + + {isLoading ? ( + + ) : ( + Send Reset Link + )} + + + + Back to Sign In + + + + ); + + // Render sent confirmation step + const renderSentStep = () => ( + <> + + + + + Check Your Email + + We've sent a password reset link to{'\n'} + {email} + + + + + + What's next? + + 1. Check your email inbox{'\n'} + 2. Click the reset link in the email{'\n'} + 3. Enter your reset code below + + + + setStep('reset')} + > + I Have My Reset Code + + + + {isLoading ? ( + + ) : ( + Resend Email + )} + + + + Back to Sign In + + + + ); + + // Render reset password step + const renderResetStep = () => ( + <> + + Reset Password + + Enter the code from your email and create a new password. + + + + + {/* Reset Token Input */} + + Reset Code + { + setResetToken(text); + if (validationErrors.resetToken) { + setValidationErrors((prev) => ({ ...prev, resetToken: undefined })); + } + }} + autoCapitalize="none" + autoCorrect={false} + editable={!isLoading} + /> + {validationErrors.resetToken && ( + {validationErrors.resetToken} + )} + + + {/* New Password Input */} + + New Password + + { + setNewPassword(text); + if (validationErrors.newPassword) { + setValidationErrors((prev) => ({ ...prev, newPassword: undefined })); + } + }} + secureTextEntry={!showPassword} + autoCapitalize="none" + autoCorrect={false} + editable={!isLoading} + /> + setShowPassword(!showPassword)} + disabled={isLoading} + > + + {showPassword ? 'Hide' : 'Show'} + + + + {validationErrors.newPassword && ( + {validationErrors.newPassword} + )} + + + {/* Confirm Password Input */} + + Confirm New Password + { + setConfirmPassword(text); + if (validationErrors.confirmPassword) { + setValidationErrors((prev) => ({ + ...prev, + confirmPassword: undefined, + })); + } + }} + secureTextEntry={!showPassword} + autoCapitalize="none" + autoCorrect={false} + editable={!isLoading} + /> + {validationErrors.confirmPassword && ( + {validationErrors.confirmPassword} + )} + + + {error && ( + + {error} + + )} + + + {isLoading ? ( + + ) : ( + Reset Password + )} + + + setStep('sent')} + disabled={isLoading} + > + Go Back + + + + ); + + return ( + + + {step === 'email' && renderEmailStep()} + {step === 'sent' && renderSentStep()} + {step === 'reset' && renderResetStep()} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + scrollContent: { + flexGrow: 1, + paddingHorizontal: 24, + paddingTop: 60, + paddingBottom: 40, + }, + header: { + marginBottom: 32, + alignItems: 'center', + }, + iconContainer: { + width: 64, + height: 64, + borderRadius: 32, + backgroundColor: '#E8F5E9', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 24, + }, + checkIcon: { + fontSize: 32, + color: '#34C759', + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#333333', + marginBottom: 12, + textAlign: 'center', + }, + subtitle: { + fontSize: 16, + color: '#666666', + textAlign: 'center', + lineHeight: 24, + }, + emailHighlight: { + fontWeight: '600', + color: '#007AFF', + }, + form: { + flex: 1, + }, + inputContainer: { + marginBottom: 20, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#333333', + marginBottom: 8, + }, + input: { + height: 50, + borderWidth: 1, + borderColor: '#E0E0E0', + borderRadius: 8, + paddingHorizontal: 16, + fontSize: 16, + color: '#333333', + backgroundColor: '#FAFAFA', + }, + inputError: { + borderColor: '#FF3B30', + }, + passwordContainer: { + position: 'relative', + }, + passwordInput: { + paddingRight: 60, + }, + showPasswordButton: { + position: 'absolute', + right: 16, + top: 0, + bottom: 0, + justifyContent: 'center', + }, + showPasswordText: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, + errorText: { + fontSize: 12, + color: '#FF3B30', + marginTop: 4, + }, + apiError: { + backgroundColor: '#FFF0F0', + borderRadius: 8, + padding: 12, + marginBottom: 16, + borderWidth: 1, + borderColor: '#FF3B30', + }, + apiErrorText: { + fontSize: 14, + color: '#FF3B30', + textAlign: 'center', + }, + instructionsContainer: { + backgroundColor: '#F5F5F5', + borderRadius: 8, + padding: 16, + marginBottom: 24, + }, + instructionsTitle: { + fontSize: 14, + fontWeight: '600', + color: '#333333', + marginBottom: 8, + }, + instructionsText: { + fontSize: 14, + color: '#666666', + lineHeight: 22, + }, + button: { + height: 50, + backgroundColor: '#007AFF', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, + }, + buttonDisabled: { + backgroundColor: '#B0B0B0', + }, + buttonText: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + }, + resendButton: { + height: 50, + backgroundColor: '#FFFFFF', + borderRadius: 8, + borderWidth: 1, + borderColor: '#007AFF', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, + }, + resendButtonText: { + fontSize: 16, + fontWeight: '600', + color: '#007AFF', + }, + backButton: { + height: 50, + justifyContent: 'center', + alignItems: 'center', + }, + backButtonText: { + fontSize: 14, + color: '#666666', + fontWeight: '600', + }, +}); diff --git a/mobile/app/(auth)/login.tsx b/mobile/app/(auth)/login.tsx new file mode 100644 index 0000000..be22a00 --- /dev/null +++ b/mobile/app/(auth)/login.tsx @@ -0,0 +1,354 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + ScrollView, + KeyboardAvoidingView, + Platform, + ActivityIndicator, + Alert, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { useAuthStore } from '../lib/auth-store'; + +export default function LoginScreen() { + const router = useRouter(); + const { login, isLoading, error, clearError } = useAuthStore(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [validationErrors, setValidationErrors] = useState<{ + email?: string; + password?: string; + }>({}); + + // Validate email format + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Validate form + const validateForm = (): boolean => { + const errors: { email?: string; password?: string } = {}; + + if (!email.trim()) { + errors.email = 'Email is required'; + } else if (!validateEmail(email)) { + errors.email = 'Please enter a valid email'; + } + + if (!password) { + errors.password = 'Password is required'; + } else if (password.length < 6) { + errors.password = 'Password must be at least 6 characters'; + } + + setValidationErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Handle login + const handleLogin = async () => { + clearError(); + + if (!validateForm()) { + return; + } + + try { + await login({ email: email.trim().toLowerCase(), password }); + router.replace('/(tabs)'); + } catch { + // Error is handled by the store + } + }; + + // Navigate to signup + const handleSignup = () => { + clearError(); + router.push('/(auth)/signup'); + }; + + // Navigate to forgot password + const handleForgotPassword = () => { + clearError(); + router.push('/(auth)/forgot-password'); + }; + + return ( + + + {/* Header */} + + Welcome Back + Sign in to your account + + + {/* Form */} + + {/* Email Input */} + + Email + { + setEmail(text); + if (validationErrors.email) { + setValidationErrors((prev) => ({ ...prev, email: undefined })); + } + }} + keyboardType="email-address" + autoCapitalize="none" + autoCorrect={false} + autoComplete="email" + editable={!isLoading} + /> + {validationErrors.email && ( + {validationErrors.email} + )} + + + {/* Password Input */} + + Password + + { + setPassword(text); + if (validationErrors.password) { + setValidationErrors((prev) => ({ ...prev, password: undefined })); + } + }} + secureTextEntry={!showPassword} + autoCapitalize="none" + autoCorrect={false} + autoComplete="password" + editable={!isLoading} + /> + setShowPassword(!showPassword)} + disabled={isLoading} + > + + {showPassword ? 'Hide' : 'Show'} + + + + {validationErrors.password && ( + {validationErrors.password} + )} + + + {/* Forgot Password Link */} + + Forgot Password? + + + {/* API Error */} + {error && ( + + {error} + + )} + + {/* Sign In Button */} + + {isLoading ? ( + + ) : ( + Sign In + )} + + + {/* Sign Up Link */} + + Don't have an account? + + Sign Up + + + + + {/* App Info */} + + MoreThanADiagnosis + + Supporting the kidney disease community + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + scrollContent: { + flexGrow: 1, + paddingHorizontal: 24, + paddingTop: 60, + paddingBottom: 40, + }, + header: { + marginBottom: 40, + }, + title: { + fontSize: 32, + fontWeight: 'bold', + color: '#333333', + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: '#666666', + }, + form: { + flex: 1, + }, + inputContainer: { + marginBottom: 20, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#333333', + marginBottom: 8, + }, + input: { + height: 50, + borderWidth: 1, + borderColor: '#E0E0E0', + borderRadius: 8, + paddingHorizontal: 16, + fontSize: 16, + color: '#333333', + backgroundColor: '#FAFAFA', + }, + inputError: { + borderColor: '#FF3B30', + }, + passwordContainer: { + position: 'relative', + }, + passwordInput: { + paddingRight: 60, + }, + showPasswordButton: { + position: 'absolute', + right: 16, + top: 0, + bottom: 0, + justifyContent: 'center', + }, + showPasswordText: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, + errorText: { + fontSize: 12, + color: '#FF3B30', + marginTop: 4, + }, + forgotPassword: { + alignSelf: 'flex-end', + marginBottom: 24, + }, + forgotPasswordText: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, + apiError: { + backgroundColor: '#FFF0F0', + borderRadius: 8, + padding: 12, + marginBottom: 16, + borderWidth: 1, + borderColor: '#FF3B30', + }, + apiErrorText: { + fontSize: 14, + color: '#FF3B30', + textAlign: 'center', + }, + button: { + height: 50, + backgroundColor: '#007AFF', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 24, + }, + buttonDisabled: { + backgroundColor: '#B0B0B0', + }, + buttonText: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + }, + signupContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + signupText: { + fontSize: 14, + color: '#666666', + }, + signupLink: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, + footer: { + marginTop: 40, + alignItems: 'center', + }, + footerText: { + fontSize: 16, + fontWeight: '600', + color: '#333333', + marginBottom: 4, + }, + footerSubtext: { + fontSize: 12, + color: '#999999', + }, +}); diff --git a/mobile/app/(auth)/signup.tsx b/mobile/app/(auth)/signup.tsx new file mode 100644 index 0000000..55a306c --- /dev/null +++ b/mobile/app/(auth)/signup.tsx @@ -0,0 +1,475 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + ScrollView, + KeyboardAvoidingView, + Platform, + ActivityIndicator, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { useAuthStore } from '../lib/auth-store'; + +export default function SignupScreen() { + const router = useRouter(); + const { signup, isLoading, error, clearError } = useAuthStore(); + + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [validationErrors, setValidationErrors] = useState<{ + name?: string; + email?: string; + password?: string; + confirmPassword?: string; + }>({}); + + // Validate email format + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + // Calculate password strength + const getPasswordStrength = (password: string): { level: number; text: string; color: string } => { + if (!password) { + return { level: 0, text: '', color: '#E0E0E0' }; + } + + let strength = 0; + + if (password.length >= 8) strength++; + if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++; + if (/\d/.test(password)) strength++; + if (/[^a-zA-Z0-9]/.test(password)) strength++; + + const levels = [ + { level: 1, text: 'Weak', color: '#FF3B30' }, + { level: 2, text: 'Fair', color: '#FF9500' }, + { level: 3, text: 'Good', color: '#FFCC00' }, + { level: 4, text: 'Strong', color: '#34C759' }, + ]; + + return levels[Math.min(strength, 4) - 1] || { level: 0, text: 'Too short', color: '#FF3B30' }; + }; + + // Validate form + const validateForm = (): boolean => { + const errors: { + name?: string; + email?: string; + password?: string; + confirmPassword?: string; + } = {}; + + if (!name.trim()) { + errors.name = 'Name is required'; + } else if (name.trim().length < 2) { + errors.name = 'Name must be at least 2 characters'; + } + + if (!email.trim()) { + errors.email = 'Email is required'; + } else if (!validateEmail(email)) { + errors.email = 'Please enter a valid email'; + } + + if (!password) { + errors.password = 'Password is required'; + } else if (password.length < 8) { + errors.password = 'Password must be at least 8 characters'; + } + + if (!confirmPassword) { + errors.confirmPassword = 'Please confirm your password'; + } else if (password !== confirmPassword) { + errors.confirmPassword = 'Passwords do not match'; + } + + setValidationErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Handle signup + const handleSignup = async () => { + clearError(); + + if (!validateForm()) { + return; + } + + try { + await signup({ + name: name.trim(), + email: email.trim().toLowerCase(), + password, + }); + router.replace('/(tabs)'); + } catch { + // Error is handled by the store + } + }; + + // Navigate to login + const handleLogin = () => { + clearError(); + router.push('/(auth)/login'); + }; + + const passwordStrength = getPasswordStrength(password); + + return ( + + + {/* Header */} + + Create Account + Join our community today + + + {/* Form */} + + {/* Name Input */} + + Name + { + setName(text); + if (validationErrors.name) { + setValidationErrors((prev) => ({ ...prev, name: undefined })); + } + }} + autoCapitalize="words" + autoCorrect={false} + autoComplete="name" + editable={!isLoading} + /> + {validationErrors.name && ( + {validationErrors.name} + )} + + + {/* Email Input */} + + Email + { + setEmail(text); + if (validationErrors.email) { + setValidationErrors((prev) => ({ ...prev, email: undefined })); + } + }} + keyboardType="email-address" + autoCapitalize="none" + autoCorrect={false} + autoComplete="email" + editable={!isLoading} + /> + {validationErrors.email && ( + {validationErrors.email} + )} + + + {/* Password Input */} + + Password + + { + setPassword(text); + if (validationErrors.password) { + setValidationErrors((prev) => ({ ...prev, password: undefined })); + } + }} + secureTextEntry={!showPassword} + autoCapitalize="none" + autoCorrect={false} + autoComplete="password-new" + editable={!isLoading} + /> + setShowPassword(!showPassword)} + disabled={isLoading} + > + + {showPassword ? 'Hide' : 'Show'} + + + + {validationErrors.password && ( + {validationErrors.password} + )} + {/* Password Strength Indicator */} + {password.length > 0 && ( + + + {[1, 2, 3, 4].map((level) => ( + + ))} + + + {passwordStrength.text} + + + )} + + + {/* Confirm Password Input */} + + Confirm Password + { + setConfirmPassword(text); + if (validationErrors.confirmPassword) { + setValidationErrors((prev) => ({ + ...prev, + confirmPassword: undefined, + })); + } + }} + secureTextEntry={!showPassword} + autoCapitalize="none" + autoCorrect={false} + autoComplete="password-new" + editable={!isLoading} + /> + {validationErrors.confirmPassword && ( + {validationErrors.confirmPassword} + )} + + + {/* API Error */} + {error && ( + + {error} + + )} + + {/* Create Account Button */} + + {isLoading ? ( + + ) : ( + Create Account + )} + + + {/* Terms */} + + By creating an account, you agree to our{' '} + Terms of Service and{' '} + Privacy Policy + + + {/* Sign In Link */} + + Already have an account? + + Sign In + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#FFFFFF', + }, + scrollContent: { + flexGrow: 1, + paddingHorizontal: 24, + paddingTop: 60, + paddingBottom: 40, + }, + header: { + marginBottom: 32, + }, + title: { + fontSize: 32, + fontWeight: 'bold', + color: '#333333', + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + color: '#666666', + }, + form: { + flex: 1, + }, + inputContainer: { + marginBottom: 20, + }, + label: { + fontSize: 14, + fontWeight: '600', + color: '#333333', + marginBottom: 8, + }, + input: { + height: 50, + borderWidth: 1, + borderColor: '#E0E0E0', + borderRadius: 8, + paddingHorizontal: 16, + fontSize: 16, + color: '#333333', + backgroundColor: '#FAFAFA', + }, + inputError: { + borderColor: '#FF3B30', + }, + passwordContainer: { + position: 'relative', + }, + passwordInput: { + paddingRight: 60, + }, + showPasswordButton: { + position: 'absolute', + right: 16, + top: 0, + bottom: 0, + justifyContent: 'center', + }, + showPasswordText: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, + errorText: { + fontSize: 12, + color: '#FF3B30', + marginTop: 4, + }, + strengthContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 8, + }, + strengthBars: { + flexDirection: 'row', + flex: 1, + gap: 4, + }, + strengthBar: { + flex: 1, + height: 4, + borderRadius: 2, + }, + strengthText: { + fontSize: 12, + fontWeight: '600', + marginLeft: 8, + minWidth: 50, + }, + apiError: { + backgroundColor: '#FFF0F0', + borderRadius: 8, + padding: 12, + marginBottom: 16, + borderWidth: 1, + borderColor: '#FF3B30', + }, + apiErrorText: { + fontSize: 14, + color: '#FF3B30', + textAlign: 'center', + }, + button: { + height: 50, + backgroundColor: '#007AFF', + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + marginBottom: 16, + }, + buttonDisabled: { + backgroundColor: '#B0B0B0', + }, + buttonText: { + fontSize: 16, + fontWeight: '600', + color: '#FFFFFF', + }, + termsText: { + fontSize: 12, + color: '#666666', + textAlign: 'center', + marginBottom: 24, + lineHeight: 18, + }, + termsLink: { + color: '#007AFF', + fontWeight: '600', + }, + loginContainer: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + loginText: { + fontSize: 14, + color: '#666666', + }, + loginLink: { + fontSize: 14, + color: '#007AFF', + fontWeight: '600', + }, +}); diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx index 700577d..1c9a26c 100644 --- a/mobile/app/(tabs)/profile.tsx +++ b/mobile/app/(tabs)/profile.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { View, ScrollView, @@ -7,8 +7,11 @@ import { SafeAreaView, TouchableOpacity, Alert, + ActivityIndicator, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; +import { useRouter } from 'expo-router'; +import { useAuthStore } from '../lib/auth-store'; const styles = StyleSheet.create({ container: { @@ -111,21 +114,38 @@ const styles = StyleSheet.create({ }); export default function ProfileScreen() { - const [isLoggedIn, setIsLoggedIn] = useState(false); + const router = useRouter(); + const { user, isAuthenticated, isLoading, logout } = useAuthStore(); const handleSignOut = () => { Alert.alert('Sign Out', 'Are you sure you want to sign out?', [ - { text: 'Cancel', onPress: () => {} }, + { text: 'Cancel', style: 'cancel' }, { text: 'Sign Out', - onPress: () => { - setIsLoggedIn(false); + style: 'destructive', + onPress: async () => { + await logout(); + router.replace('/(auth)/login'); }, }, ]); }; - if (!isLoggedIn) { + const handleSignIn = () => { + router.push('/(auth)/login'); + }; + + // Get user initials for avatar + const getInitials = () => { + if (!user?.name) return '?'; + const names = user.name.split(' '); + if (names.length >= 2) { + return `${names[0][0]}${names[1][0]}`.toUpperCase(); + } + return names[0][0].toUpperCase(); + }; + + if (!isAuthenticated) { return ( @@ -139,7 +159,7 @@ export default function ProfileScreen() { setIsLoggedIn(true)} + onPress={handleSignIn} > @@ -182,10 +202,10 @@ export default function ProfileScreen() { - 👤 + {getInitials()} - John Doe - Active Member + {user?.name || 'User'} + {user?.email || 'Active Member'} diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 7fc7d85..7b8aa7e 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -1,16 +1,57 @@ -import { Stack } from 'expo-router'; +import { Stack, useRouter, useSegments } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { useEffect } from 'react'; +import { View, ActivityIndicator, StyleSheet } from 'react-native'; +import { useAuthStore } from './lib/auth-store'; export const unstable_settings = { initialRouteName: '(tabs)', }; -export default function RootLayout() { +// Auth protection hook +function useProtectedRoute() { + const { isAuthenticated, isInitialized } = useAuthStore(); + const segments = useSegments(); + const router = useRouter(); + useEffect(() => { - // Setup any global configuration here + if (!isInitialized) { + return; + } + + const inAuthGroup = segments[0] === '(auth)'; + + if (!isAuthenticated && !inAuthGroup) { + // Redirect to login if not authenticated and not in auth group + router.replace('/(auth)/login'); + } else if (isAuthenticated && inAuthGroup) { + // Redirect to tabs if authenticated and in auth group + router.replace('/(tabs)'); + } + }, [isAuthenticated, isInitialized, segments]); +} + +export default function RootLayout() { + const { initialize, isInitialized } = useAuthStore(); + + // Initialize auth state on app start + useEffect(() => { + initialize(); }, []); + // Use protected route hook + useProtectedRoute(); + + // Show loading screen while initializing + if (!isInitialized) { + return ( + + + + + ); + } + return ( <> @@ -21,3 +62,12 @@ export default function RootLayout() { ); } + +const styles = StyleSheet.create({ + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#FFFFFF', + }, +}); diff --git a/mobile/app/lib/api.ts b/mobile/app/lib/api.ts new file mode 100644 index 0000000..a80b07a --- /dev/null +++ b/mobile/app/lib/api.ts @@ -0,0 +1,284 @@ +import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { + LoginRequest, + SignupRequest, + ForgotPasswordRequest, + ResetPasswordRequest, + AuthResponse, + User, + Resource, + ResourceCategory, + CommunitySpace, + UserPreferences, + ApiError, +} from './types'; + +// Storage keys +const TOKEN_KEY = 'auth_token'; +const REFRESH_TOKEN_KEY = 'refresh_token'; + +// API base URL - update this for your environment +const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.morethanadiagnosis.org/api/v1'; + +// Create axios instance +const api: AxiosInstance = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor - add auth token to requests +api.interceptors.request.use( + async (config: InternalAxiosRequestConfig) => { + const token = await AsyncStorage.getItem(TOKEN_KEY); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor - handle token refresh +api.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config; + + // If 401 and we haven't tried to refresh yet + if (error.response?.status === 401 && originalRequest && !originalRequest._retry) { + originalRequest._retry = true; + + try { + const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_KEY); + if (refreshToken) { + const response = await axios.post(`${API_BASE_URL}/auth/refresh`, { + refreshToken, + }); + + const { accessToken, refreshToken: newRefreshToken } = response.data; + + // Store new tokens + await AsyncStorage.setItem(TOKEN_KEY, accessToken); + await AsyncStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken); + + // Retry original request with new token + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${accessToken}`; + } + return api(originalRequest); + } + } catch (refreshError) { + // Refresh failed - clear tokens and redirect to login + await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]); + // The auth store should handle navigation + } + } + + return Promise.reject(error); + } +); + +// Extend AxiosRequestConfig to include _retry property +declare module 'axios' { + export interface InternalAxiosRequestConfig { + _retry?: boolean; + } +} + +// ========================================== +// Authentication API +// ========================================== + +export const authApi = { + login: async (data: LoginRequest): Promise => { + const response = await api.post('/auth/login', data); + const { accessToken, refreshToken } = response.data; + + // Store tokens + await AsyncStorage.setItem(TOKEN_KEY, accessToken); + await AsyncStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); + + return response.data; + }, + + signup: async (data: SignupRequest): Promise => { + const response = await api.post('/auth/signup', data); + const { accessToken, refreshToken } = response.data; + + // Store tokens + await AsyncStorage.setItem(TOKEN_KEY, accessToken); + await AsyncStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); + + return response.data; + }, + + forgotPassword: async (data: ForgotPasswordRequest): Promise<{ message: string }> => { + const response = await api.post<{ message: string }>('/auth/forgot-password', data); + return response.data; + }, + + resetPassword: async (data: ResetPasswordRequest): Promise<{ message: string }> => { + const response = await api.post<{ message: string }>('/auth/reset-password', data); + return response.data; + }, + + logout: async (): Promise => { + try { + await api.post('/auth/logout'); + } catch { + // Ignore errors on logout + } finally { + await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]); + } + }, + + refreshToken: async (): Promise => { + const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_KEY); + if (!refreshToken) { + throw new Error('No refresh token available'); + } + + const response = await api.post('/auth/refresh', { refreshToken }); + const { accessToken, refreshToken: newRefreshToken } = response.data; + + await AsyncStorage.setItem(TOKEN_KEY, accessToken); + await AsyncStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken); + + return response.data; + }, + + getStoredToken: async (): Promise => { + return AsyncStorage.getItem(TOKEN_KEY); + }, +}; + +// ========================================== +// User API +// ========================================== + +export const userApi = { + getMe: async (): Promise => { + const response = await api.get('/user/me'); + return response.data; + }, + + updateMe: async (data: Partial): Promise => { + const response = await api.put('/user/me', data); + return response.data; + }, + + getPreferences: async (): Promise => { + const response = await api.get('/user/preferences'); + return response.data; + }, + + updatePreferences: async (data: Partial): Promise => { + const response = await api.put('/user/preferences', data); + return response.data; + }, +}; + +// ========================================== +// Resources API +// ========================================== + +export const resourcesApi = { + getCategories: async (): Promise => { + const response = await api.get('/resources/categories'); + return response.data; + }, + + getAll: async (): Promise => { + const response = await api.get('/resources'); + return response.data; + }, + + getById: async (id: string): Promise => { + const response = await api.get(`/resources/${id}`); + return response.data; + }, + + getByCategory: async (categoryId: string): Promise => { + const response = await api.get(`/resources/category/${categoryId}`); + return response.data; + }, +}; + +// ========================================== +// Community API +// ========================================== + +export const communityApi = { + getSpaces: async (): Promise => { + const response = await api.get('/community'); + return response.data; + }, + + getSpaceById: async (id: string): Promise => { + const response = await api.get(`/community/${id}`); + return response.data; + }, + + joinSpace: async (id: string): Promise<{ message: string }> => { + const response = await api.post<{ message: string }>(`/community/${id}/join`); + return response.data; + }, + + leaveSpace: async (id: string): Promise<{ message: string }> => { + const response = await api.post<{ message: string }>(`/community/${id}/leave`); + return response.data; + }, +}; + +// ========================================== +// Helper functions +// ========================================== + +export const handleApiError = (error: unknown): string => { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + + // Network error + if (!axiosError.response) { + return 'Network error. Please check your connection.'; + } + + // API error response + const apiError = axiosError.response.data; + if (apiError?.message) { + return apiError.message; + } + + // HTTP status errors + switch (axiosError.response.status) { + case 400: + return 'Invalid request. Please check your input.'; + case 401: + return 'Invalid credentials. Please try again.'; + case 403: + return 'You do not have permission to perform this action.'; + case 404: + return 'Resource not found.'; + case 422: + return 'Validation error. Please check your input.'; + case 500: + return 'Server error. Please try again later.'; + default: + return 'An unexpected error occurred.'; + } + } + + if (error instanceof Error) { + return error.message; + } + + return 'An unexpected error occurred.'; +}; + +export default api; diff --git a/mobile/app/lib/auth-store.ts b/mobile/app/lib/auth-store.ts new file mode 100644 index 0000000..02ab7b7 --- /dev/null +++ b/mobile/app/lib/auth-store.ts @@ -0,0 +1,220 @@ +import { create } from 'zustand'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { authApi, userApi, handleApiError } from './api'; +import { User, LoginRequest, SignupRequest } from './types'; + +// Storage keys +const USER_KEY = 'auth_user'; + +interface AuthState { + // State + user: User | null; + isAuthenticated: boolean; + isLoading: boolean; + isInitialized: boolean; + error: string | null; + + // Actions + initialize: () => Promise; + login: (data: LoginRequest) => Promise; + signup: (data: SignupRequest) => Promise; + logout: () => Promise; + clearError: () => void; + setUser: (user: User) => void; + updateUser: (data: Partial) => Promise; +} + +export const useAuthStore = create((set, get) => ({ + // Initial state + user: null, + isAuthenticated: false, + isLoading: false, + isInitialized: false, + error: null, + + // Initialize auth state on app start + initialize: async () => { + try { + set({ isLoading: true, error: null }); + + // Check for stored token + const token = await authApi.getStoredToken(); + + if (token) { + // Try to fetch current user + try { + const user = await userApi.getMe(); + await AsyncStorage.setItem(USER_KEY, JSON.stringify(user)); + + set({ + user, + isAuthenticated: true, + isInitialized: true, + isLoading: false, + }); + } catch { + // Token invalid or expired - try to refresh + try { + await authApi.refreshToken(); + const user = await userApi.getMe(); + await AsyncStorage.setItem(USER_KEY, JSON.stringify(user)); + + set({ + user, + isAuthenticated: true, + isInitialized: true, + isLoading: false, + }); + } catch { + // Refresh failed - clear auth state + await authApi.logout(); + await AsyncStorage.removeItem(USER_KEY); + + set({ + user: null, + isAuthenticated: false, + isInitialized: true, + isLoading: false, + }); + } + } + } else { + // No token - check for cached user data for offline display + const cachedUser = await AsyncStorage.getItem(USER_KEY); + + set({ + user: cachedUser ? JSON.parse(cachedUser) : null, + isAuthenticated: false, + isInitialized: true, + isLoading: false, + }); + } + } catch (error) { + set({ + user: null, + isAuthenticated: false, + isInitialized: true, + isLoading: false, + error: handleApiError(error), + }); + } + }, + + // Login + login: async (data: LoginRequest) => { + try { + set({ isLoading: true, error: null }); + + const response = await authApi.login(data); + const { user } = response; + + // Cache user data + await AsyncStorage.setItem(USER_KEY, JSON.stringify(user)); + + set({ + user, + isAuthenticated: true, + isLoading: false, + error: null, + }); + } catch (error) { + set({ + isLoading: false, + error: handleApiError(error), + }); + throw error; + } + }, + + // Signup + signup: async (data: SignupRequest) => { + try { + set({ isLoading: true, error: null }); + + const response = await authApi.signup(data); + const { user } = response; + + // Cache user data + await AsyncStorage.setItem(USER_KEY, JSON.stringify(user)); + + set({ + user, + isAuthenticated: true, + isLoading: false, + error: null, + }); + } catch (error) { + set({ + isLoading: false, + error: handleApiError(error), + }); + throw error; + } + }, + + // Logout + logout: async () => { + try { + set({ isLoading: true }); + + await authApi.logout(); + await AsyncStorage.removeItem(USER_KEY); + + set({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + }); + } catch (error) { + // Still clear local state even if API call fails + await AsyncStorage.removeItem(USER_KEY); + + set({ + user: null, + isAuthenticated: false, + isLoading: false, + error: null, + }); + } + }, + + // Clear error + clearError: () => { + set({ error: null }); + }, + + // Set user (for updates from other parts of the app) + setUser: (user: User) => { + set({ user }); + AsyncStorage.setItem(USER_KEY, JSON.stringify(user)); + }, + + // Update user profile + updateUser: async (data: Partial) => { + try { + set({ isLoading: true, error: null }); + + const updatedUser = await userApi.updateMe(data); + await AsyncStorage.setItem(USER_KEY, JSON.stringify(updatedUser)); + + set({ + user: updatedUser, + isLoading: false, + }); + } catch (error) { + set({ + isLoading: false, + error: handleApiError(error), + }); + throw error; + } + }, +})); + +// Selector hooks for specific state slices +export const useUser = () => useAuthStore((state) => state.user); +export const useIsAuthenticated = () => useAuthStore((state) => state.isAuthenticated); +export const useIsLoading = () => useAuthStore((state) => state.isLoading); +export const useAuthError = () => useAuthStore((state) => state.error); +export const useIsInitialized = () => useAuthStore((state) => state.isInitialized); diff --git a/mobile/app/lib/types.ts b/mobile/app/lib/types.ts new file mode 100644 index 0000000..a76e796 --- /dev/null +++ b/mobile/app/lib/types.ts @@ -0,0 +1,93 @@ +// User types +export interface User { + id: string; + email: string; + name: string; + displayName?: string; + avatarUrl?: string; + createdAt: string; + updatedAt: string; +} + +// Auth types +export interface LoginRequest { + email: string; + password: string; +} + +export interface SignupRequest { + email: string; + password: string; + name: string; +} + +export interface ForgotPasswordRequest { + email: string; +} + +export interface ResetPasswordRequest { + token: string; + newPassword: string; +} + +export interface AuthResponse { + user: User; + accessToken: string; + refreshToken: string; + expiresIn: number; +} + +export interface RefreshTokenRequest { + refreshToken: string; +} + +// Resource types +export interface Resource { + id: string; + title: string; + description: string; + category: string; + url?: string; + imageUrl?: string; + createdAt: string; +} + +export interface ResourceCategory { + id: string; + name: string; + description: string; + icon: string; + resourceCount: number; +} + +// Community types +export interface CommunitySpace { + id: string; + name: string; + description: string; + memberCount: number; + isPrivate: boolean; + imageUrl?: string; +} + +// User preferences +export interface UserPreferences { + notifications: boolean; + emailUpdates: boolean; + darkMode: boolean; + language: string; +} + +// API Error response +export interface ApiError { + message: string; + code?: string; + details?: Record; +} + +// Generic API response wrapper +export interface ApiResponse { + data: T; + message?: string; + success: boolean; +} diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 52d180b..66a342a 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -8,12 +8,14 @@ "name": "morethanadiagnosis-mobile", "version": "0.1.0", "dependencies": { + "@react-native-async-storage/async-storage": "^2.2.0", "@tanstack/react-query": "^5.25.0", "axios": "^1.6.0", "expo": "^51.0.0", "expo-constants": "~15.4.0", "expo-linking": "~6.0.0", "expo-router": "^3.4.0", + "expo-secure-store": "^15.0.7", "expo-splash-screen": "^31.0.11", "expo-status-bar": "~1.12.1", "react": "^18.2.0", @@ -517,6 +519,90 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-proposal-async-generator-functions": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", @@ -691,6 +777,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -760,6 +859,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", @@ -862,6 +993,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", @@ -877,6 +1025,24 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", @@ -894,6 +1060,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz", @@ -909,6 +1091,40 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, "node_modules/@babel/plugin-transform-classes": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", @@ -961,6 +1177,105 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz", + "integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", @@ -992,6 +1307,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", @@ -1009,6 +1341,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", @@ -1024,6 +1372,55 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz", + "integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", @@ -1040,6 +1437,42 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz", + "integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", @@ -1056,6 +1489,54 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", @@ -1075,6 +1556,56 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz", + "integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", @@ -1123,6 +1654,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", @@ -1218,6 +1765,55 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-runtime": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz", @@ -1293,6 +1889,38 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", @@ -1312,6 +1940,39 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", @@ -1328,6 +1989,118 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz", + "integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.4", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/preset-flow": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.27.1.tgz", @@ -1345,6 +2118,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/preset-react": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", @@ -2913,6 +3701,18 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native-community/cli": { "version": "12.3.7", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-12.3.7.tgz", @@ -4850,7 +5650,7 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -4863,7 +5663,7 @@ "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -6581,7 +7381,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/dag-map": { @@ -7380,7 +8180,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -8011,6 +8810,15 @@ "expo": "*" } }, + "node_modules/expo-secure-store": { + "version": "15.0.7", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.7.tgz", + "integrity": "sha512-9q7+G1Zxr5P6J5NRIlm86KulvmYwc6UnQlYPjQLDu1drDnerz6AT6l884dPu29HgtDTn4rR0heYeeGFhMKM7/Q==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-splash-screen": { "version": "31.0.11", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.11.tgz", @@ -9711,6 +10519,15 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -11150,6 +11967,18 @@ "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==", "license": "BSD-2-Clause" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/mobile/package.json b/mobile/package.json index f5b4311..0e4f38a 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -16,12 +16,14 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@react-native-async-storage/async-storage": "^2.2.0", "@tanstack/react-query": "^5.25.0", "axios": "^1.6.0", "expo": "^51.0.0", "expo-constants": "~15.4.0", "expo-linking": "~6.0.0", "expo-router": "^3.4.0", + "expo-secure-store": "^15.0.7", "expo-splash-screen": "^31.0.11", "expo-status-bar": "~1.12.1", "react": "^18.2.0",