diff --git a/messages/en.json b/messages/en.json index e1fd815..77b3d33 100644 --- a/messages/en.json +++ b/messages/en.json @@ -116,6 +116,16 @@ "transitFeedsRedirectTitle": "You've been redirected from TransitFeeds", "transitFeedsRedirectBody": "This page now lives on MobilityDatabase.org, where you'll find the most up-to-date transit data." }, + "emailVerification": { + "loadingTitle": "Verifying your email", + "loadingDescription": "Please wait while we confirm your email address.", + "successTitle": "Email verified", + "successDescription": "Your email address has been verified successfully.You can now continue using your Mobility Database account.", + "errorTitle": "Unable to verify email", + "errorDescription": "This verification link is invalid, incomplete, or has already been used.", + "invalidLink": "The verification link is missing required information.", + "verificationFailed": "We could not verify your email with this link." + }, "feeds": { "feeds": "Feeds", "dataType": "Data Format", diff --git a/messages/fr.json b/messages/fr.json index b65bfd9..0450aa9 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -116,6 +116,16 @@ "transitFeedsRedirectTitle": "Vous avez été redirigé depuis TransitFeeds", "transitFeedsRedirectBody": "Cette page se trouve désormais sur MobilityDatabase.org, où vous trouverez les données de transit les plus récentes." }, + "emailVerification": { + "loadingTitle": "Vérification de votre e-mail", + "loadingDescription": "Veuillez patienter pendant que nous confirmons votre adresse e-mail.", + "successTitle": "E-mail vérifié", + "successDescription": "Votre adresse e-mail a bien été vérifiée. Vous pouvez retourner sur Mobility Database et continuer à utiliser votre compte.", + "errorTitle": "Impossible de vérifier l'e-mail", + "errorDescription": "Ce lien de vérification est invalide, incomplet ou a déjà été utilisé.", + "invalidLink": "Le lien de vérification ne contient pas les informations requises.", + "verificationFailed": "Nous n'avons pas pu vérifier votre e-mail avec ce lien." + }, "feeds": { "feeds": "Feeds", "dataType": "Data Format", diff --git a/src/app/[locale]/email-verification/EmailVerificationContent.tsx b/src/app/[locale]/email-verification/EmailVerificationContent.tsx new file mode 100644 index 0000000..d84bbdf --- /dev/null +++ b/src/app/[locale]/email-verification/EmailVerificationContent.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as React from 'react'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { + Alert, + CircularProgress, + Stack, + Typography, + useTheme, +} from '@mui/material'; +import { useTranslations } from 'next-intl'; +import { app } from '../../../firebase'; +import { ContentBox } from '../../components/ContentBox'; + +type VerificationStatus = 'loading' | 'success' | 'error'; + +interface EmailVerificationContentProps { + mode?: string; + oobCode?: string; +} + +export default function EmailVerificationContent({ + mode, + oobCode, +}: EmailVerificationContentProps): React.ReactElement { + const t = useTranslations('emailVerification'); + const theme = useTheme(); + const [status, setStatus] = React.useState('loading'); + const [errorMessage, setErrorMessage] = React.useState(null); + + React.useEffect(() => { + const verifyEmail = async (): Promise => { + if (mode !== 'verifyEmail' || oobCode == null || oobCode.length === 0) { + setStatus('error'); + setErrorMessage(t('invalidLink')); + return; + } + + try { + await app.auth().applyActionCode(oobCode); + + setStatus('success'); + setErrorMessage(null); + } catch { + setStatus('error'); + setErrorMessage(t('verificationFailed')); + } + }; + + void verifyEmail(); + }, [mode, oobCode]); + + return ( + + + {status === 'loading' ? ( + + ) : status === 'success' ? ( + + ) : ( + + )} + + + + {status === 'loading' + ? t('loadingTitle') + : status === 'success' + ? t('successTitle') + : t('errorTitle')} + + + {status === 'loading' + ? t('loadingDescription') + : status === 'success' + ? t('successDescription') + : t('errorDescription')} + + + + {errorMessage != null && ( + + {errorMessage} + + )} + + + ); +} diff --git a/src/app/[locale]/email-verification/page.tsx b/src/app/[locale]/email-verification/page.tsx new file mode 100644 index 0000000..3b63fff --- /dev/null +++ b/src/app/[locale]/email-verification/page.tsx @@ -0,0 +1,48 @@ +import { type ReactElement } from 'react'; +import { setRequestLocale } from 'next-intl/server'; +import { type Metadata } from 'next'; +import { type Locale, routing } from '../../../i18n/routing'; +import EmailVerificationContent from './EmailVerificationContent'; + +export const metadata: Metadata = { + title: 'Email Verification | MobilityDatabase', + description: + 'Verify your Mobility Database account email address through Firebase authentication.', + robots: { + index: false, + follow: false, + googleBot: { + index: false, + follow: false, + 'max-image-preview': 'none', + 'max-snippet': -1, + 'max-video-preview': -1, + }, + }, +}; + +export function generateStaticParams(): Array<{ + locale: Locale; +}> { + return routing.locales.map((locale) => ({ locale })); +} + +interface PageProps { + params: Promise<{ locale: string }>; + searchParams: Promise<{ + mode?: string; + oobCode?: string; + }>; +} + +export default async function EmailVerificationPage({ + params, + searchParams, +}: PageProps): Promise { + const { locale } = await params; + const { mode, oobCode } = await searchParams; + + setRequestLocale(locale); + + return ; +}