1251 lines
33 KiB
TypeScript
1251 lines
33 KiB
TypeScript
/*
|
|
* File: ResetPasswordScreen.tsx
|
|
* Description: Password reset screen for onboarding flow with document upload support
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
ScrollView,
|
|
KeyboardAvoidingView,
|
|
Platform,
|
|
Alert,
|
|
Image,
|
|
PermissionsAndroid,
|
|
} from 'react-native';
|
|
import {
|
|
launchImageLibrary,
|
|
launchCamera,
|
|
ImagePickerResponse,
|
|
MediaType,
|
|
} from 'react-native-image-picker';
|
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
|
import { updateOnboarded, logout } from '../redux/authSlice';
|
|
import { authAPI } from '../services/authAPI';
|
|
import { showError, showSuccess } from '../../../shared/utils/toast';
|
|
import { validateFileType, validateFileSize, prepareFileForUpload } from '../../../shared/utils/fileUpload';
|
|
import { theme } from '../../../theme/theme';
|
|
import Icon from 'react-native-vector-icons/Feather';
|
|
import { AuthNavigationProp } from '../navigation/navigationTypes';
|
|
|
|
/**
|
|
* ResetPasswordScreenProps Interface
|
|
*
|
|
* Purpose: Defines the props required by the ResetPasswordScreen component
|
|
*
|
|
* Props:
|
|
* - navigation: React Navigation object for screen navigation (optional when used in root stack)
|
|
*/
|
|
interface ResetPasswordScreenProps {
|
|
navigation?: AuthNavigationProp;
|
|
}
|
|
|
|
/**
|
|
* PasswordRule Interface
|
|
*
|
|
* Purpose: Defines the structure for password validation rules
|
|
*/
|
|
interface PasswordRule {
|
|
id: string;
|
|
label: string;
|
|
validator: (password: string) => boolean;
|
|
isValid: boolean;
|
|
}
|
|
|
|
/**
|
|
* ImageData Interface
|
|
*
|
|
* Purpose: Defines the structure for image data
|
|
*/
|
|
interface ImageData {
|
|
uri: string;
|
|
name: string;
|
|
type: string;
|
|
size?: number;
|
|
}
|
|
|
|
/**
|
|
* OnboardingStep Type
|
|
*
|
|
* Purpose: Defines the different onboarding steps
|
|
*/
|
|
type OnboardingStep = 'document' | 'password';
|
|
|
|
/**
|
|
* ResetPasswordScreen Component
|
|
*
|
|
* Purpose: Password reset screen for users who need to set their initial password
|
|
*
|
|
* Features:
|
|
* 1. Two flows based on platform:
|
|
* - platform = 'web': Document upload first, then password reset
|
|
* - platform = 'app': Password reset only
|
|
* 2. Document upload with camera/gallery selection
|
|
* 3. Password and confirm password input fields
|
|
* 4. Real-time password validation with visual feedback
|
|
* 5. Password visibility toggles
|
|
* 6. Password strength requirements display
|
|
* 7. Integration with Redux for onboarding status
|
|
* 8. API integration for document upload and password change
|
|
*
|
|
* Password Requirements:
|
|
* - At least 8 characters
|
|
* - One uppercase letter
|
|
* - One lowercase letter
|
|
* - One number
|
|
* - One special character
|
|
* - Passwords must match
|
|
*/
|
|
export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|
navigation,
|
|
}) => {
|
|
// ============================================================================
|
|
// STATE MANAGEMENT
|
|
// ============================================================================
|
|
|
|
// Form input states
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [isPasswordVisible, setPasswordVisible] = useState(false);
|
|
const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// Document upload states
|
|
const [selectedImage, setSelectedImage] = useState<ImageData | null>(null);
|
|
const [documentUploaded, setDocumentUploaded] = useState(false);
|
|
|
|
// Current step state
|
|
const [currentStep, setCurrentStep] = useState<OnboardingStep>('document');
|
|
|
|
// Redux state
|
|
const dispatch = useAppDispatch();
|
|
const user = useAppSelector((state) => state.auth.user);
|
|
|
|
// Password validation rules
|
|
const [passwordRules, setPasswordRules] = useState<PasswordRule[]>([
|
|
{
|
|
id: 'length',
|
|
label: 'At least 8 characters',
|
|
validator: (pwd: string) => pwd.length >= 8,
|
|
isValid: false,
|
|
},
|
|
{
|
|
id: 'uppercase',
|
|
label: 'One uppercase letter',
|
|
validator: (pwd: string) => /[A-Z]/.test(pwd),
|
|
isValid: false,
|
|
},
|
|
{
|
|
id: 'lowercase',
|
|
label: 'One lowercase letter',
|
|
validator: (pwd: string) => /[a-z]/.test(pwd),
|
|
isValid: false,
|
|
},
|
|
{
|
|
id: 'number',
|
|
label: 'One number',
|
|
validator: (pwd: string) => /\d/.test(pwd),
|
|
isValid: false,
|
|
},
|
|
{
|
|
id: 'special',
|
|
label: 'One special character',
|
|
validator: (pwd: string) => /[!@#$%^&*(),.?":{}|<>]/.test(pwd),
|
|
isValid: false,
|
|
},
|
|
{
|
|
id: 'match',
|
|
label: 'Passwords match',
|
|
validator: (pwd: string) => pwd.length > 0 && confirmPassword.length > 0 && pwd === confirmPassword,
|
|
isValid: false,
|
|
},
|
|
]);
|
|
|
|
// ============================================================================
|
|
// EFFECTS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* useEffect for password validation
|
|
*
|
|
* Purpose: Update password rules when password or confirm password changes
|
|
*/
|
|
useEffect(() => {
|
|
updatePasswordRules(password);
|
|
}, [password, confirmPassword]);
|
|
|
|
/**
|
|
* useEffect for determining initial step
|
|
*
|
|
* Purpose: Set the initial step based on user signup platform
|
|
*/
|
|
useEffect(() => {
|
|
if (user?.platform === 'app') {
|
|
setCurrentStep('password');
|
|
} else {
|
|
setCurrentStep('document');
|
|
}
|
|
}, [user?.platform]);
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* validatePassword Function
|
|
*
|
|
* Purpose: Check if all password requirements are met
|
|
*
|
|
* @param pwd - Password to validate
|
|
* @returns boolean indicating if password meets all requirements
|
|
*/
|
|
const validatePassword = (pwd: string): boolean => {
|
|
return passwordRules.every(rule => rule.isValid);
|
|
};
|
|
|
|
/**
|
|
* updatePasswordRules Function
|
|
*
|
|
* Purpose: Update password validation rules based on current password
|
|
*
|
|
* @param pwd - Current password value
|
|
*/
|
|
const updatePasswordRules = (pwd: string) => {
|
|
setPasswordRules(prevRules =>
|
|
prevRules.map(rule => {
|
|
if (rule.id === 'match') {
|
|
return {
|
|
...rule,
|
|
isValid: pwd.length > 0 && confirmPassword.length > 0 && pwd === confirmPassword,
|
|
};
|
|
}
|
|
return {
|
|
...rule,
|
|
isValid: rule.validator(pwd),
|
|
};
|
|
})
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Request Camera Permission
|
|
*
|
|
* Purpose: Request camera permission for Android devices
|
|
*
|
|
* @returns Promise<boolean> - Whether permission was granted
|
|
*/
|
|
const requestCameraPermission = async (): Promise<boolean> => {
|
|
if (Platform.OS === 'android') {
|
|
try {
|
|
const granted = await PermissionsAndroid.request(
|
|
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
{
|
|
title: 'Camera Permission',
|
|
message: 'This app needs camera permission to capture images.',
|
|
buttonNeutral: 'Ask Me Later',
|
|
buttonNegative: 'Cancel',
|
|
buttonPositive: 'OK',
|
|
}
|
|
);
|
|
return granted === PermissionsAndroid.RESULTS.GRANTED;
|
|
} catch (err) {
|
|
console.warn('Camera permission error:', err);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Format File Size
|
|
*
|
|
* Purpose: Convert bytes to human readable format
|
|
*
|
|
* @param bytes - File size in bytes
|
|
* @returns Formatted file size string
|
|
*/
|
|
const formatFileSize = (bytes?: number): string => {
|
|
if (!bytes) return '';
|
|
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
if (bytes === 0) return '0 Bytes';
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
|
};
|
|
|
|
/**
|
|
* Get File Type Display
|
|
*
|
|
* Purpose: Get display name for file type
|
|
*
|
|
* @param type - MIME type
|
|
* @returns Display name for file type
|
|
*/
|
|
const getFileTypeDisplay = (type: string): string => {
|
|
if (type.includes('jpeg') || type.includes('jpg')) return 'JPEG';
|
|
if (type.includes('png')) return 'PNG';
|
|
if (type.includes('gif')) return 'GIF';
|
|
if (type.includes('webp')) return 'WebP';
|
|
return 'Image';
|
|
};
|
|
|
|
// ============================================================================
|
|
// DOCUMENT UPLOAD HANDLERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Handle Image Picker Selection
|
|
*
|
|
* Purpose: Show options for camera or gallery selection
|
|
*/
|
|
const handleImagePicker = () => {
|
|
Alert.alert(
|
|
'Select Image',
|
|
'Choose how you want to upload your document',
|
|
[
|
|
{
|
|
text: 'Camera',
|
|
onPress: () => handleCameraCapture(),
|
|
},
|
|
{
|
|
text: 'Gallery',
|
|
onPress: () => handleGalleryPicker(),
|
|
},
|
|
{
|
|
text: 'Cancel',
|
|
style: 'cancel',
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Handle Camera Capture
|
|
*
|
|
* Purpose: Launch camera to capture image
|
|
*/
|
|
const handleCameraCapture = async () => {
|
|
const hasPermission = await requestCameraPermission();
|
|
|
|
if (!hasPermission) {
|
|
showError('Permission Error', 'Camera permission is required to capture images.');
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
mediaType: 'photo' as MediaType,
|
|
maxWidth: 2000,
|
|
maxHeight: 2000,
|
|
includeBase64: false,
|
|
};
|
|
|
|
launchCamera(options, (response: ImagePickerResponse) => {
|
|
if (response.didCancel) {
|
|
return;
|
|
}
|
|
|
|
if (response.errorMessage) {
|
|
showError('Camera Error', response.errorMessage);
|
|
return;
|
|
}
|
|
|
|
if (response.assets && response.assets[0]) {
|
|
const asset = response.assets[0];
|
|
const imageData: ImageData = {
|
|
uri: asset.uri!,
|
|
name: asset.fileName || `document_${Date.now()}.jpg`,
|
|
type: asset.type || 'image/jpeg',
|
|
size: asset.fileSize,
|
|
};
|
|
|
|
// Validate file type and size
|
|
if (!validateFileType(imageData)) {
|
|
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
|
|
return;
|
|
}
|
|
|
|
if (!validateFileSize(imageData, 10)) {
|
|
showError('File Too Large', 'Please select an image under 10MB.');
|
|
return;
|
|
}
|
|
|
|
setSelectedImage(imageData);
|
|
showSuccess('Success', 'Document captured successfully!');
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handle Gallery Picker
|
|
*
|
|
* Purpose: Launch image library to select image
|
|
*/
|
|
const handleGalleryPicker = () => {
|
|
const options = {
|
|
mediaType: 'photo' as MediaType,
|
|
maxWidth: 2000,
|
|
maxHeight: 2000,
|
|
includeBase64: false,
|
|
};
|
|
|
|
launchImageLibrary(options, (response: ImagePickerResponse) => {
|
|
if (response.didCancel) {
|
|
return;
|
|
}
|
|
|
|
if (response.errorMessage) {
|
|
showError('Gallery Error', response.errorMessage);
|
|
return;
|
|
}
|
|
|
|
if (response.assets && response.assets[0]) {
|
|
const asset = response.assets[0];
|
|
const imageData: ImageData = {
|
|
uri: asset.uri!,
|
|
name: asset.fileName || `document_${Date.now()}.jpg`,
|
|
type: asset.type || 'image/jpeg',
|
|
size: asset.fileSize,
|
|
};
|
|
|
|
// Validate file type and size
|
|
if (!validateFileType(imageData)) {
|
|
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
|
|
return;
|
|
}
|
|
|
|
if (!validateFileSize(imageData, 10)) {
|
|
showError('File Too Large', 'Please select an image under 10MB.');
|
|
return;
|
|
}
|
|
|
|
setSelectedImage(imageData);
|
|
showSuccess('Success', 'Document selected from gallery!');
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handle Remove Image
|
|
*
|
|
* Purpose: Remove selected image
|
|
*/
|
|
const handleRemoveImage = () => {
|
|
setSelectedImage(null);
|
|
setDocumentUploaded(false);
|
|
};
|
|
|
|
/**
|
|
* Handle Document Upload
|
|
*
|
|
* Purpose: Upload document to server
|
|
*/
|
|
const handleDocumentUpload = async () => {
|
|
if (!selectedImage) {
|
|
showError('Validation Error', 'Please upload a document to continue.');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
|
|
// Prepare file with proper structure using utility function
|
|
const preparedFile = prepareFileForUpload(selectedImage, 'id_photo');
|
|
|
|
formData.append('id_photo', preparedFile as any);
|
|
|
|
const response: any = await authAPI.uploadDocument(formData, user?.access_token);
|
|
|
|
|
|
if (response.data && response.data.message) {
|
|
if (response.data.success) {
|
|
showSuccess(response.data.message);
|
|
setDocumentUploaded(true);
|
|
// Move to password step after successful upload
|
|
setCurrentStep('password');
|
|
} else {
|
|
showError(response.data.message);
|
|
}
|
|
} else {
|
|
showError('Error while uploading document');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Document upload error:', error);
|
|
showError('Failed to upload document. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// EVENT HANDLERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* handlePasswordChange Function
|
|
*
|
|
* Purpose: Handle password input changes
|
|
*
|
|
* @param pwd - New password value
|
|
*/
|
|
const handlePasswordChange = (pwd: string) => {
|
|
setPassword(pwd);
|
|
};
|
|
|
|
/**
|
|
* handleConfirmPasswordChange Function
|
|
*
|
|
* Purpose: Handle confirm password input changes
|
|
*
|
|
* @param pwd - New confirm password value
|
|
*/
|
|
const handleConfirmPasswordChange = (pwd: string) => {
|
|
setConfirmPassword(pwd);
|
|
};
|
|
|
|
/**
|
|
* handleReset Function
|
|
*
|
|
* Purpose: Handle password reset submission
|
|
*
|
|
* Flow:
|
|
* 1. Validate required fields
|
|
* 2. Validate password requirements
|
|
* 3. Check password match
|
|
* 4. Call API to change password
|
|
* 5. Update onboarding status on success
|
|
*/
|
|
const handleReset = async () => {
|
|
// Validate required fields
|
|
if (!password.trim() || !confirmPassword.trim()) {
|
|
Alert.alert('Validation Error', 'Both password fields are required.');
|
|
return;
|
|
}
|
|
|
|
// Validate password requirements
|
|
if (!validatePassword(password)) {
|
|
Alert.alert('Validation Error', 'Please meet all password requirements.');
|
|
return;
|
|
}
|
|
|
|
// Check password match
|
|
if (password !== confirmPassword) {
|
|
Alert.alert('Validation Error', 'Passwords do not match.');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
// Call API to change password
|
|
const response: any = await authAPI.changepassword({
|
|
password,
|
|
token: user?.access_token,
|
|
});
|
|
|
|
|
|
|
|
if (response.data && response.data.message) {
|
|
if (response.data.success) {
|
|
showSuccess(response.data.message);
|
|
|
|
// Update onboarding status
|
|
dispatch(updateOnboarded(true));
|
|
|
|
// Navigate to main app
|
|
// The app will automatically navigate to MainTabNavigator due to Redux state change
|
|
} else {
|
|
showError(response.data.message);
|
|
}
|
|
} else {
|
|
showError('Error while changing password');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Password reset error:', error);
|
|
showError('Failed to reset password. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* handleBack Function
|
|
*
|
|
* Purpose: Handle back navigation - logs out user since they can't go back to login
|
|
*/
|
|
const handleBack = () => {
|
|
Alert.alert(
|
|
'Sign Out',
|
|
'Are you sure you want to sign out? You will need to log in again.',
|
|
[
|
|
{
|
|
text: 'Cancel',
|
|
style: 'cancel',
|
|
},
|
|
{
|
|
text: 'Sign Out',
|
|
style: 'destructive',
|
|
onPress: () => {
|
|
// Dispatch logout action
|
|
dispatch(logout());
|
|
},
|
|
},
|
|
]
|
|
);
|
|
};
|
|
|
|
/**
|
|
* renderPasswordRule Function
|
|
*
|
|
* Purpose: Render individual password validation rule
|
|
*
|
|
* @param rule - Password rule to render
|
|
* @returns JSX element for the rule
|
|
*/
|
|
const renderPasswordRule = (rule: PasswordRule) => (
|
|
<View key={rule.id} style={styles.ruleContainer}>
|
|
<View style={[styles.checkbox, rule.isValid && styles.checkboxChecked]}>
|
|
{rule.isValid && (
|
|
<Icon name="check" size={12} color={theme.colors.background} />
|
|
)}
|
|
</View>
|
|
<Text style={[styles.ruleText, rule.isValid && styles.ruleTextValid]}>
|
|
{rule.label}
|
|
</Text>
|
|
</View>
|
|
);
|
|
|
|
/**
|
|
* renderDocumentUploadStep Function
|
|
*
|
|
* Purpose: Render document upload step
|
|
*
|
|
* @returns JSX element for document upload step
|
|
*/
|
|
const renderDocumentUploadStep = () => (
|
|
<>
|
|
{/* Icon */}
|
|
<View style={styles.iconContainer}>
|
|
<Icon name="file-text" size={64} color={theme.colors.primary} />
|
|
</View>
|
|
|
|
{/* Title and Subtitle */}
|
|
<Text style={styles.title}>Upload Your ID Document</Text>
|
|
<Text style={styles.subtitle}>
|
|
Please upload a clear photo of your hospital-issued ID for verification
|
|
</Text>
|
|
|
|
{/* Document Upload Area */}
|
|
<TouchableOpacity
|
|
style={styles.uploadContainer}
|
|
onPress={handleImagePicker}
|
|
disabled={loading}
|
|
>
|
|
<View style={styles.uploadContent}>
|
|
{selectedImage ? (
|
|
<View style={styles.imagePreviewContainer}>
|
|
<Image source={{ uri: selectedImage.uri }} style={styles.imagePreview} />
|
|
<TouchableOpacity
|
|
style={styles.imageOverlay}
|
|
onPress={handleRemoveImage}
|
|
>
|
|
<Icon name="x" size={20} color={theme.colors.background} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.uploadedText}>Document Selected</Text>
|
|
<Text style={styles.fileName}>{selectedImage.name}</Text>
|
|
<View style={styles.fileDetails}>
|
|
<Text style={styles.fileType}>{getFileTypeDisplay(selectedImage.type)}</Text>
|
|
{selectedImage.size && (
|
|
<Text style={styles.fileSize}> • {formatFileSize(selectedImage.size)}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<Icon name="image" size={48} color={theme.colors.textSecondary} />
|
|
<Text style={styles.uploadText}>Tap to upload document</Text>
|
|
<Text style={styles.uploadSubtext}>JPG, PNG supported</Text>
|
|
</>
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
{selectedImage && (
|
|
<TouchableOpacity
|
|
style={styles.changeButton}
|
|
onPress={handleImagePicker}
|
|
disabled={loading}
|
|
>
|
|
<Text style={styles.changeButtonText}>Change Document</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
|
|
{/* Upload Button */}
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.button,
|
|
styles.uploadButton,
|
|
(!selectedImage || loading) && styles.buttonDisabled,
|
|
]}
|
|
onPress={handleDocumentUpload}
|
|
disabled={!selectedImage || loading}
|
|
>
|
|
{loading ? (
|
|
<View style={styles.loadingContainer}>
|
|
<Text style={styles.buttonText}>Uploading Document...</Text>
|
|
</View>
|
|
) : (
|
|
<Text style={styles.buttonText}>Upload Document</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</>
|
|
);
|
|
|
|
/**
|
|
* renderPasswordResetStep Function
|
|
*
|
|
* Purpose: Render password reset step
|
|
*
|
|
* @returns JSX element for password reset step
|
|
*/
|
|
const renderPasswordResetStep = () => (
|
|
<>
|
|
{/* Icon */}
|
|
<View style={styles.iconContainer}>
|
|
<Icon name="lock" size={64} color={theme.colors.primary} />
|
|
</View>
|
|
|
|
{/* Title and Subtitle */}
|
|
<Text style={styles.title}>Set Your Password</Text>
|
|
<Text style={styles.subtitle}>
|
|
Create a strong password to complete your account setup
|
|
</Text>
|
|
|
|
{/* Password Input */}
|
|
<View style={styles.inputContainer}>
|
|
<Icon name="lock" size={20} color={theme.colors.textSecondary} style={styles.inputIcon} />
|
|
<TextInput
|
|
placeholder="Password"
|
|
value={password}
|
|
onChangeText={handlePasswordChange}
|
|
style={styles.inputField}
|
|
secureTextEntry={!isPasswordVisible}
|
|
placeholderTextColor={theme.colors.textMuted}
|
|
editable={!loading}
|
|
/>
|
|
<TouchableOpacity
|
|
onPress={() => setPasswordVisible(!isPasswordVisible)}
|
|
style={styles.eyeIcon}
|
|
disabled={loading}
|
|
>
|
|
<Icon
|
|
name={isPasswordVisible ? 'eye-off' : 'eye'}
|
|
size={22}
|
|
color={theme.colors.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Confirm Password Input */}
|
|
<View style={styles.inputContainer}>
|
|
<Icon name="lock" size={20} color={theme.colors.textSecondary} style={styles.inputIcon} />
|
|
<TextInput
|
|
placeholder="Confirm Password"
|
|
value={confirmPassword}
|
|
onChangeText={handleConfirmPasswordChange}
|
|
style={styles.inputField}
|
|
secureTextEntry={!isConfirmPasswordVisible}
|
|
placeholderTextColor={theme.colors.textMuted}
|
|
editable={!loading}
|
|
/>
|
|
<TouchableOpacity
|
|
onPress={() => setConfirmPasswordVisible(!isConfirmPasswordVisible)}
|
|
style={styles.eyeIcon}
|
|
disabled={loading}
|
|
>
|
|
<Icon
|
|
name={isConfirmPasswordVisible ? 'eye-off' : 'eye'}
|
|
size={22}
|
|
color={theme.colors.textSecondary}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Password Rules Section */}
|
|
<View style={styles.rulesContainer}>
|
|
<Text style={styles.rulesTitle}>Password Requirements:</Text>
|
|
<View style={styles.rulesGrid}>
|
|
{passwordRules.map(renderPasswordRule)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Reset Button */}
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.button,
|
|
styles.resetButton,
|
|
loading && styles.buttonDisabled,
|
|
]}
|
|
onPress={handleReset}
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<View style={styles.loadingContainer}>
|
|
<Text style={styles.buttonText}>Setting Password...</Text>
|
|
</View>
|
|
) : (
|
|
<Text style={styles.buttonText}>Set Password</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</>
|
|
);
|
|
|
|
// ============================================================================
|
|
// RENDER SECTION
|
|
// ============================================================================
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={styles.container}
|
|
>
|
|
<ScrollView
|
|
style={styles.scrollView}
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<TouchableOpacity onPress={handleBack} style={styles.backButton}>
|
|
<Icon name="log-out" size={24} color={theme.colors.textPrimary} />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>
|
|
{currentStep === 'document' ? 'Upload Document' : 'Set Password'}
|
|
</Text>
|
|
<View style={styles.headerSpacer} />
|
|
</View>
|
|
|
|
{/* Step Indicator */}
|
|
{user?.platform === 'web' && (
|
|
<View style={styles.stepIndicator}>
|
|
<View style={styles.stepContainer}>
|
|
<View style={[styles.stepCircle, currentStep === 'document' && styles.stepCircleActive]}>
|
|
<Text style={[styles.stepNumber, currentStep === 'document' && styles.stepNumberActive]}>1</Text>
|
|
</View>
|
|
<Text style={[styles.stepLabel, currentStep === 'document' && styles.stepLabelActive]}>Document</Text>
|
|
</View>
|
|
<View style={styles.stepLine} />
|
|
<View style={styles.stepContainer}>
|
|
<View style={[styles.stepCircle, currentStep === 'password' && styles.stepCircleActive]}>
|
|
<Text style={[styles.stepNumber, currentStep === 'password' && styles.stepNumberActive]}>2</Text>
|
|
</View>
|
|
<Text style={[styles.stepLabel, currentStep === 'password' && styles.stepLabelActive]}>Password</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Content based on current step */}
|
|
{currentStep === 'document' ? renderDocumentUploadStep() : renderPasswordResetStep()}
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// STYLES SECTION
|
|
// ============================================================================
|
|
|
|
const styles = StyleSheet.create({
|
|
// Main container
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
|
|
// Scroll view
|
|
scrollView: {
|
|
flex: 1,
|
|
},
|
|
|
|
// Scroll content
|
|
scrollContent: {
|
|
flexGrow: 1,
|
|
padding: theme.spacing.lg,
|
|
},
|
|
|
|
// Header section
|
|
header: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: theme.spacing.xl,
|
|
},
|
|
|
|
// Back button
|
|
backButton: {
|
|
padding: theme.spacing.sm,
|
|
borderRadius: theme.borderRadius.medium,
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
},
|
|
|
|
// Header title
|
|
headerTitle: {
|
|
fontSize: theme.typography.fontSize.displaySmall,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
color: theme.colors.textPrimary,
|
|
},
|
|
|
|
// Header spacer
|
|
headerSpacer: {
|
|
width: 40,
|
|
},
|
|
|
|
// Step indicator
|
|
stepIndicator: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: theme.spacing.xl,
|
|
paddingHorizontal: theme.spacing.lg,
|
|
},
|
|
|
|
// Step container
|
|
stepContainer: {
|
|
alignItems: 'center',
|
|
},
|
|
|
|
// Step circle
|
|
stepCircle: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderWidth: 2,
|
|
borderColor: theme.colors.border,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
|
|
// Step circle active
|
|
stepCircleActive: {
|
|
backgroundColor: theme.colors.primary,
|
|
borderColor: theme.colors.primary,
|
|
},
|
|
|
|
// Step number
|
|
stepNumber: {
|
|
fontSize: theme.typography.fontSize.bodyMedium,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.textSecondary,
|
|
},
|
|
|
|
// Step number active
|
|
stepNumberActive: {
|
|
color: theme.colors.background,
|
|
},
|
|
|
|
// Step label
|
|
stepLabel: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textSecondary,
|
|
},
|
|
|
|
// Step label active
|
|
stepLabelActive: {
|
|
color: theme.colors.primary,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
|
|
// Step line
|
|
stepLine: {
|
|
flex: 1,
|
|
height: 2,
|
|
backgroundColor: theme.colors.border,
|
|
marginHorizontal: theme.spacing.md,
|
|
},
|
|
|
|
// Icon container
|
|
iconContainer: {
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.xl,
|
|
},
|
|
|
|
// Title
|
|
title: {
|
|
fontSize: theme.typography.fontSize.displayMedium,
|
|
fontFamily: theme.typography.fontFamily.bold,
|
|
color: theme.colors.textPrimary,
|
|
textAlign: 'center',
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
|
|
// Subtitle
|
|
subtitle: {
|
|
fontSize: theme.typography.fontSize.bodyLarge,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textSecondary,
|
|
textAlign: 'center',
|
|
marginBottom: theme.spacing.xl,
|
|
},
|
|
|
|
// Upload container
|
|
uploadContainer: {
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderWidth: 2,
|
|
borderColor: theme.colors.border,
|
|
borderStyle: 'dashed',
|
|
borderRadius: theme.borderRadius.medium,
|
|
padding: theme.spacing.xl,
|
|
marginBottom: theme.spacing.md,
|
|
minHeight: 200,
|
|
},
|
|
|
|
// Upload content
|
|
uploadContent: {
|
|
alignItems: 'center',
|
|
},
|
|
|
|
// Image preview container
|
|
imagePreviewContainer: {
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
},
|
|
|
|
// Image preview
|
|
imagePreview: {
|
|
width: '100%',
|
|
height: 150,
|
|
borderRadius: theme.borderRadius.medium,
|
|
marginBottom: theme.spacing.sm,
|
|
resizeMode: 'contain',
|
|
},
|
|
|
|
// Image overlay (remove button)
|
|
imageOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
right: 20,
|
|
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
|
borderRadius: 17,
|
|
width: 34,
|
|
height: 34,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
|
|
// Upload text
|
|
uploadText: {
|
|
fontSize: theme.typography.fontSize.bodyLarge,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.textPrimary,
|
|
marginTop: theme.spacing.sm,
|
|
},
|
|
|
|
// Upload subtext
|
|
uploadSubtext: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textSecondary,
|
|
marginTop: theme.spacing.xs,
|
|
},
|
|
|
|
// Uploaded text
|
|
uploadedText: {
|
|
fontSize: theme.typography.fontSize.bodyLarge,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.success,
|
|
marginTop: theme.spacing.sm,
|
|
},
|
|
|
|
// File name
|
|
fileName: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textSecondary,
|
|
marginTop: theme.spacing.xs,
|
|
textAlign: 'center',
|
|
maxWidth: '80%',
|
|
},
|
|
|
|
// File details
|
|
fileDetails: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginTop: theme.spacing.xs,
|
|
},
|
|
|
|
// File type
|
|
fileType: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textMuted,
|
|
},
|
|
|
|
// File size
|
|
fileSize: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textMuted,
|
|
},
|
|
|
|
// Change button
|
|
changeButton: {
|
|
alignSelf: 'center',
|
|
marginBottom: theme.spacing.xl,
|
|
},
|
|
|
|
// Change button text
|
|
changeButtonText: {
|
|
fontSize: theme.typography.fontSize.bodyMedium,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.primary,
|
|
},
|
|
|
|
// Input container
|
|
inputContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
borderWidth: 1,
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.medium,
|
|
marginBottom: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.md,
|
|
paddingVertical: 2,
|
|
},
|
|
|
|
// Input icon
|
|
inputIcon: {
|
|
marginRight: theme.spacing.sm,
|
|
},
|
|
|
|
// Input field
|
|
inputField: {
|
|
flex: 1,
|
|
paddingVertical: theme.spacing.md,
|
|
fontSize: theme.typography.fontSize.bodyLarge,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textPrimary,
|
|
},
|
|
|
|
// Eye icon
|
|
eyeIcon: {
|
|
padding: theme.spacing.sm,
|
|
},
|
|
|
|
// Rules container
|
|
rulesContainer: {
|
|
marginTop: theme.spacing.sm,
|
|
marginBottom: theme.spacing.xl,
|
|
paddingHorizontal: theme.spacing.sm,
|
|
},
|
|
|
|
// Rules title
|
|
rulesTitle: {
|
|
fontSize: theme.typography.fontSize.bodyMedium,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
color: theme.colors.textPrimary,
|
|
marginBottom: theme.spacing.sm,
|
|
},
|
|
|
|
// Rules grid
|
|
rulesGrid: {
|
|
gap: theme.spacing.xs,
|
|
},
|
|
|
|
// Rule container
|
|
ruleContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.xs,
|
|
},
|
|
|
|
// Checkbox
|
|
checkbox: {
|
|
width: 18,
|
|
height: 18,
|
|
borderRadius: 4,
|
|
borderWidth: 2,
|
|
borderColor: theme.colors.border,
|
|
backgroundColor: theme.colors.backgroundAlt,
|
|
marginRight: theme.spacing.sm,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
|
|
// Checkbox checked
|
|
checkboxChecked: {
|
|
backgroundColor: theme.colors.primary,
|
|
borderColor: theme.colors.primary,
|
|
},
|
|
|
|
// Rule text
|
|
ruleText: {
|
|
fontSize: theme.typography.fontSize.bodySmall,
|
|
fontFamily: theme.typography.fontFamily.regular,
|
|
color: theme.colors.textSecondary,
|
|
flex: 1,
|
|
},
|
|
|
|
// Rule text valid
|
|
ruleTextValid: {
|
|
color: theme.colors.success,
|
|
},
|
|
|
|
// Base button
|
|
button: {
|
|
paddingVertical: theme.spacing.md,
|
|
paddingHorizontal: theme.spacing.lg,
|
|
borderRadius: theme.borderRadius.medium,
|
|
alignItems: 'center',
|
|
marginBottom: theme.spacing.md,
|
|
},
|
|
|
|
// Upload button
|
|
uploadButton: {
|
|
backgroundColor: theme.colors.primary,
|
|
...theme.shadows.primary,
|
|
},
|
|
|
|
// Reset button
|
|
resetButton: {
|
|
backgroundColor: theme.colors.primary,
|
|
...theme.shadows.primary,
|
|
},
|
|
|
|
// Disabled button
|
|
buttonDisabled: {
|
|
opacity: 0.6,
|
|
},
|
|
|
|
// Button text
|
|
buttonText: {
|
|
color: theme.colors.background,
|
|
fontSize: theme.typography.fontSize.bodyLarge,
|
|
fontFamily: theme.typography.fontFamily.medium,
|
|
},
|
|
|
|
// Loading container
|
|
loadingContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
},
|
|
});
|
|
|
|
export default ResetPasswordScreen;
|
|
|
|
/*
|
|
* End of File: ResetPasswordScreen.tsx
|
|
* Design & Developed by Tech4Biz Solutions
|
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
|
*/
|