313 lines
8.8 KiB
TypeScript
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 };
|
|
}
|