diff --git a/apps/main/src/hooks/auth.ts b/apps/main/src/hooks/auth.ts
index 66735a0..8e909e3 100644
--- a/apps/main/src/hooks/auth.ts
+++ b/apps/main/src/hooks/auth.ts
@@ -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 };
+}
diff --git a/apps/main/src/lib/routes.tsx b/apps/main/src/lib/routes.tsx
index 1707c52..211f18a 100644
--- a/apps/main/src/lib/routes.tsx
+++ b/apps/main/src/lib/routes.tsx
@@ -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: ,
},
+ // Email confirmation waiting page (public)
+ {
+ path: "/confirm-email",
+ element: ,
+ },
// Authentication pages (redirected to "/" if user is authenticated)
{
path: "/",
diff --git a/apps/main/src/locales/en/auth.json b/apps/main/src/locales/en/auth.json
index f3dca44..2931e11 100644
--- a/apps/main/src/locales/en/auth.json
+++ b/apps/main/src/locales/en/auth.json
@@ -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."
}
}
diff --git a/apps/main/src/locales/fr/auth.json b/apps/main/src/locales/fr/auth.json
index 548c2c9..6cf4069 100644
--- a/apps/main/src/locales/fr/auth.json
+++ b/apps/main/src/locales/fr/auth.json
@@ -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."
}
}
diff --git a/apps/main/src/pages/confirm-email.tsx b/apps/main/src/pages/confirm-email.tsx
new file mode 100644
index 0000000..3f362a9
--- /dev/null
+++ b/apps/main/src/pages/confirm-email.tsx
@@ -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 (
+
+
+
+
+
+
+
{t("confirmEmail.title")}
+ {t("confirmEmail.subtitle")}
+
+
+
+
+
+
+
+
+ {t("confirmEmail.emailSentTo")}
+
+
+ {pendingEmail}
+
+
+
+
+
+
+
+
+ {t("confirmEmail.nextSteps")}
+
+
+ - {t("confirmEmail.step1")}
+ - {t("confirmEmail.step2")}
+ - {t("confirmEmail.step3")}
+
+
+
+
+ {t("confirmEmail.didntReceive")}
+
+
+
+
+
+ {t("confirmEmail.wrongEmail")}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/main/src/pages/reset-password.test.tsx b/apps/main/src/pages/reset-password.test.tsx
index 67f5609..08fe1fb 100644
--- a/apps/main/src/pages/reset-password.test.tsx
+++ b/apps/main/src/pages/reset-password.test.tsx
@@ -22,30 +22,30 @@ describe("ResetPasswordPage", () => {
it("renders form with email input", () => {
renderWithProviders();
- 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();
- expect(screen.getByText(/Entrez votre adresse email/i)).toBeInTheDocument();
+ expect(screen.getByText(/resetPassword.description/i)).toBeInTheDocument();
});
it("shows link back to login", () => {
renderWithProviders();
- 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();
- 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();
- 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();
- 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();
- 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();
- const emailInput = screen.getByLabelText(/Email/i);
+ const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i);
expect(emailInput).toHaveAttribute("required");
});
it("requires valid email format", () => {
renderWithProviders();
- const emailInput = screen.getByLabelText(/Email/i);
+ const emailInput = screen.getByLabelText(/resetPassword.emailLabel/i);
expect(emailInput).toHaveAttribute("type", "email");
});
});
diff --git a/apps/main/src/providers/UserStoreProvider.tsx b/apps/main/src/providers/UserStoreProvider.tsx
index 8e68824..3e36fae 100644
--- a/apps/main/src/providers/UserStoreProvider.tsx
+++ b/apps/main/src/providers/UserStoreProvider.tsx
@@ -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({