Shared login handler

This commit is contained in:
Nate Kelley 2025-09-04 13:54:46 -06:00
parent 567142c7fc
commit 40f2c1a27e
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
2 changed files with 149 additions and 113 deletions

View File

@ -12,133 +12,38 @@ import { AppTooltip } from '@/components/ui/tooltip';
import { Text, Title } from '@/components/ui/typography';
import { env } from '@/env';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import {
signInWithAzure,
signInWithEmailAndPassword,
signInWithGithub,
signInWithGoogle,
signUpWithEmailAndPassword,
} from '@/integrations/supabase/signIn';
import { cn } from '@/lib/classMerge';
import { isValidEmail } from '@/lib/email';
import { inputHasText, truncateText } from '@/lib/text';
import { PolicyCheck } from './PolicyCheck';
import { useAuthMutations } from './useAuthMutations';
import { type LastUsedReturnType, useLastUsed } from './useLastUsed';
export const LoginForm: React.FC<{
redirectTo: string | null | undefined;
isAnonymousUser?: boolean;
}> = ({ redirectTo }) => {
const navigate = useNavigate();
const lastUsedProps = useLastUsed();
const [loading, setLoading] = useState<'google' | 'github' | 'azure' | 'email' | null>(null);
const [errorMessages, setErrorMessages] = useState<string[]>([]);
const [signUpFlow, setSignUpFlow] = useState(
lastUsedProps.isAnonymousUser && !env.VITE_PUBLIC_USER
);
const [signUpSuccess, setSignUpSuccess] = useState(false);
// Reusable OAuth handler to reduce code duplication
const handleOAuthSignIn = async (
provider: 'google' | 'github' | 'azure',
signInFn: (data: {
data: { redirectTo?: string | null };
}) => Promise<{ success: boolean; url?: string; error?: string }>
) => {
setLoading(provider);
try {
const result = await signInFn({ data: { redirectTo } });
console.log(`${provider} result:`, result);
// Use the centralized auth mutations hook
const {
onSignInWithGoogle,
onSignInWithGithub,
onSignInWithAzure,
onSubmitClick,
loadingType,
errorMessages,
clearErrors,
} = useAuthMutations(redirectTo, () => setSignUpSuccess(true));
if (result && 'success' in result && !result.success) {
setErrorMessages([result.error || `An error occurred during ${provider} sign-in`]);
setLoading(null);
return;
}
if (result && 'success' in result && result.success && result.url) {
// Redirect to OAuth provider's URL
window.location.href = result.url;
}
} catch (error: unknown) {
console.error(error);
setErrorMessages(['An unexpected error occurred. Please try again.']);
setLoading(null);
}
};
const onSignInWithUsernameAndPassword = useMemoizedFn(
async ({ email, password }: { email: string; password: string }) => {
setLoading('email');
try {
const result = await signInWithEmailAndPassword({
data: { email, password, redirectUrl: redirectTo },
});
if (result?.error) {
setErrorMessages([result.message]);
setLoading(null);
} else {
navigate({ to: redirectTo || '/' });
}
} catch (error: unknown) {
console.error(error);
setErrorMessages(['An unexpected error occurred. Please try again.']);
setLoading(null);
}
}
);
const onSignInWithGoogle = useMemoizedFn(async () => {
return handleOAuthSignIn('google', signInWithGoogle);
});
const onSignInWithGithub = useMemoizedFn(async () => {
return handleOAuthSignIn('github', signInWithGithub);
});
const onSignInWithAzure = useMemoizedFn(async () => {
return handleOAuthSignIn('azure', signInWithAzure);
});
const onSignUp = useMemoizedFn(async (d: { email: string; password: string }) => {
setLoading('email');
try {
const result = await signUpWithEmailAndPassword({
data: { ...d, redirectTo },
});
if (result && 'success' in result && !result.success) {
setErrorMessages([result.error || 'An error occurred during sign-up']);
setLoading(null);
return;
}
if (result && 'success' in result && result.success) {
setSignUpSuccess(true);
setLoading(null);
// For sign-up, we don't need to navigate immediately as the user
// needs to check their email for verification
}
} catch (error: unknown) {
console.error(error);
setErrorMessages(['An unexpected error occurred. Please try again.']);
setLoading(null);
}
});
const onSubmitClick = useMemoizedFn(async (d: { email: string; password: string }) => {
try {
setErrorMessages([]);
setLoading('email');
if (signUpFlow) await onSignUp(d);
else await onSignInWithUsernameAndPassword(d);
} catch (error: unknown) {
console.error(error);
setErrorMessages(['An unexpected error occurred. Please try again.']);
setLoading(null);
}
// Wrapper for submit to handle sign up flow
const handleSubmitClick = useMemoizedFn((d: { email: string; password: string }) => {
onSubmitClick(d, signUpFlow);
});
return (
@ -148,11 +53,11 @@ export const LoginForm: React.FC<{
<SignUpSuccess setSignUpSuccess={setSignUpSuccess} setSignUpFlow={setSignUpFlow} />
) : (
<LoginOptions
onSubmitClick={onSubmitClick}
onSubmitClick={handleSubmitClick}
setSignUpFlow={setSignUpFlow}
errorMessages={errorMessages}
loading={loading}
setErrorMessages={setErrorMessages}
loading={loadingType}
setErrorMessages={clearErrors}
signUpFlow={signUpFlow}
onSignInWithGoogle={onSignInWithGoogle}
onSignInWithGithub={onSignInWithGithub}
@ -194,7 +99,7 @@ const LoginOptions: React.FC<{
const [password, setPassword] = useState(env.VITE_PUBLIC_USER_PASSWORD || '');
const [password2, setPassword2] = useState('');
const [passwordCheck, setPasswordCheck] = useState(false);
const navigate = useNavigate();
const disableSubmitButton =
!inputHasText(password) || !inputHasText(password2) || password !== password2 || !passwordCheck;

View File

@ -0,0 +1,131 @@
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import {
signInWithAzure,
signInWithEmailAndPassword,
signInWithGithub,
signInWithGoogle,
signUpWithEmailAndPassword,
} from '@/integrations/supabase/signIn';
// Reusable OAuth mutation hook
export const useOAuthMutation = (
provider: 'google' | 'github' | 'azure',
mutationFn: () => Promise<{ success: boolean; url?: string; error?: string }>
) => {
return useMutation({
mutationFn,
onSuccess: (data) => {
console.log(`${provider} OAuth result:`, data);
if (data.success && data.url) {
window.location.href = data.url;
}
},
});
};
// Combined mutation state hook
export const useCombinedMutationState = (
mutations: Array<{
isPending: boolean;
error: unknown;
reset: () => void;
name: string;
}>
) => {
const isLoading = mutations.some((m) => m.isPending);
const loadingType: 'google' | 'github' | 'azure' | 'email' | null =
(mutations.find((m) => m.isPending)?.name as 'google' | 'github' | 'azure' | 'email') || null;
const errorMessages = mutations
.filter((m) => m.error)
.map((m) => (m.error as Error)?.message || `${m.name} error`);
const clearErrors = useMemoizedFn(() => {
mutations.forEach((m) => {
m.reset();
});
});
return { isLoading, loadingType, errorMessages, clearErrors };
};
// Reusable handler generator
export const createMutationHandler = (mutation: { mutate: () => void }) =>
useMemoizedFn(() => mutation.mutate());
// Complete auth mutations hook
export const useAuthMutations = (redirectTo?: string | null, onSignUpSuccess?: () => void) => {
const navigate = useNavigate();
// OAuth Mutations
const googleSignInMutation = useOAuthMutation('google', () =>
signInWithGoogle({ data: { redirectTo } })
);
const githubSignInMutation = useOAuthMutation('github', () =>
signInWithGithub({ data: { redirectTo } })
);
const azureSignInMutation = useOAuthMutation('azure', () =>
signInWithAzure({ data: { redirectTo } })
);
// Email/Password Mutations
const emailSignInMutation = useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
signInWithEmailAndPassword({ data: { email, password, redirectUrl: redirectTo } }),
onSuccess: (data) => {
if (!data.error) {
navigate({ to: redirectTo || '/' });
}
},
});
const emailSignUpMutation = useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
signUpWithEmailAndPassword({ data: { email, password, redirectTo } }),
onSuccess: (data) => {
if (data.success && onSignUpSuccess) {
onSignUpSuccess();
}
},
});
// Combined state
const combinedState = useCombinedMutationState([
{ ...googleSignInMutation, name: 'google' },
{ ...githubSignInMutation, name: 'github' },
{ ...azureSignInMutation, name: 'azure' },
{ ...emailSignInMutation, name: 'email' },
{ ...emailSignUpMutation, name: 'email' },
]);
// Handlers
const handlers = {
onSignInWithGoogle: createMutationHandler(googleSignInMutation),
onSignInWithGithub: createMutationHandler(githubSignInMutation),
onSignInWithAzure: createMutationHandler(azureSignInMutation),
onSubmitClick: useMemoizedFn(
async (d: { email: string; password: string }, isSignUp: boolean) => {
if (isSignUp) {
emailSignUpMutation.mutate(d);
} else {
emailSignInMutation.mutate(d);
}
}
),
};
return {
mutations: {
googleSignInMutation,
githubSignInMutation,
azureSignInMutation,
emailSignInMutation,
emailSignUpMutation,
},
...combinedState,
...handlers,
};
};