Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | 2x 26x 26x 26x 26x 26x 26x 3x 3x 26x 3x 3x 3x 26x 26x 26x 1x 25x 26x | /**
* ForgotPasswordConfirmation Component (TDD - GREEN Phase)
* Story 1.2.2: Implement Forgot Password Flow
*/
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Typography, Button, Alert, Snackbar } from '@mui/material';
import { CheckCircle } from '@mui/icons-material';
import { useForgotPassword } from '@hooks/useForgotPassword';
interface ForgotPasswordConfirmationProps {
email: string;
}
export const ForgotPasswordConfirmation: React.FC<ForgotPasswordConfirmationProps> = ({
email,
}) => {
const { t } = useTranslation(['auth', 'common']);
const [cooldown, setCooldown] = useState(0);
const [showSuccessToast, setShowSuccessToast] = useState(false);
// Reuse useForgotPassword for resending - Amplify allows multiple calls to resetPassword
const { mutate: resendCode, isPending } = useForgotPassword();
useEffect(() => {
if (cooldown > 0) {
const timer = setTimeout(() => setCooldown(cooldown - 1), 1000);
return () => clearTimeout(timer);
}
}, [cooldown]);
const handleResend = () => {
resendCode(email, {
onSuccess: () => {
setCooldown(60); // 60-second cooldown
setShowSuccessToast(true);
},
});
};
// Mask email for display (u***@example.com)
const maskEmail = (email: string) => {
const [local, domain] = email.split('@');
if (local.length === 1) {
return `${local}@${domain}`;
}
return `${local[0]}${'*'.repeat(local.length - 1)}@${domain}`;
};
return (
<Box sx={{ maxWidth: 400, mx: 'auto', mt: 8, textAlign: 'center' }}>
<CheckCircle sx={{ fontSize: 64, color: 'success.main', mb: 2 }} />
<Typography variant="h4" gutterBottom>
{t('auth:forgot.confirmTitle')}
</Typography>
<Typography variant="body1" sx={{ mb: 2 }}>
{t('auth:forgot.confirmMessage')}
</Typography>
<Typography variant="h6" sx={{ mb: 3, fontWeight: 'bold' }}>
{maskEmail(email)}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('auth:forgot.confirmInstructions')}
</Typography>
<Button variant="contained" fullWidth href="/auth/login" sx={{ mb: 2 }}>
{t('auth:forgot.backToLogin')}
</Button>
<Box sx={{ mt: 3 }}>
<Typography variant="body2" color="text.secondary">
{t('auth:forgot.didntReceive')}
</Typography>
<Button onClick={handleResend} disabled={cooldown > 0 || isPending} sx={{ mt: 1 }}>
{cooldown > 0
? t('common:status.wait', { seconds: cooldown })
: t('auth:forgot.resendButton')}
</Button>
</Box>
<Alert severity="info" sx={{ mt: 3 }}>
{t('auth:forgot.checkSpam')}
</Alert>
{/* Success toast notification */}
<Snackbar
open={showSuccessToast}
autoHideDuration={3000}
onClose={() => setShowSuccessToast(false)}
message={t('auth:forgot.resendSuccess')}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
/>
</Box>
);
};
|