NeoScan_Radiologist/app/modules/Settings/screens/ChangePasswordScreen.tsx
2025-08-14 20:16:03 +05:30

827 lines
24 KiB
TypeScript

/*
* File: ChangePasswordScreen.tsx
* Description: Change password screen with comprehensive password validation
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TextInput,
TouchableOpacity,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme/theme';
import { SettingsHeader } from '../components/SettingsHeader';
import { useAppDispatch } from '../../../store/hooks';
import { changePasswordAsync } from '../../Auth/redux/authActions';
/**
* ChangePasswordScreenProps Interface
*
* Purpose: Defines the props required by the ChangePasswordScreen component
*
* Props:
* - navigation: React Navigation object for screen navigation
*/
interface ChangePasswordScreenProps {
navigation: any;
}
/**
* FormData Interface
*
* Purpose: Defines the structure of the password change form data
*/
interface FormData {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}
/**
* FormErrors Interface
*
* Purpose: Defines the structure of form validation errors
*/
interface FormErrors {
currentPassword?: string;
newPassword?: string;
confirmPassword?: string;
}
/**
* PasswordStrength Interface
*
* Purpose: Defines the structure of password strength information
*/
interface PasswordStrength {
score: number;
label: string;
color: string;
requirements: string[];
}
/**
* ChangePasswordScreen Component
*
* Purpose: Allows users to change their password with comprehensive validation
* including current password verification, new password strength requirements,
* and password confirmation
*
* Features:
* 1. Current password verification
* 2. New password strength validation
* 3. Password confirmation matching
* 4. Real-time password strength indicator
* 5. Comprehensive error handling
*/
export const ChangePasswordScreen: React.FC<ChangePasswordScreenProps> = ({
navigation,
}) => {
// ============================================================================
// REDUX STATE
// ============================================================================
const dispatch = useAppDispatch();
// ============================================================================
// LOCAL STATE
// ============================================================================
const [formData, setFormData] = useState<FormData>({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [passwordStrength, setPasswordStrength] = useState<PasswordStrength>({
score: 0,
label: 'Very Weak',
color: theme.colors.error,
requirements: [],
});
// ============================================================================
// PASSWORD STRENGTH VALIDATION
// ============================================================================
/**
* checkPasswordStrength Function
*
* Purpose: Analyze password strength and return strength information
*
* @param password - Password to analyze
* @returns PasswordStrength object with score, label, color, and requirements
*/
const checkPasswordStrength = (password: string): PasswordStrength => {
const requirements: string[] = [];
let score = 0;
// Length requirement
if (password.length >= 8) {
score += 1;
requirements.push('✓ At least 8 characters');
} else {
requirements.push('✗ At least 8 characters');
}
// Uppercase requirement
if (/[A-Z]/.test(password)) {
score += 1;
requirements.push('✓ Contains uppercase letter');
} else {
requirements.push('✗ Contains uppercase letter');
}
// Lowercase requirement
if (/[a-z]/.test(password)) {
score += 1;
requirements.push('✓ Contains lowercase letter');
} else {
requirements.push('✗ Contains lowercase letter');
}
// Number requirement
if (/\d/.test(password)) {
score += 1;
requirements.push('✓ Contains number');
} else {
requirements.push('✗ Contains number');
}
// Special character requirement
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
score += 1;
requirements.push('✓ Contains special character');
} else {
requirements.push('✗ Contains special character');
}
// Determine strength label and color
let label: string;
let color: string;
if (score <= 1) {
label = 'Very Weak';
color = theme.colors.error;
} else if (score <= 2) {
label = 'Weak';
color = theme.colors.warning;
} else if (score <= 3) {
label = 'Fair';
color = theme.colors.warning;
} else if (score <= 4) {
label = 'Good';
color = theme.colors.info;
} else {
label = 'Strong';
color = theme.colors.success;
}
return {
score,
label,
color,
requirements,
};
};
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* validateField Function
*
* Purpose: Validate individual form fields
*
* @param field - Field name to validate
* @param value - Field value to validate
* @returns Validation error message or undefined
*/
const validateField = (field: keyof FormData, value: string): string | undefined => {
switch (field) {
case 'currentPassword':
if (!value.trim()) {
return 'Current password is required';
}
if (value.trim().length < 6) {
return 'Current password must be at least 6 characters';
}
break;
case 'newPassword':
if (!value.trim()) {
return 'New password is required';
}
if (value.trim().length < 8) {
return 'New password must be at least 8 characters';
}
if (value === formData.currentPassword) {
return 'New password must be different from current password';
}
break;
case 'confirmPassword':
if (!value.trim()) {
return 'Please confirm your new password';
}
if (value !== formData.newPassword) {
return 'Passwords do not match';
}
break;
}
return undefined;
};
/**
* validateForm Function
*
* Purpose: Validate entire form and return validation errors
*
* @returns Object containing validation errors
*/
const validateForm = (): FormErrors => {
const newErrors: FormErrors = {};
Object.keys(formData).forEach((field) => {
const key = field as keyof FormData;
const error = validateField(key, formData[key]);
if (error) {
newErrors[key] = error;
}
});
return newErrors;
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* handleInputChange Function
*
* Purpose: Handle input field changes and update password strength
*
* @param field - Field name that changed
* @param value - New field value
*/
const handleInputChange = (field: keyof FormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear field-specific error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
// Update password strength for new password field
if (field === 'newPassword') {
const strength = checkPasswordStrength(value);
setPasswordStrength(strength);
}
};
/**
* handleInputBlur Function
*
* Purpose: Validate field when user leaves the input
*
* @param field - Field name to validate
*/
const handleInputBlur = (field: keyof FormData) => {
const error = validateField(field, formData[field]);
setErrors(prev => ({ ...prev, [field]: error }));
};
/**
* handleSubmit Function
*
* Purpose: Handle form submission with validation and API call
*/
const handleSubmit = async () => {
// Validate form
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
// Check password strength
if (passwordStrength.score < 3) {
Alert.alert(
'Weak Password',
'Please choose a stronger password that meets the requirements.',
[{ text: 'OK' }]
);
return;
}
setIsSubmitting(true);
try {
// Dispatch password change action
await dispatch(changePasswordAsync({
currentPassword: formData.currentPassword,
newPassword: formData.newPassword,
})).unwrap();
// Navigate back after successful password change
navigation.goBack();
} catch (error: any) {
// Handle error - toast notification is already shown in the thunk
console.error('Password change error:', error);
} finally {
setIsSubmitting(false);
}
};
/**
* handleCancel Function
*
* Purpose: Handle cancel action
*/
const handleCancel = () => {
navigation.goBack();
};
/**
* togglePasswordVisibility Function
*
* Purpose: Toggle password visibility for specified field
*
* @param field - Field to toggle visibility for
*/
const togglePasswordVisibility = (field: 'current' | 'new' | 'confirm') => {
switch (field) {
case 'current':
setShowCurrentPassword(!showCurrentPassword);
break;
case 'new':
setShowNewPassword(!showNewPassword);
break;
case 'confirm':
setShowConfirmPassword(!showConfirmPassword);
break;
}
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
{/* Header with back button */}
<SettingsHeader
title="Change Password"
showBackButton={true}
onBackPress={handleCancel}
/>
{/* Scrollable form content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Password requirements info */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Password Requirements</Text>
<Text style={styles.infoText}>
Your new password must meet the following requirements to ensure security:
</Text>
<View style={styles.requirementsList}>
<Text style={styles.requirementItem}> At least 8 characters long</Text>
<Text style={styles.requirementItem}> Contains uppercase and lowercase letters</Text>
<Text style={styles.requirementItem}> Contains at least one number</Text>
<Text style={styles.requirementItem}> Contains at least one special character</Text>
</View>
</View>
{/* Password change form */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Change Password</Text>
{/* Current Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Current Password *</Text>
<View style={styles.passwordInputContainer}>
<TextInput
style={[
styles.textInput,
styles.passwordInput,
errors.currentPassword ? styles.inputError : null,
]}
value={formData.currentPassword}
onChangeText={(value) => handleInputChange('currentPassword', value)}
onBlur={() => handleInputBlur('currentPassword')}
placeholder="Enter your current password"
placeholderTextColor={theme.colors.textMuted}
secureTextEntry={!showCurrentPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeIcon}
onPress={() => togglePasswordVisibility('current')}
activeOpacity={0.7}
>
<Icon
name={showCurrentPassword ? 'eye-off' : 'eye'}
size={20}
color={theme.colors.textMuted}
/>
</TouchableOpacity>
</View>
{errors.currentPassword && (
<Text style={styles.errorText}>{errors.currentPassword}</Text>
)}
</View>
{/* New Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>New Password *</Text>
<View style={styles.passwordInputContainer}>
<TextInput
style={[
styles.textInput,
styles.passwordInput,
errors.newPassword ? styles.inputError : null,
]}
value={formData.newPassword}
onChangeText={(value) => handleInputChange('newPassword', value)}
onBlur={() => handleInputBlur('newPassword')}
placeholder="Enter your new password"
placeholderTextColor={theme.colors.textMuted}
secureTextEntry={!showNewPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeIcon}
onPress={() => togglePasswordVisibility('new')}
activeOpacity={0.7}
>
<Icon
name={showNewPassword ? 'eye-off' : 'eye'}
size={20}
color={theme.colors.textMuted}
/>
</TouchableOpacity>
</View>
{errors.newPassword && (
<Text style={styles.errorText}>{errors.newPassword}</Text>
)}
</View>
{/* Password Strength Indicator */}
{formData.newPassword.length > 0 && (
<View style={styles.strengthContainer}>
<Text style={styles.strengthLabel}>Password Strength:</Text>
<View style={styles.strengthBar}>
<View
style={[
styles.strengthProgress,
{
width: `${(passwordStrength.score / 5) * 100}%`,
backgroundColor: passwordStrength.color,
}
]}
/>
</View>
<Text style={[styles.strengthText, { color: passwordStrength.color }]}>
{passwordStrength.label}
</Text>
</View>
)}
{/* Password Requirements Check */}
{formData.newPassword.length > 0 && (
<View style={styles.requirementsCheck}>
{passwordStrength.requirements.map((requirement, index) => (
<Text key={index} style={styles.requirementCheckItem}>
{requirement}
</Text>
))}
</View>
)}
{/* Confirm Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Confirm New Password *</Text>
<View style={styles.passwordInputContainer}>
<TextInput
style={[
styles.textInput,
styles.passwordInput,
errors.confirmPassword ? styles.inputError : null,
]}
value={formData.confirmPassword}
onChangeText={(value) => handleInputChange('confirmPassword', value)}
onBlur={() => handleInputBlur('confirmPassword')}
placeholder="Confirm your new password"
placeholderTextColor={theme.colors.textMuted}
secureTextEntry={!showConfirmPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeIcon}
onPress={() => togglePasswordVisibility('confirm')}
activeOpacity={0.7}
>
<Icon
name={showConfirmPassword ? 'eye-off' : 'eye'}
size={20}
color={theme.colors.textMuted}
/>
</TouchableOpacity>
</View>
{errors.confirmPassword && (
<Text style={styles.errorText}>{errors.confirmPassword}</Text>
)}
</View>
</View>
{/* Action buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.submitButton,
isSubmitting && styles.submitButtonDisabled,
]}
onPress={handleSubmit}
disabled={isSubmitting}
activeOpacity={0.7}
>
<Text style={[
styles.submitButtonText,
isSubmitting && styles.submitButtonTextDisabled,
]}>
{isSubmitting ? 'Changing Password...' : 'Change Password'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cancelButton}
onPress={handleCancel}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
{/* Bottom spacing for tab bar */}
<View style={styles.bottomSpacing} />
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES SECTION
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view styling
scrollView: {
flex: 1,
},
// Scroll content styling
scrollContent: {
paddingHorizontal: theme.spacing.md,
},
// Bottom spacing for tab bar
bottomSpacing: {
height: theme.spacing.xl,
},
// Information sections
infoSection: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
},
infoText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
lineHeight: 22,
marginBottom: theme.spacing.md,
},
// Requirements list styling
requirementsList: {
marginTop: theme.spacing.sm,
},
requirementItem: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Input container styling
inputContainer: {
marginBottom: theme.spacing.sm,
backgroundColor: theme.colors.background,
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.small,
},
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
fontWeight: '600',
},
// Password input styling
passwordInputContainer: {
position: 'relative',
},
textInput: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
backgroundColor: theme.colors.background,
minHeight: 48,
},
passwordInput: {
paddingRight: theme.spacing.xl + theme.spacing.md,
},
eyeIcon: {
position: 'absolute',
right: theme.spacing.md,
top: theme.spacing.md,
padding: theme.spacing.xs,
},
inputError: {
borderColor: theme.colors.error,
backgroundColor: theme.colors.error + '10',
},
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.sm,
marginLeft: theme.spacing.xs,
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
backgroundColor: theme.colors.error + '10',
borderRadius: theme.borderRadius.small,
alignSelf: 'flex-start',
},
// Password strength styling
strengthContainer: {
marginBottom: theme.spacing.md,
},
strengthLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
strengthBar: {
height: 4,
backgroundColor: theme.colors.border,
borderRadius: 2,
marginBottom: theme.spacing.xs,
overflow: 'hidden',
},
strengthProgress: {
height: '100%',
borderRadius: 2,
},
strengthText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
textAlign: 'center',
},
// Requirements check styling
requirementsCheck: {
marginBottom: theme.spacing.md,
padding: theme.spacing.sm,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.small,
},
requirementCheckItem: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Button container
buttonContainer: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.md,
},
submitButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
submitButtonDisabled: {
backgroundColor: theme.colors.border,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
submitButtonText: {
color: theme.colors.background,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
},
submitButtonTextDisabled: {
color: theme.colors.textMuted,
},
cancelButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
},
cancelButtonText: {
color: theme.colors.textSecondary,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
},
});
/*
* End of File: ChangePasswordScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/