628 lines
17 KiB
TypeScript
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.
|
|
*/
|