xtablo-source/apps/main/src/hooks/auth.ts
Arthur Belleville e8044182d8
fix: resolve lint and formatting issues in apps/main
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:33:38 +02:00

313 lines
8.8 KiB
TypeScript

import { Session, User as SupabaseUser } from "@supabase/supabase-js";
import { useMutation } from "@tanstack/react-query";
import { queryClient, toast, useSession } from "@xtablo/shared";
import { AxiosInstance } from "axios";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { match } from "ts-pattern";
import { api } from "../lib/api";
import {
DEFAULT_SIGNUP_BILLING_INTENT,
PENDING_BILLING_CHECKOUT_PLAN_KEY,
SIGNUP_BILLING_INTENT_KEY,
SignupBillingIntent,
} from "../lib/billing";
import { supabase } from "../lib/supabase";
import { clearOrgIdCookie } from "./organization";
export type User = SupabaseUser & {
user_metadata: {
email: string;
email_verified: boolean;
first_name: string;
last_name: string;
business_name: string;
};
};
interface SignUpData {
email: string;
password: string;
confirm_password: string;
first_name: string;
last_name: string;
business_name: string;
billing_intent?: SignupBillingIntent;
}
interface LoginData {
email: string;
password: string;
}
interface AuthResponse {
user: SupabaseUser | null;
session: Session | null;
}
export const resolveSignupBillingIntent = (
billingIntent?: SignupBillingIntent
): SignupBillingIntent => billingIntent ?? DEFAULT_SIGNUP_BILLING_INTENT;
export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string>>({});
const { mutate, isPending } = useMutation<
AuthResponse,
{ message: string; code: string },
SignUpData
>({
mutationFn: async (data: SignUpData) => {
const { data: response, error } = await supabase.auth.signUp({
email: data.email,
password: data.password,
options: {
emailRedirectTo: `${window.location.origin}/`,
data: {
first_name: data.first_name,
last_name: data.last_name,
business_name: data.business_name,
billing_intent: resolveSignupBillingIntent(data.billing_intent),
},
},
});
if (error) throw error;
return response;
},
onSuccess: async (data, variables) => {
const selectedPlan = resolveSignupBillingIntent(variables.billing_intent);
localStorage.setItem(SIGNUP_BILLING_INTENT_KEY, selectedPlan);
localStorage.setItem(PENDING_BILLING_CHECKOUT_PLAN_KEY, selectedPlan);
// 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");
return;
}
if (data.session?.access_token) {
try {
const checkoutResponse = await api.post(
"/api/v1/stripe/create-checkout-session",
{
plan: selectedPlan,
successUrl: `${window.location.origin}/settings?success=true`,
cancelUrl: `${window.location.origin}/settings?canceled=true`,
},
{
headers: {
Authorization: `Bearer ${data.session.access_token}`,
},
}
);
if (checkoutResponse.data?.url) {
localStorage.removeItem(SIGNUP_BILLING_INTENT_KEY);
localStorage.removeItem(PENDING_BILLING_CHECKOUT_PLAN_KEY);
window.location.href = checkoutResponse.data.url;
return;
}
} catch (error) {
console.error("Signup checkout bootstrap failed after signup:", error);
toast.add({
title: "Paiement",
description:
"Votre compte est créé, mais la redirection vers le paiement a échoué. Vous pourrez finaliser le paiement dans l'application.",
type: "warning",
position: "top-center",
});
}
}
localStorage.removeItem(SIGNUP_BILLING_INTENT_KEY);
if (redirectUrl) {
localStorage.removeItem("redirectUrl");
navigate(decodeURIComponent(redirectUrl));
return;
}
navigate("/");
},
onError: (error) => {
const errMap: Record<string, string> = {};
match(error.code)
.with("user_already_exists", () => {
errMap["email"] = "Cette adresse email est déjà utilisée";
})
.otherwise(() => {
toast.add(
{
title: "Erreur",
description: error.message,
type: "error",
position: "top-left",
},
{
timeout: 5000,
}
);
});
setErrors(errMap);
},
});
return { mutate, isPending, errors };
}
export function useLoginEmail({ redirectUrl }: { redirectUrl: string | null }) {
const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string>>({});
const { mutate, isPending } = useMutation<
AuthResponse,
{ message: string; code: string },
LoginData
>({
mutationFn: async (data: LoginData) => {
const { data: response, error } = await supabase.auth.signInWithPassword({
email: data.email.trim(),
password: data.password.trim(),
});
if (error) throw error;
return response;
},
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));
} else {
navigate("/");
}
},
onError: (error) => {
match(error.code)
.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(
{
title: "Erreur",
description: error.message,
type: "error",
position: "top-left",
},
{
timeout: 5000,
}
);
});
},
});
return { mutate, isPending, errors };
}
export function useLoginGoogle() {
const { mutate } = useMutation({
mutationFn: async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${window.location.origin}/login-with-oauth`,
},
});
if (error) throw error;
return data;
},
});
return { loginWithGoogle: mutate };
}
export function useLogout() {
return useMutation({
mutationFn: async () => {
const { error } = await supabase.auth.signOut();
if (error) throw error;
clearOrgIdCookie();
queryClient.removeQueries();
},
onSuccess: () => {
window.location.href = "/login";
},
onError: (error) => {
toast.add({
title: "Erreur",
description: error.message,
type: "error",
});
},
});
}
export const useAuthedApi = (): AxiosInstance => {
const { session } = useSession();
return api.create({
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
});
};
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 };
}