NeoScan_Radiologist/app/modules/Auth/components/signup/PasswordStep.tsx
2025-08-05 18:01:36 +05:30

628 lines
17 KiB
TypeScript

/*
* File: PasswordStep.tsx
* Description: Password step component for signup flow with comprehensive validation
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native';
import { theme } from '../../../../theme/theme';
import { PasswordStepProps } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
// ============================================================================
// INTERFACES
// ============================================================================
/**
* PasswordRule Interface
*
* Purpose: Defines the structure for password validation rules
*/
interface PasswordRule {
id: string;
label: string;
validator: (password: string) => boolean;
isValid: boolean;
}
// ============================================================================
// PASSWORD STEP COMPONENT
// ============================================================================
/**
* PasswordStep Component
*
* Purpose: Second step of signup flow - password creation with comprehensive validation
*
* Features:
* - Password input with visibility toggle
* - Comprehensive password validation rules
* - Real-time password strength checking
* - Visual feedback for each requirement
* - Continue button with loading state
* - Back navigation with modern header
*/
const PasswordStep: React.FC<PasswordStepProps> = ({
onContinue,
onBack,
data,
isLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [password, setPassword] = useState(data.password || '');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [confirmPasswordError, setConfirmPasswordError] = useState('');
const [isPasswordVisible, setPasswordVisible] = useState(false);
const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
// 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 === confirmPassword && confirmPassword.length > 0,
isValid: false,
},
]);
// ============================================================================
// EFFECTS
// ============================================================================
/**
* useEffect for password validation
*
* Purpose: Update password rules when password or confirm password changes
*/
useEffect(() => {
updatePasswordRules(password);
}, [password, confirmPassword]);
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* Update Password Rules
*
* Purpose: Update password validation rules based on current password and confirm password
*
* @param pwd - Current password value
*/
const updatePasswordRules = (pwd: string) => {
setPasswordRules(prevRules =>
prevRules.map(rule => {
if (rule.id === 'match') {
return {
...rule,
isValid: pwd === confirmPassword && confirmPassword.length > 0,
};
}
return {
...rule,
isValid: rule.validator(pwd),
};
})
);
};
/**
* Validate Password
*
* 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);
};
/**
* Handle Password Change
*
* Purpose: Update password and clear errors
*/
const handlePasswordChange = (text: string) => {
setPassword(text);
setPasswordError('');
// Clear confirm password error if passwords now match
if (confirmPassword && text === confirmPassword) {
setConfirmPasswordError('');
}
// Update password rules to reflect the match status
updatePasswordRules(text);
};
/**
* Handle Confirm Password Change
*
* Purpose: Update confirm password and validate match
*/
const handleConfirmPasswordChange = (text: string) => {
setConfirmPassword(text);
setConfirmPasswordError('');
// Check if passwords match
if (text && text !== password) {
setConfirmPasswordError('Passwords do not match');
}
// Update password rules to reflect the match status
updatePasswordRules(password);
};
/**
* Handle Continue
*
* Purpose: Validate password and proceed to next step
*/
const handleContinue = () => {
// Clear previous errors
setPasswordError('');
setConfirmPasswordError('');
// Validate password
if (!password.trim()) {
setPasswordError('Password is required');
return;
}
if (!validatePassword(password.trim())) {
setPasswordError('Please meet all password requirements');
return;
}
// Validate confirm password
if (!confirmPassword.trim()) {
setConfirmPasswordError('Please confirm your password');
return;
}
if (password.trim() !== confirmPassword.trim()) {
setConfirmPasswordError('Passwords do not match');
return;
}
// Call parent handler
onContinue(password.trim());
};
/**
* Render Password Rule
*
* 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>
);
// ============================================================================
// RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Step 2 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Create a strong password</Text>
<Text style={styles.description}>
Choose a password that meets all the security requirements below.
</Text>
{/* Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Password</Text>
<View style={[
styles.inputWrapper,
passwordError ? styles.inputWrapperError : null,
]}>
<TextInput
style={styles.input}
placeholder="Enter your password"
placeholderTextColor={theme.colors.textMuted}
value={password}
onChangeText={handlePasswordChange}
secureTextEntry={!isPasswordVisible}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<TouchableOpacity
onPress={() => setPasswordVisible(!isPasswordVisible)}
style={styles.eyeIcon}
disabled={isLoading}
>
<Icon
name={isPasswordVisible ? 'eye-off' : 'eye'}
size={22}
color={theme.colors.textSecondary}
/>
</TouchableOpacity>
</View>
{passwordError ? (
<Text style={styles.errorText}>{passwordError}</Text>
) : null}
</View>
{/* Confirm Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Confirm Password</Text>
<View style={[
styles.inputWrapper,
confirmPasswordError ? styles.inputWrapperError : null,
]}>
<TextInput
style={styles.input}
placeholder="Confirm your password"
placeholderTextColor={theme.colors.textMuted}
value={confirmPassword}
onChangeText={handleConfirmPasswordChange}
secureTextEntry={!isConfirmPasswordVisible}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<TouchableOpacity
onPress={() => setConfirmPasswordVisible(!isConfirmPasswordVisible)}
style={styles.eyeIcon}
disabled={isLoading}
>
<Icon
name={isConfirmPasswordVisible ? 'eye-off' : 'eye'}
size={22}
color={theme.colors.textSecondary}
/>
</TouchableOpacity>
</View>
{confirmPasswordError ? (
<Text style={styles.errorText}>{confirmPasswordError}</Text>
) : null}
</View>
{/* Password Requirements */}
<View style={styles.requirementsContainer}>
<Text style={styles.requirementsTitle}>Password Requirements:</Text>
<View style={styles.rulesGrid}>
{passwordRules.map(renderPasswordRule)}
</View>
</View>
{/* Continue Button */}
<TouchableOpacity
style={[
styles.continueButton,
(!password.trim() || !confirmPassword.trim() || isLoading || !validatePassword(password) || password !== confirmPassword) ? styles.continueButtonDisabled : null,
]}
onPress={handleContinue}
disabled={!password.trim() || !confirmPassword.trim() || isLoading || !validatePassword(password) || password !== confirmPassword}
>
<Text style={[
styles.continueButtonText,
(!password.trim() || !confirmPassword.trim() || isLoading || !validatePassword(password) || password !== confirmPassword) ? styles.continueButtonTextDisabled : null,
]}>
{isLoading ? 'Processing...' : 'Continue'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view
scrollView: {
flex: 1,
},
// Scroll content
scrollContent: {
flexGrow: 1,
paddingHorizontal: theme.spacing.sm,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
marginBottom: theme.spacing.lg,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
justifyContent: 'center',
paddingBottom: theme.spacing.xl,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xl,
},
// Input container
inputContainer: {
marginBottom: theme.spacing.xl,
},
// Input label
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Input wrapper
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.background,
},
// Input field
input: {
flex: 1,
paddingHorizontal: theme.spacing.md,
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,
},
// Input wrapper error state
inputWrapperError: {
borderColor: theme.colors.error,
},
// Error text
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.xs,
},
// Requirements container
requirementsContainer: {
marginBottom: theme.spacing.xl,
padding: theme.spacing.md,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.medium,
},
// Requirements title
requirementsTitle: {
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,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.lg,
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
});
export default PasswordStep;
/*
* End of File: PasswordStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/