Add confirm email page
This commit is contained in:
parent
db14f0129a
commit
40ce04ecc0
7 changed files with 313 additions and 19 deletions
|
|
@ -52,6 +52,7 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
|
|||
email: data.email,
|
||||
password: data.password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/`,
|
||||
data: {
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
|
|
@ -60,13 +61,20 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
|
|||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
// Only sign up to stream if user is immediately confirmed (auto-confirm enabled in Supabase)
|
||||
if (response.session?.access_token) {
|
||||
await signUpToStream(response.session.access_token);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
onSuccess: () => {
|
||||
if (redirectUrl) {
|
||||
onSuccess: (data) => {
|
||||
// If there's no session, it means email confirmation is required
|
||||
if (!data.user?.email_confirmed_at) {
|
||||
// Store the email for the confirmation page
|
||||
localStorage.setItem("pendingConfirmationEmail", data.user?.email || "");
|
||||
navigate("/confirm-email");
|
||||
} else if (redirectUrl) {
|
||||
localStorage.removeItem("redirectUrl");
|
||||
navigate(decodeURIComponent(redirectUrl));
|
||||
} else {
|
||||
|
|
@ -120,7 +128,22 @@ export function useLoginEmail({ redirectUrl }: { redirectUrl: string | null }) {
|
|||
}
|
||||
return response;
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
// Check if email is verified
|
||||
if (data.user && !data.user.email_confirmed_at) {
|
||||
// Email not verified, redirect to confirmation page
|
||||
localStorage.setItem("pendingConfirmationEmail", data.user.email || "");
|
||||
toast.add({
|
||||
title: "Email non vérifié",
|
||||
description: "Veuillez vérifier votre email avant de vous connecter",
|
||||
type: "warning",
|
||||
position: "top-center",
|
||||
});
|
||||
navigate("/confirm-email");
|
||||
return;
|
||||
}
|
||||
|
||||
// Email verified, proceed with login
|
||||
if (redirectUrl) {
|
||||
localStorage.removeItem("redirectUrl");
|
||||
navigate(decodeURIComponent(redirectUrl));
|
||||
|
|
@ -133,6 +156,20 @@ export function useLoginEmail({ redirectUrl }: { redirectUrl: string | null }) {
|
|||
.with("invalid_credentials", () => {
|
||||
setErrors({ email: "Email ou mot de passe incorrect" });
|
||||
})
|
||||
.with("email_not_confirmed", () => {
|
||||
toast.add(
|
||||
{
|
||||
title: "Email non vérifié",
|
||||
description: "Veuillez vérifier votre email avant de vous connecter",
|
||||
type: "warning",
|
||||
position: "top-center",
|
||||
},
|
||||
{
|
||||
timeout: 7000,
|
||||
}
|
||||
);
|
||||
navigate("/confirm-email");
|
||||
})
|
||||
.otherwise(() => {
|
||||
toast.add(
|
||||
{
|
||||
|
|
@ -199,3 +236,35 @@ export const useAuthedApi = (): AxiosInstance => {
|
|||
},
|
||||
});
|
||||
};
|
||||
|
||||
export function useResendConfirmationEmail() {
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async (email: string) => {
|
||||
const { error } = await supabase.auth.resend({
|
||||
type: "signup",
|
||||
email: email,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}/`,
|
||||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.add({
|
||||
title: "Email envoyé",
|
||||
description: "Un nouvel email de confirmation vous a été envoyé",
|
||||
type: "success",
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.add({
|
||||
title: "Erreur",
|
||||
description: error.message || "Impossible d'envoyer l'email de confirmation",
|
||||
type: "error",
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
return { mutate, isPending };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { SignUpPage } from "../pages/signup";
|
|||
import { TabloPage } from "../pages/tablo";
|
||||
import { TabloDetailsPage } from "../pages/tablo-details";
|
||||
import { UpdatePasswordPage } from "../pages/update-password";
|
||||
import { ConfirmEmailPage } from "../pages/confirm-email";
|
||||
import ChatProvider from "../providers/ChatProvider";
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
|
|
@ -132,6 +133,11 @@ export const routes: RouteObject[] = [
|
|||
path: "/update-password",
|
||||
element: <UpdatePasswordPage />,
|
||||
},
|
||||
// Email confirmation waiting page (public)
|
||||
{
|
||||
path: "/confirm-email",
|
||||
element: <ConfirmEmailPage />,
|
||||
},
|
||||
// Authentication pages (redirected to "/" if user is authenticated)
|
||||
{
|
||||
path: "/",
|
||||
|
|
|
|||
|
|
@ -70,5 +70,35 @@
|
|||
"goToLogin": "Go to login",
|
||||
"error": "An error occurred while updating your password. Please try again or request a new reset link.",
|
||||
"errorTitle": "Error"
|
||||
},
|
||||
"confirmEmail": {
|
||||
"title": "Check your email",
|
||||
"subtitle": "We've sent you a confirmation email to verify your account.",
|
||||
"emailSentTo": "Email sent to:",
|
||||
"nextSteps": "Next steps:",
|
||||
"step1": "Check your email inbox (and spam folder)",
|
||||
"step2": "Click the confirmation link in the email",
|
||||
"step3": "You'll be redirected to your account",
|
||||
"didntReceive": "Didn't receive the email?",
|
||||
"resendEmail": "Resend confirmation email",
|
||||
"resending": "Sending...",
|
||||
"wrongEmail": "Wrong email address?",
|
||||
"backToSignup": "Back to sign up"
|
||||
},
|
||||
"verifyEmail": {
|
||||
"verifying": "Verifying your email...",
|
||||
"pleaseWait": "Please wait while we verify your email address.",
|
||||
"successTitle": "Email verified!",
|
||||
"successDescription": "Your email has been successfully verified. Welcome to Xtablo!",
|
||||
"redirecting": "You will be redirected to your account in a few seconds...",
|
||||
"goToApp": "Go to app",
|
||||
"errorTitle": "Verification failed",
|
||||
"error": "We couldn't verify your email address.",
|
||||
"noSession": "No active session found. Please try logging in.",
|
||||
"errorHelp": "The verification link may have expired or already been used. Please try signing up again or contact support.",
|
||||
"tryAgain": "Try signing up again",
|
||||
"backToLogin": "Back to login",
|
||||
"alreadyVerified": "Already verified",
|
||||
"alreadyVerifiedDescription": "Your email has already been verified. You can access your account."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,5 +70,35 @@
|
|||
"goToLogin": "Aller à la connexion",
|
||||
"error": "Une erreur est survenue lors de la mise à jour de votre mot de passe. Veuillez réessayer ou demander un nouveau lien de réinitialisation.",
|
||||
"errorTitle": "Erreur"
|
||||
},
|
||||
"confirmEmail": {
|
||||
"title": "Vérifiez votre email",
|
||||
"subtitle": "Nous vous avons envoyé un email de confirmation pour vérifier votre compte.",
|
||||
"emailSentTo": "Email envoyé à :",
|
||||
"nextSteps": "Prochaines étapes :",
|
||||
"step1": "Vérifiez votre boîte de réception (et le dossier spam)",
|
||||
"step2": "Cliquez sur le lien de confirmation dans l'email",
|
||||
"step3": "Vous serez redirigé vers votre compte",
|
||||
"didntReceive": "Vous n'avez pas reçu l'email ?",
|
||||
"resendEmail": "Renvoyer l'email de confirmation",
|
||||
"resending": "Envoi en cours...",
|
||||
"wrongEmail": "Mauvaise adresse email ?",
|
||||
"backToSignup": "Retour à l'inscription"
|
||||
},
|
||||
"verifyEmail": {
|
||||
"verifying": "Vérification de votre email...",
|
||||
"pleaseWait": "Veuillez patienter pendant que nous vérifions votre adresse email.",
|
||||
"successTitle": "Email vérifié !",
|
||||
"successDescription": "Votre email a été vérifié avec succès. Bienvenue sur Xtablo !",
|
||||
"redirecting": "Vous serez redirigé vers votre compte dans quelques secondes...",
|
||||
"goToApp": "Aller à l'application",
|
||||
"errorTitle": "Échec de la vérification",
|
||||
"error": "Nous n'avons pas pu vérifier votre adresse email.",
|
||||
"noSession": "Aucune session active trouvée. Veuillez essayer de vous connecter.",
|
||||
"errorHelp": "Le lien de vérification a peut-être expiré ou a déjà été utilisé. Veuillez réessayer de vous inscrire ou contacter le support.",
|
||||
"tryAgain": "Réessayer de s'inscrire",
|
||||
"backToLogin": "Retour à la connexion",
|
||||
"alreadyVerified": "Déjà vérifié",
|
||||
"alreadyVerifiedDescription": "Votre email a déjà été vérifié. Vous pouvez accéder à votre compte."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
apps/main/src/pages/confirm-email.tsx
Normal file
106
apps/main/src/pages/confirm-email.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { Text } from "@xtablo/ui/components/typography";
|
||||
import { CheckCircle2, Loader2Icon, MailIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useResendConfirmationEmail } from "../hooks/auth";
|
||||
|
||||
export function ConfirmEmailPage() {
|
||||
const { t } = useTranslation("auth");
|
||||
const navigate = useNavigate();
|
||||
const pendingEmail = localStorage.getItem("pendingConfirmationEmail");
|
||||
const { mutate: resendEmail, isPending } = useResendConfirmationEmail();
|
||||
|
||||
if (!pendingEmail) {
|
||||
// If no pending email, redirect to signup
|
||||
navigate("/signup");
|
||||
return;
|
||||
}
|
||||
|
||||
const handleResendEmail = () => {
|
||||
resendEmail(pendingEmail);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-primary/10 via-background to-secondary/5">
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-full max-w-lg p-8 bg-card/80 backdrop-blur-lg rounded-2xl",
|
||||
"border border-border",
|
||||
"shadow-xl"
|
||||
)}
|
||||
>
|
||||
<div className="text-center space-y-6">
|
||||
<div className="flex justify-center">
|
||||
<div className="rounded-full bg-blue-100 dark:bg-blue-900/20 p-3">
|
||||
<MailIcon className="w-12 h-12 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold text-foreground">{t("confirmEmail.title")}</h1>
|
||||
<Text className="text-muted-foreground">{t("confirmEmail.subtitle")}</Text>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-left">
|
||||
<Text className="text-sm text-blue-800 dark:text-blue-200 font-medium">
|
||||
{t("confirmEmail.emailSentTo")}
|
||||
</Text>
|
||||
<Text className="text-sm text-blue-700 dark:text-blue-300 font-mono mt-1">
|
||||
{pendingEmail}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-4 text-left space-y-3">
|
||||
<Text className="text-sm font-medium text-foreground">
|
||||
{t("confirmEmail.nextSteps")}
|
||||
</Text>
|
||||
<ol className="text-sm text-muted-foreground space-y-2 list-decimal list-inside">
|
||||
<li>{t("confirmEmail.step1")}</li>
|
||||
<li>{t("confirmEmail.step2")}</li>
|
||||
<li>{t("confirmEmail.step3")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 space-y-3">
|
||||
<Text className="text-sm text-muted-foreground">{t("confirmEmail.didntReceive")}</Text>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleResendEmail}
|
||||
disabled={isPending}
|
||||
className="w-full"
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2Icon className="w-4 h-4 mr-2 animate-spin" />
|
||||
{t("confirmEmail.resending")}
|
||||
</>
|
||||
) : (
|
||||
t("confirmEmail.resendEmail")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-border">
|
||||
<Text className="text-sm text-muted-foreground mb-3">
|
||||
{t("confirmEmail.wrongEmail")}
|
||||
</Text>
|
||||
<Link to="/signup" className="block">
|
||||
<Button variant="ghost" className="w-full">
|
||||
{t("confirmEmail.backToSignup")}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,30 +22,30 @@ describe("ResetPasswordPage", () => {
|
|||
it("renders form with email input", () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
expect(screen.getByText(/Mot de passe oublié/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/Email/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/resetPassword.title/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/resetPassword.emailLabel/i)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: /Réinitialiser le mot de passe/i })
|
||||
screen.getByRole("button", { name: /resetPassword.submit/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("displays help text", () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
expect(screen.getByText(/Entrez votre adresse email/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/resetPassword.description/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows link back to login", () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
const loginLink = screen.getByText(/Retour à la connexion/i);
|
||||
const loginLink = screen.getByText(/resetPassword.backToLogin/i);
|
||||
expect(loginLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("updates email input on change", () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText(/Email/i) as HTMLInputElement;
|
||||
const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i) as HTMLInputElement;
|
||||
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
|
||||
|
||||
expect(emailInput.value).toBe("test@example.com");
|
||||
|
|
@ -54,22 +54,22 @@ describe("ResetPasswordPage", () => {
|
|||
it.skip("submits form and shows success message", async () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText(/Email/i);
|
||||
const submitButton = screen.getByRole("button", { name: /Réinitialiser le mot de passe/i });
|
||||
const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i);
|
||||
const submitButton = screen.getByRole("button", { name: /resetPassword.submit/i });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Email envoyé/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/resetPassword.emailSent/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it.skip("displays email in success message", async () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText(/Email/i);
|
||||
const submitButton = screen.getByRole("button", { name: /Réinitialiser le mot de passe/i });
|
||||
const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i);
|
||||
const submitButton = screen.getByRole("button", { name: /resetPassword.submit/i });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
|
@ -82,28 +82,28 @@ describe("ResetPasswordPage", () => {
|
|||
it.skip("shows return to login button in success state", async () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText(/Email/i);
|
||||
const submitButton = screen.getByRole("button", { name: /Réinitialiser le mot de passe/i });
|
||||
const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i);
|
||||
const submitButton = screen.getByRole("button", { name: /resetPassword.submit/i });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "test@example.com" } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: /Retour à la connexion/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /resetPassword.backToLogin/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("requires email field to be filled", () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText(/Email/i);
|
||||
const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i);
|
||||
expect(emailInput).toHaveAttribute("required");
|
||||
});
|
||||
|
||||
it("requires valid email format", () => {
|
||||
renderWithProviders(<ResetPasswordPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText(/Email/i);
|
||||
const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i);
|
||||
expect(emailInput).toHaveAttribute("type", "email");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,59 @@ export const UserStoreProvider = ({ children }: { children: React.ReactNode }) =
|
|||
enabled: shouldFetchUser,
|
||||
});
|
||||
|
||||
// Handle email verification
|
||||
// useEffect(() => {
|
||||
// const checkEmailVerification = async () => {
|
||||
// // Check if we're coming from an email verification link
|
||||
// const hashParams = new URLSearchParams(window.location.hash.substring(1));
|
||||
// const type = hashParams.get("type");
|
||||
|
||||
// if (type === "signup" || type === "email") {
|
||||
// try {
|
||||
// const {
|
||||
// data: { user: supabaseUser },
|
||||
// } = await supabase.auth.getUser();
|
||||
|
||||
// if (supabaseUser && supabaseUser.email_confirmed_at) {
|
||||
// // Email was just verified
|
||||
// const wasJustVerified = !sessionStorage.getItem("email_verified_shown");
|
||||
|
||||
// if (wasJustVerified) {
|
||||
// // Clear the pending confirmation email
|
||||
// localStorage.removeItem("pendingConfirmationEmail");
|
||||
|
||||
// // Mark that we've shown the verification success
|
||||
// sessionStorage.setItem("email_verified_shown", "true");
|
||||
|
||||
// // Show success message
|
||||
// toast.add({
|
||||
// title: t("verifyEmail.successTitle"),
|
||||
// description: t("verifyEmail.successDescription"),
|
||||
// type: "success",
|
||||
// position: "top-center",
|
||||
// });
|
||||
|
||||
// // Clean up the URL hash
|
||||
// window.history.replaceState(null, "", window.location.pathname);
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Email verification check error:", error);
|
||||
// toast.add({
|
||||
// title: t("verifyEmail.errorTitle"),
|
||||
// description: t("verifyEmail.error"),
|
||||
// type: "error",
|
||||
// position: "top-center",
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
// if (session) {
|
||||
// checkEmailVerification();
|
||||
// }
|
||||
// }, [session, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
datadogRum.setUser({
|
||||
|
|
|
|||
Loading…
Reference in a new issue