B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,82 @@
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import type { AuthChangeEvent, Session } from '@supabase/supabase-js';
import { useSupabase } from './use-supabase';
/**
* @name PRIVATE_PATH_PREFIXES
* @description A list of private path prefixes
*/
const PRIVATE_PATH_PREFIXES = ['/home', '/admin', '/join', '/update-password'];
/**
* @name AUTH_PATHS
* @description A list of auth paths
*/
const AUTH_PATHS = ['/auth'];
/**
* @name useAuthChangeListener
* @param privatePathPrefixes - A list of private path prefixes
* @param appHomePath - The path to redirect to when the user is signed out
* @param onEvent - Callback function to be called when an auth event occurs
*/
export function useAuthChangeListener({
privatePathPrefixes = PRIVATE_PATH_PREFIXES,
appHomePath,
onEvent,
}: {
appHomePath: string;
privatePathPrefixes?: string[];
onEvent?: (event: AuthChangeEvent, user: Session | null) => void;
}) {
const client = useSupabase();
const pathName = usePathname();
useEffect(() => {
// keep this running for the whole session unless the component was unmounted
const listener = client.auth.onAuthStateChange((event, user) => {
if (onEvent) {
onEvent(event, user);
}
// log user out if user is falsy
// and if the current path is a private route
const shouldRedirectUser =
!user && isPrivateRoute(pathName, privatePathPrefixes);
if (shouldRedirectUser) {
// send user away when signed out
window.location.assign('/');
return;
}
// revalidate user session when user signs in or out
if (event === 'SIGNED_OUT') {
// sometimes Supabase sends SIGNED_OUT event
// but in the auth path, so we ignore it
if (AUTH_PATHS.some((path) => pathName.startsWith(path))) {
return;
}
window.location.reload();
}
});
// destroy listener on un-mounts
return () => listener.data.subscription.unsubscribe();
}, [client.auth, pathName, appHomePath, privatePathPrefixes, onEvent]);
}
/**
* Determines if a given path is a private route.
*/
function isPrivateRoute(path: string, privatePathPrefixes: string[]) {
return privatePathPrefixes.some((prefix) => path.startsWith(prefix));
}

View File

@@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
import { useFactorsMutationKey } from './use-user-factors-mutation-key';
export function useFetchAuthFactors(userId: string) {
const client = useSupabase();
const queryKey = useFactorsMutationKey(userId);
const queryFn = async () => {
const { data, error } = await client.auth.mfa.listFactors();
if (error) {
throw error;
}
return data;
};
return useQuery({
queryKey,
queryFn,
staleTime: 0,
});
}

View File

@@ -0,0 +1,42 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
interface RequestPasswordResetMutationParams {
email: string;
redirectTo: string;
captchaToken?: string;
}
/**
* @name useRequestResetPassword
* @description Requests a password reset for a user. This function will
* trigger a password reset email to be sent to the user's email address.
* After the user clicks the link in the email, they will be redirected to
* /password-reset where their password can be updated.
*/
export function useRequestResetPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'reset-password'];
const mutationFn = async (params: RequestPasswordResetMutationParams) => {
const { error, data } = await client.auth.resetPasswordForEmail(
params.email,
{
redirectTo: params.redirectTo,
captchaToken: params.captchaToken,
},
);
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
});
}

View File

@@ -0,0 +1,30 @@
import type { SignInWithPasswordCredentials } from '@supabase/supabase-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignInWithEmailPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-in-with-email-password'];
const mutationFn = async (credentials: SignInWithPasswordCredentials) => {
const response = await client.auth.signInWithPassword(credentials);
if (response.error) {
throw response.error.message;
}
const user = response.data?.user;
const identities = user?.identities ?? [];
// if the user has no identities, it means that the email is taken
if (identities.length === 0) {
throw new Error('User already registered');
}
return response.data;
};
return useMutation({ mutationKey, mutationFn });
}

View File

@@ -0,0 +1,43 @@
import type { SignInWithPasswordlessCredentials } from '@supabase/supabase-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignInWithOtp() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-in-with-otp'];
const mutationFn = async (credentials: SignInWithPasswordlessCredentials) => {
const result = await client.auth.signInWithOtp(credentials);
if (result.error) {
if (shouldIgnoreError(result.error.message)) {
console.warn(
`Ignoring error during development: ${result.error.message}`,
);
return {} as never;
}
throw result.error.message;
}
return result.data;
};
return useMutation({
mutationFn,
mutationKey,
});
}
export default useSignInWithOtp;
function shouldIgnoreError(error: string) {
return isSmsProviderNotSetupError(error);
}
function isSmsProviderNotSetupError(error: string) {
return error.includes(`sms Provider could not be found`);
}

View File

@@ -0,0 +1,25 @@
import type { SignInWithOAuthCredentials } from '@supabase/supabase-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignInWithProvider() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-in-with-provider'];
const mutationFn = async (credentials: SignInWithOAuthCredentials) => {
const response = await client.auth.signInWithOAuth(credentials);
if (response.error) {
throw response.error.message;
}
return response.data;
};
return useMutation({
mutationFn,
mutationKey,
});
}

View File

@@ -0,0 +1,13 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignOut() {
const client = useSupabase();
return useMutation({
mutationFn: () => {
return client.auth.signOut();
},
});
}

View File

@@ -0,0 +1,46 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
interface Credentials {
email: string;
password: string;
emailRedirectTo: string;
captchaToken?: string;
}
export function useSignUpWithEmailAndPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-up-with-email-password'];
const mutationFn = async (params: Credentials) => {
const { emailRedirectTo, captchaToken, ...credentials } = params;
const response = await client.auth.signUp({
...credentials,
options: {
emailRedirectTo,
captchaToken,
},
});
if (response.error) {
throw response.error.message;
}
const user = response.data?.user;
const identities = user?.identities ?? [];
// if the user has no identities, it means that the email is taken
if (identities.length === 0) {
throw new Error('User already registered');
}
return response.data;
};
return useMutation({
mutationKey,
mutationFn,
});
}

View File

@@ -0,0 +1,8 @@
import { useMemo } from 'react';
import { getSupabaseBrowserClient } from '../clients/browser-client';
import { Database } from '../database.types';
export function useSupabase<Db = Database>() {
return useMemo(() => getSupabaseBrowserClient<Db>(), []);
}

View File

@@ -0,0 +1,31 @@
import type { UserAttributes } from '@supabase/supabase-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
type Params = UserAttributes & { redirectTo: string };
export function useUpdateUser() {
const client = useSupabase();
const mutationKey = ['supabase:user'];
const mutationFn = async (attributes: Params) => {
const { redirectTo, ...params } = attributes;
const response = await client.auth.updateUser(params, {
emailRedirectTo: redirectTo,
});
if (response.error) {
throw response.error;
}
return response.data;
};
return useMutation({
mutationKey,
mutationFn,
});
}

View File

@@ -0,0 +1,3 @@
export function useFactorsMutationKey(userId: string) {
return ['mfa-factors', userId];
}

View File

@@ -0,0 +1,35 @@
import type { User } from '@supabase/supabase-js';
import { useQuery } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
const queryKey = ['supabase:user'];
export function useUser(initialData?: User | null) {
const client = useSupabase();
const queryFn = async () => {
const response = await client.auth.getUser();
// this is most likely a session error or the user is not logged in
if (response.error) {
return undefined;
}
if (response.data?.user) {
return response.data.user;
}
return Promise.reject(new Error('Unexpected result format'));
};
return useQuery({
queryFn,
queryKey,
initialData,
refetchInterval: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
}

View File

@@ -0,0 +1,26 @@
import type { VerifyOtpParams } from '@supabase/supabase-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useVerifyOtp() {
const client = useSupabase();
const mutationKey = ['verify-otp'];
const mutationFn = async (params: VerifyOtpParams) => {
const { data, error } = await client.auth.verifyOtp(params);
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
});
}