Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
101 changes: 101 additions & 0 deletions src/app/[locale]/email-verification/EmailVerificationContent.tsx
Original file line number Diff line number Diff line change
@@ -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<VerificationStatus>('loading');
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);

React.useEffect(() => {
const verifyEmail = async (): Promise<void> => {
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 (
<ContentBox
title=''
sx={{
display: 'flex',
justifyContent: 'center',
backgroundColor: theme.palette.background.paper,
maxWidth: theme.breakpoints.values.sm,
mx: 'auto',
mt: 6,
}}
>
<Stack spacing={3} alignItems='center' textAlign='center'>
{status === 'loading' ? (
<CircularProgress aria-label={t('loadingTitle')} />
) : status === 'success' ? (
<CheckCircleOutlineIcon color='success' sx={{ fontSize: 56 }} />
) : (
<ErrorOutlineIcon color='error' sx={{ fontSize: 56 }} />
)}

<Stack spacing={1.5}>
<Typography variant='h4' component='h1' sx={{ fontWeight: 700 }}>
{status === 'loading'
? t('loadingTitle')
: status === 'success'
? t('successTitle')
: t('errorTitle')}
</Typography>
<Typography variant='body1' color='text.secondary'>
{status === 'loading'
? t('loadingDescription')
: status === 'success'
? t('successDescription')
: t('errorDescription')}
</Typography>
</Stack>

{errorMessage != null && (
<Alert severity='error' variant='outlined' sx={{ width: '100%' }}>
{errorMessage}
</Alert>
)}
</Stack>
</ContentBox>
);
}
48 changes: 48 additions & 0 deletions src/app/[locale]/email-verification/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
}>;
Comment on lines +30 to +35
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The page ignores Firebase’s lang query parameter (action links typically include &lang=en|fr). With localePrefix: 'as-needed', users opening /email-verification?...&lang=fr will still resolve to the default en locale unless the URL is /fr/..., so the French verification email/link can land on an English page. Consider accepting lang in searchParams and, when it matches a supported locale and differs from the current route locale, redirecting to the corresponding localized path before rendering.

Copilot uses AI. Check for mistakes.
}

export default async function EmailVerificationPage({
params,
searchParams,
}: PageProps): Promise<ReactElement> {
const { locale } = await params;
const { mode, oobCode } = await searchParams;

setRequestLocale(locale);

return <EmailVerificationContent mode={mode} oobCode={oobCode} />;
}
Loading