/* * 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 = ({ 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(null); const [documentUploaded, setDocumentUploaded] = useState(false); // Current step state const [currentStep, setCurrentStep] = useState('document'); // Redux state const dispatch = useAppDispatch(); const user = useAppSelector((state) => state.auth.user); // Password validation rules const [passwordRules, setPasswordRules] = useState([ { 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 - Whether permission was granted */ const requestCameraPermission = async (): Promise => { 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); console.log('upload response',response) 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, }); console.log('reset response', response); 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) => ( {rule.isValid && ( )} {rule.label} ); /** * renderDocumentUploadStep Function * * Purpose: Render document upload step * * @returns JSX element for document upload step */ const renderDocumentUploadStep = () => ( <> {/* Icon */} {/* Title and Subtitle */} Upload Your ID Document Please upload a clear photo of your hospital-issued ID for verification {/* Document Upload Area */} {selectedImage ? ( Document Selected {selectedImage.name} {getFileTypeDisplay(selectedImage.type)} {selectedImage.size && ( • {formatFileSize(selectedImage.size)} )} ) : ( <> Tap to upload document JPG, PNG supported )} {selectedImage && ( Change Document )} {/* Upload Button */} {loading ? ( Uploading Document... ) : ( Upload Document )} ); /** * renderPasswordResetStep Function * * Purpose: Render password reset step * * @returns JSX element for password reset step */ const renderPasswordResetStep = () => ( <> {/* Icon */} {/* Title and Subtitle */} Set Your Password Create a strong password to complete your account setup {/* Password Input */} setPasswordVisible(!isPasswordVisible)} style={styles.eyeIcon} disabled={loading} > {/* Confirm Password Input */} setConfirmPasswordVisible(!isConfirmPasswordVisible)} style={styles.eyeIcon} disabled={loading} > {/* Password Rules Section */} Password Requirements: {passwordRules.map(renderPasswordRule)} {/* Reset Button */} {loading ? ( Setting Password... ) : ( Set Password )} ); // ============================================================================ // RENDER SECTION // ============================================================================ return ( {/* Header */} {currentStep === 'document' ? 'Upload Document' : 'Set Password'} {/* Step Indicator */} {user?.platform === 'web' && ( 1 Document 2 Password )} {/* Content based on current step */} {currentStep === 'document' ? renderDocumentUploadStep() : renderPasswordResetStep()} ); }; // ============================================================================ // 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. */