feat(compliance): Implement COPPA/GDPR compliance UI
Frontend Compliance Features: - Created compliance API client (data export, account deletion, deletion status) - Added DataExport component with download functionality - Added AccountDeletion component with 30-day grace period UI - Updated Settings page with Privacy & Compliance sections COPPA Age Verification: - Added date of birth field to registration - Age calculation with COPPA compliance (under 13 blocked) - Parental email and consent for users 13-17 - Dynamic form validation based on age Privacy & Terms: - Separate checkboxes for Terms of Service and Privacy Policy - Required acceptance for registration - Links to policy pages Completes GDPR Right to Data Portability and Right to Erasure. Completes COPPA parental consent requirements.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
TextField,
|
||||
@@ -32,12 +32,39 @@ const registerSchema = z.object({
|
||||
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain at least one number'),
|
||||
confirmPassword: z.string(),
|
||||
dateOfBirth: z.string().min(1, 'Date of birth is required'),
|
||||
parentalEmail: z.string().email('Invalid email address').optional().or(z.literal('')),
|
||||
agreeToTerms: z.boolean().refine(val => val === true, {
|
||||
message: 'You must agree to the terms and conditions',
|
||||
message: 'You must agree to the Terms of Service',
|
||||
}),
|
||||
agreeToPrivacy: z.boolean().refine(val => val === true, {
|
||||
message: 'You must agree to the Privacy Policy',
|
||||
}),
|
||||
coppaConsent: z.boolean().optional(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match',
|
||||
path: ['confirmPassword'],
|
||||
}).refine((data) => {
|
||||
// Check if user is under 18 and requires parental consent
|
||||
const birthDate = new Date(data.dateOfBirth);
|
||||
const today = new Date();
|
||||
const age = today.getFullYear() - birthDate.getFullYear() -
|
||||
(today.getMonth() < birthDate.getMonth() ||
|
||||
(today.getMonth() === birthDate.getMonth() && today.getDate() < birthDate.getDate()) ? 1 : 0);
|
||||
|
||||
if (age < 13) {
|
||||
return false; // Users under 13 cannot register (COPPA compliance)
|
||||
}
|
||||
|
||||
if (age >= 13 && age < 18) {
|
||||
// Users 13-17 need parental email and consent
|
||||
return !!data.parentalEmail && data.parentalEmail.length > 0 && data.coppaConsent === true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, {
|
||||
message: 'Users under 13 cannot create an account. Users 13-17 require parental consent and email.',
|
||||
path: ['dateOfBirth'],
|
||||
});
|
||||
|
||||
type RegisterFormData = z.infer<typeof registerSchema>;
|
||||
@@ -47,16 +74,50 @@ export default function RegisterPage() {
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [userAge, setUserAge] = useState<number | null>(null);
|
||||
const [requiresParentalConsent, setRequiresParentalConsent] = useState(false);
|
||||
const { register: registerUser } = useAuth();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<RegisterFormData>({
|
||||
resolver: zodResolver(registerSchema),
|
||||
defaultValues: {
|
||||
agreeToTerms: false,
|
||||
agreeToPrivacy: false,
|
||||
coppaConsent: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Watch date of birth to calculate age and show parental consent if needed
|
||||
const dateOfBirth = watch('dateOfBirth');
|
||||
|
||||
// Calculate age when date of birth changes
|
||||
const calculateAge = (dob: string): number | null => {
|
||||
if (!dob) return null;
|
||||
const birthDate = new Date(dob);
|
||||
const today = new Date();
|
||||
const age = today.getFullYear() - birthDate.getFullYear() -
|
||||
(today.getMonth() < birthDate.getMonth() ||
|
||||
(today.getMonth() === birthDate.getMonth() && today.getDate() < birthDate.getDate()) ? 1 : 0);
|
||||
return age;
|
||||
};
|
||||
|
||||
// Update age and parental consent requirement when DOB changes
|
||||
React.useEffect(() => {
|
||||
if (dateOfBirth) {
|
||||
const age = calculateAge(dateOfBirth);
|
||||
setUserAge(age);
|
||||
setRequiresParentalConsent(age !== null && age >= 13 && age < 18);
|
||||
} else {
|
||||
setUserAge(null);
|
||||
setRequiresParentalConsent(false);
|
||||
}
|
||||
}, [dateOfBirth]);
|
||||
|
||||
const onSubmit = async (data: RegisterFormData) => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@@ -66,6 +127,9 @@ export default function RegisterPage() {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
dateOfBirth: data.dateOfBirth,
|
||||
parentalEmail: data.parentalEmail || undefined,
|
||||
coppaConsentGiven: data.coppaConsent || false,
|
||||
});
|
||||
// Navigation to onboarding is handled in the register function
|
||||
} catch (err: any) {
|
||||
@@ -208,33 +272,115 @@ export default function RegisterPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
{...register('agreeToTerms')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
I agree to the{' '}
|
||||
<MuiLink href="/terms" target="_blank">
|
||||
Terms of Service
|
||||
</MuiLink>{' '}
|
||||
and{' '}
|
||||
<MuiLink href="/privacy" target="_blank">
|
||||
Privacy Policy
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mt: 2 }}
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Date of Birth"
|
||||
type="date"
|
||||
margin="normal"
|
||||
error={!!errors.dateOfBirth}
|
||||
helperText={errors.dateOfBirth?.message || 'Required for COPPA compliance (users under 13 cannot register)'}
|
||||
{...register('dateOfBirth')}
|
||||
disabled={isLoading}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
/>
|
||||
{errors.agreeToTerms && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 1 }}>
|
||||
{errors.agreeToTerms.message}
|
||||
</Typography>
|
||||
|
||||
{userAge !== null && userAge < 13 && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
Users under 13 years old cannot create an account per COPPA regulations.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{requiresParentalConsent && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
As you are under 18, parental consent is required to create an account.
|
||||
</Alert>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Parent/Guardian Email"
|
||||
type="email"
|
||||
margin="normal"
|
||||
error={!!errors.parentalEmail}
|
||||
helperText={errors.parentalEmail?.message || 'We will send a consent email to your parent/guardian'}
|
||||
{...register('parentalEmail')}
|
||||
disabled={isLoading}
|
||||
InputProps={{
|
||||
sx: { borderRadius: 3 },
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
{...register('coppaConsent')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
I confirm that I have my parent/guardian's permission to create this account
|
||||
</Typography>
|
||||
}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
{errors.coppaConsent && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 1 }}>
|
||||
Parental consent is required for users under 18
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
{...register('agreeToTerms')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
I agree to the{' '}
|
||||
<MuiLink href="/terms" target="_blank" underline="hover">
|
||||
Terms of Service
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{errors.agreeToTerms && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5, ml: 4 }}>
|
||||
{errors.agreeToTerms.message}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
{...register('agreeToPrivacy')}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
I agree to the{' '}
|
||||
<MuiLink href="/privacy" target="_blank" underline="hover">
|
||||
Privacy Policy
|
||||
</MuiLink>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{errors.agreeToPrivacy && (
|
||||
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5, ml: 4 }}>
|
||||
{errors.agreeToPrivacy.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
type="submit"
|
||||
|
||||
Reference in New Issue
Block a user