Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/shared/src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import classNames from 'classnames';
import type { Dispatch, FormEvent, ReactElement, SetStateAction } from 'react';
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import type { TurnstileInstance } from '@marsidev/react-turnstile';
import { Turnstile } from '@marsidev/react-turnstile';
import type { LoginPasswordParameters } from '../../lib/auth';
import { AuthEventNames } from '../../lib/auth';
import { Origin } from '../../lib/log';
import { formToJson } from '../../lib/form';
import { Button, ButtonVariant } from '../buttons/Button';
import { ClickableText } from '../buttons/ClickableText';
Expand All @@ -12,6 +16,7 @@ import AuthForm from './AuthForm';
import { IconSize } from '../Icon';
import Alert, { AlertParagraph, AlertType } from '../widgets/Alert';
import { labels } from '../../lib';
import { useLogContext } from '../../contexts/LogContext';

export interface LoginFormProps {
onForgotPassword?: (email: string) => unknown;
Expand All @@ -34,7 +39,9 @@ export type LoginHintState = [
export type LoginFormParams = Pick<
LoginPasswordParameters,
'identifier' | 'password'
>;
> & {
turnstileToken?: string;
};

function LoginForm({
onForgotPassword,
Expand All @@ -48,14 +55,39 @@ function LoginForm({
autoFocus = true,
onSignup,
}: LoginFormProps): ReactElement {
const { logEvent } = useLogContext();
const turnstileRef = useRef<TurnstileInstance>(null);
const [turnstileLoaded, setTurnstileLoaded] = useState(false);
const [turnstileError, setTurnstileError] = useState(false);
const turnstileSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_KEY ?? '';

useEffect(() => {
if (hint) {
turnstileRef.current?.reset();
}
}, [hint]);

const onLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();

if (!onPasswordLogin) {
return;
}

if (turnstileSiteKey && !turnstileRef.current?.getResponse()) {
logEvent({
event_name: AuthEventNames.LoginError,
extra: JSON.stringify({
error: 'Turnstile not valid',
origin: Origin.LoginTurnstile,
}),
});
setTurnstileError(true);
return;
}

const form = formToJson<LoginFormParams>(e.currentTarget);
form.turnstileToken = turnstileRef.current?.getResponse() ?? undefined;
onPasswordLogin(form);
};
const [shouldFocus, setShouldFocus] = useState(autoFocus);
Expand Down Expand Up @@ -109,6 +141,21 @@ function LoginForm({
saveHintSpace
onChange={() => hint && setHint(null)}
/>
{turnstileSiteKey && (
<Turnstile
ref={turnstileRef}
siteKey={turnstileSiteKey}
options={{ theme: 'dark' }}
className="mx-auto min-h-[4.5rem]"
onWidgetLoad={() => setTurnstileLoaded(true)}
/>
)}
{turnstileError && (
<Alert
type={AlertType.Error}
title="Please complete the security check."
/>
)}
<span className="mt-4 flex w-full flex-row">
{onForgotPassword && (
<ClickableText
Expand All @@ -124,7 +171,7 @@ function LoginForm({
variant={ButtonVariant.Primary}
type="submit"
loading={!isReady}
disabled={isLoading}
disabled={isLoading || (!!turnstileSiteKey && !turnstileLoaded)}
>
{loginButton}
</Button>
Expand Down
19 changes: 11 additions & 8 deletions packages/shared/src/hooks/useLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type { SignBackProvider } from './auth/useSignBack';
import { useSignBack } from './auth/useSignBack';
import type { LoggedUser } from '../lib/user';
import { labels } from '../lib';
import { Origin } from '../lib/log';
import { useEventListener } from './useEventListener';
import { broadcastChannel, webappUrl } from '../lib/constants';
import { isIOSNative } from '../lib/func';
Expand Down Expand Up @@ -69,6 +70,7 @@ const useLogin = ({
return betterAuthSignIn({
email: form.identifier,
password: form.password,
turnstileToken: form.turnstileToken,
});
},
onSuccess: async (res) => {
Expand All @@ -78,7 +80,8 @@ const useLogin = ({
extra: JSON.stringify({
error: res.error,
displayedError: labels.auth.error.invalidEmailOrPassword,
origin: 'betterauth email login',
origin: Origin.BetterAuthEmailLogin,
userAgent: navigator.userAgent,
}),
});
setHint(labels.auth.error.invalidEmailOrPassword);
Expand All @@ -93,7 +96,7 @@ const useLogin = ({
event_name: AuthEventNames.LoginError,
extra: JSON.stringify({
error: 'Missing user after Better Auth email login',
origin: 'betterauth email login boot',
origin: Origin.BetterAuthEmailLoginBoot,
}),
});
setHint(labels.auth.error.generic);
Expand All @@ -113,7 +116,7 @@ const useLogin = ({
error,
'Failed to refresh Better Auth login state',
),
origin: 'betterauth email login boot',
origin: Origin.BetterAuthEmailLoginBoot,
}),
});
setHint(labels.auth.error.generic);
Expand All @@ -138,7 +141,7 @@ const useLogin = ({
event_name: AuthEventNames.LoginError,
extra: JSON.stringify({
error: result.error,
origin: 'betterauth native id token',
origin: Origin.BetterAuthNativeIdToken,
}),
});
return;
Expand All @@ -150,7 +153,7 @@ const useLogin = ({
event_name: AuthEventNames.LoginError,
extra: JSON.stringify({
error: 'Missing user after Better Auth social login',
origin: 'betterauth native id token boot',
origin: Origin.BetterAuthNativeIdTokenBoot,
}),
});
displayToast(labels.auth.error.generic);
Expand All @@ -168,7 +171,7 @@ const useLogin = ({
error,
'Failed to refresh Better Auth social login state',
),
origin: 'betterauth native id token boot',
origin: Origin.BetterAuthNativeIdTokenBoot,
}),
});
displayToast(labels.auth.error.generic);
Expand All @@ -187,7 +190,7 @@ const useLogin = ({
event_name: AuthEventNames.LoginError,
extra: JSON.stringify({
error: error || 'Failed to get social login URL',
origin: 'betterauth social url',
origin: Origin.BetterAuthSocialUrl,
}),
});
socialPopup?.close();
Expand All @@ -206,7 +209,7 @@ const useLogin = ({
event_name: AuthEventNames.LoginError,
extra: JSON.stringify({
error: 'Failed to open social login window',
origin: 'betterauth social popup',
origin: Origin.BetterAuthSocialPopup,
}),
});
displayToast(labels.auth.error.generic);
Expand Down
13 changes: 12 additions & 1 deletion packages/shared/src/lib/betterAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,22 @@ const betterAuthPost = async <T = Record<string, unknown>>(
export const betterAuthSignIn = async ({
email,
password,
turnstileToken,
}: {
email: string;
password: string;
turnstileToken?: string;
}): Promise<BetterAuthResponse> => {
return betterAuthPost('sign-in/email', { email, password }, 'Sign in failed');
const headers: Record<string, string> = {};
if (turnstileToken) {
headers['x-captcha-response'] = turnstileToken;
}
return betterAuthPost(
'sign-in/email',
{ email, password },
'Sign in failed',
Object.keys(headers).length > 0 ? headers : undefined,
);
};

export const betterAuthSignUp = async ({
Expand Down
8 changes: 8 additions & 0 deletions packages/shared/src/lib/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ export enum Origin {
// Onboarding v2
OnboardingModal = 'onboarding modal',
OnboardingFeedEnd = 'onboarding feed end',
// Auth
BetterAuthEmailLogin = 'betterauth email login',
BetterAuthEmailLoginBoot = 'betterauth email login boot',
BetterAuthNativeIdToken = 'betterauth native id token',
BetterAuthNativeIdTokenBoot = 'betterauth native id token boot',
BetterAuthSocialUrl = 'betterauth social url',
BetterAuthSocialPopup = 'betterauth social popup',
LoginTurnstile = 'login turnstile',
}

export enum LogEvent {
Expand Down
Loading