feat(auth): integrate captcha validation and component for sign-in/up… (#15054)
… process Added captcha token validation to the sign-in/up flow. Introduced `StyledLoaderContainer` for better UI feedback during loading. Enhanced error handling within `useSignInUp` to handle GraphQL errors effectively. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
co-authored by
Félix Malfait
Félix Malfait
parent
06a195af41
commit
4220bba69b
@@ -42,6 +42,7 @@ import {
|
||||
getFirstAvailableWorkspaces,
|
||||
} from '@/auth/utils/availableWorkspacesUtils';
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { captchaState } from '@/client-config/states/captchaState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
@@ -65,7 +66,6 @@ import { type AuthToken } from '~/generated/graphql';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { loginTokenState } from '../states/loginTokenState';
|
||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||
|
||||
export const useAuth = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
|
||||
+11
-1
@@ -12,6 +12,7 @@ import {
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { OTPInput, type SlotProps } from 'input-otp';
|
||||
@@ -184,6 +185,7 @@ export const SignInUpTOTPVerification = () => {
|
||||
|
||||
const navigate = useNavigateApp();
|
||||
const { readCaptchaToken } = useReadCaptchaToken();
|
||||
const { isCaptchaReady } = useCaptcha();
|
||||
const loginToken = useRecoilValue(loginTokenState);
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const { t } = useLingui();
|
||||
@@ -193,7 +195,15 @@ export const SignInUpTOTPVerification = () => {
|
||||
const submitOTP = async (values: OTPFormValues) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const captchaToken = await readCaptchaToken();
|
||||
if (!isCaptchaReady) {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Captcha (anti-bot check) is still loading, try again`,
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const captchaToken = readCaptchaToken();
|
||||
|
||||
if (!loginToken) {
|
||||
return navigate(AppPath.SignInUp);
|
||||
|
||||
@@ -10,16 +10,18 @@ import {
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { SignInUpMode } from '@/auth/types/signInUpMode';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
|
||||
import { useBuildSearchParamsFromUrlSyncedStates } from '@/domain-manager/hooks/useBuildSearchParamsFromUrlSyncedStates';
|
||||
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
|
||||
import { isMatchingLocation } from '~/utils/isMatchingLocation';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
@@ -28,6 +30,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
|
||||
const [signInUpMode, setSignInUpMode] = useRecoilState(signInUpModeState);
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
const { isCaptchaReady } = useCaptcha();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
@@ -59,49 +62,65 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
|
||||
const errorMsgUserAlreadyExist = t`An error occurred while checking user existence`;
|
||||
const continueWithCredentials = useCallback(async () => {
|
||||
const token = await readCaptchaToken();
|
||||
if (!form.getValues('email')) {
|
||||
return;
|
||||
return enqueueErrorSnackBar({
|
||||
message: t`Email is required`,
|
||||
});
|
||||
}
|
||||
if (!isCaptchaReady) {
|
||||
return enqueueErrorSnackBar({
|
||||
message: t`Captcha (anti-bot check) is still loading, try again`,
|
||||
});
|
||||
}
|
||||
try {
|
||||
const { data } = await checkUserExistsQuery({
|
||||
const token = readCaptchaToken();
|
||||
|
||||
const { data, error } = await checkUserExistsQuery({
|
||||
variables: {
|
||||
email: form.getValues('email').toLowerCase().trim(),
|
||||
captchaToken: token,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(error)) {
|
||||
return enqueueErrorSnackBar({ apolloError: error });
|
||||
}
|
||||
|
||||
setSignInUpMode(
|
||||
data?.checkUserExists.exists
|
||||
? SignInUpMode.SignIn
|
||||
: SignInUpMode.SignUp,
|
||||
);
|
||||
setSignInUpStep(SignInUpStep.Password);
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
...(error instanceof ApolloError
|
||||
? { apolloError: error }
|
||||
: { message: errorMsgUserAlreadyExist }),
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({ message: errorMsgUserAlreadyExist });
|
||||
}
|
||||
}, [
|
||||
readCaptchaToken,
|
||||
form,
|
||||
checkUserExistsQuery,
|
||||
isCaptchaReady,
|
||||
enqueueErrorSnackBar,
|
||||
setSignInUpStep,
|
||||
t,
|
||||
checkUserExistsQuery,
|
||||
setSignInUpMode,
|
||||
setSignInUpStep,
|
||||
errorMsgUserAlreadyExist,
|
||||
]);
|
||||
|
||||
const submitCredentials: SubmitHandler<Form> = useCallback(
|
||||
async (data) => {
|
||||
const token = await readCaptchaToken();
|
||||
try {
|
||||
if (!data.email || !data.password) {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
if (!data.email || !data.password) {
|
||||
throw new Error('Email and password are required');
|
||||
}
|
||||
|
||||
if (!isCaptchaReady) {
|
||||
return enqueueErrorSnackBar({
|
||||
message: t`Captcha (anti-bot check) is still loading, try again`,
|
||||
});
|
||||
}
|
||||
|
||||
const token = readCaptchaToken();
|
||||
try {
|
||||
if (
|
||||
!isInviteMode &&
|
||||
signInUpMode === SignInUpMode.SignIn &&
|
||||
@@ -158,6 +177,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
}
|
||||
},
|
||||
[
|
||||
isCaptchaReady,
|
||||
readCaptchaToken,
|
||||
signInUpMode,
|
||||
isInviteMode,
|
||||
@@ -170,6 +190,7 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||
enqueueErrorSnackBar,
|
||||
buildSearchParamsFromUrlSyncedStates,
|
||||
isOnAWorkspace,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
+17
-7
@@ -1,20 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
|
||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||
import { getCaptchaUrlByProvider } from '@/captcha/utils/getCaptchaUrlByProvider';
|
||||
import { captchaState } from '@/client-config/states/captchaState';
|
||||
import { CaptchaDriverType } from '~/generated/graphql';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
||||
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
|
||||
import { captchaState } from '@/client-config/states/captchaState';
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
import { CaptchaDriverType } from '~/generated/graphql';
|
||||
|
||||
export const CaptchaProviderScriptLoaderEffect = () => {
|
||||
const captcha = useRecoilValue(captchaState);
|
||||
const [isCaptchaScriptLoaded, setIsCaptchaScriptLoaded] = useRecoilState(
|
||||
const setIsCaptchaScriptLoaded = useSetRecoilState(
|
||||
isCaptchaScriptLoadedState,
|
||||
);
|
||||
const { isCaptchaScriptLoaded, isCaptchaConfigured } = useCaptcha();
|
||||
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -60,10 +62,12 @@ export const CaptchaProviderScriptLoaderEffect = () => {
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUndefinedOrNull(captcha?.provider) || !isCaptchaScriptLoaded) {
|
||||
if (!isCaptchaConfigured || !isCaptchaScriptLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
assertIsDefinedOrThrow(captcha);
|
||||
|
||||
let refreshInterval: NodeJS.Timeout;
|
||||
|
||||
switch (captcha.provider) {
|
||||
@@ -81,7 +85,13 @@ export const CaptchaProviderScriptLoaderEffect = () => {
|
||||
}
|
||||
|
||||
return () => clearInterval(refreshInterval);
|
||||
}, [captcha?.provider, requestFreshCaptchaToken, isCaptchaScriptLoaded]);
|
||||
}, [
|
||||
captcha,
|
||||
captcha?.provider,
|
||||
isCaptchaConfigured,
|
||||
isCaptchaScriptLoaded,
|
||||
requestFreshCaptchaToken,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
export const useReadCaptchaToken = () => {
|
||||
const readCaptchaToken = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
() => {
|
||||
const existingCaptchaToken = snapshot
|
||||
.getLoadable(captchaTokenState)
|
||||
.getValue();
|
||||
|
||||
@@ -4,8 +4,8 @@ import { captchaTokenState } from '@/captcha/states/captchaTokenState';
|
||||
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
|
||||
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
|
||||
import { captchaState } from '@/client-config/states/captchaState';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import { CaptchaDriverType } from '~/generated-metadata/graphql';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const useRequestFreshCaptchaToken = () => {
|
||||
const setCaptchaToken = useSetRecoilState(captchaTokenState);
|
||||
@@ -22,14 +22,15 @@ export const useRequestFreshCaptchaToken = () => {
|
||||
|
||||
const captcha = snapshot.getLoadable(captchaState).getValue();
|
||||
|
||||
if (isUndefinedOrNull(captcha?.provider)) {
|
||||
if (!isDefined(captcha)) {
|
||||
return;
|
||||
}
|
||||
|
||||
assertIsDefinedOrThrow(captcha);
|
||||
|
||||
setIsRequestingCaptchaToken(true);
|
||||
|
||||
let captchaWidget: any;
|
||||
|
||||
switch (captcha.provider) {
|
||||
case CaptchaDriverType.GOOGLE_RECAPTCHA:
|
||||
window.grecaptcha
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
|
||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||
import { captchaState } from '@/client-config/states/captchaState';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const useCaptcha = () => {
|
||||
const captcha = useRecoilValue(captchaState);
|
||||
const captchaToken = useRecoilValue(captchaTokenState);
|
||||
const clientConfigApiStatus = useRecoilValue(clientConfigApiStatusState);
|
||||
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
|
||||
|
||||
const isClientConfigLoaded = clientConfigApiStatus.isLoadedOnce;
|
||||
const isSiteKeyDefined = isDefined(captcha?.siteKey);
|
||||
const isTokenAvailable = isDefined(captchaToken);
|
||||
|
||||
// Captcha is ready when:
|
||||
// - Client config is loaded
|
||||
// - And either captcha is not configured with a site key (no captcha required)
|
||||
// - Or, when configured, a captcha token is available
|
||||
const isCaptchaReady =
|
||||
isClientConfigLoaded && (!isSiteKeyDefined || isTokenAvailable);
|
||||
|
||||
return {
|
||||
isCaptchaScriptLoaded,
|
||||
isCaptchaConfigured: !isUndefinedOrNull(captcha),
|
||||
isCaptchaReady,
|
||||
};
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
|
||||
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
|
||||
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
@@ -126,6 +127,7 @@ export const PasswordReset = () => {
|
||||
|
||||
const { signInWithCredentialsInWorkspace } = useAuth();
|
||||
const { readCaptchaToken } = useReadCaptchaToken();
|
||||
const { isCaptchaReady } = useCaptcha();
|
||||
|
||||
const onSubmit = async (formData: Form) => {
|
||||
try {
|
||||
@@ -151,7 +153,14 @@ export const PasswordReset = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await readCaptchaToken();
|
||||
if (!isCaptchaReady) {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Captcha (anti-bot check) is still loading, try again`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = readCaptchaToken();
|
||||
|
||||
await signInWithCredentialsInWorkspace(
|
||||
email || '',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
signInUpStepState,
|
||||
} from '@/auth/states/signInUpStepState';
|
||||
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { Logo } from '@/auth/components/Logo';
|
||||
@@ -26,13 +27,24 @@ import { SignInUpGlobalScopeFormEffect } from '@/auth/sign-in-up/components/inte
|
||||
import { SignInUpTwoFactorAuthenticationProvision } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationProvision';
|
||||
import { SignInUpTOTPVerification } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification';
|
||||
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Loader } from 'twenty-ui/feedback';
|
||||
import { AnimatedEaseIn } from 'twenty-ui/utilities';
|
||||
import { type PublicWorkspaceDataOutput } from '~/generated/graphql';
|
||||
|
||||
const StyledLoaderContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||
width: 100%;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StandardContent = ({
|
||||
workspacePublicData,
|
||||
signInUpForm,
|
||||
@@ -70,13 +82,15 @@ const StandardContent = ({
|
||||
export const SignInUp = () => {
|
||||
const { t } = useLingui();
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const clientConfigApiStatus = useRecoilValue(clientConfigApiStatusState);
|
||||
|
||||
const { form } = useSignInUpForm();
|
||||
const { signInUpStep } = useSignInUp(form);
|
||||
const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
|
||||
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
|
||||
const workspacePublicData = useRecoilValue(workspacePublicDataState);
|
||||
const { loading } = useGetPublicWorkspaceDataByDomain();
|
||||
const { loading: getPublicWorkspaceDataLoading } =
|
||||
useGetPublicWorkspaceDataByDomain();
|
||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||
const { workspaceInviteHash, workspace: workspaceFromInviteHash } =
|
||||
useWorkspaceFromInviteHash();
|
||||
@@ -121,7 +135,13 @@ export const SignInUp = () => {
|
||||
]);
|
||||
|
||||
const signInUpForm = useMemo(() => {
|
||||
if (loading) return null;
|
||||
if (getPublicWorkspaceDataLoading || !clientConfigApiStatus.isLoadedOnce) {
|
||||
return (
|
||||
<StyledLoaderContainer>
|
||||
<Loader color="gray" />
|
||||
</StyledLoaderContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefaultDomain && isMultiWorkspaceEnabled) {
|
||||
return (
|
||||
@@ -163,10 +183,11 @@ export const SignInUp = () => {
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
clientConfigApiStatus.isLoadedOnce,
|
||||
isDefaultDomain,
|
||||
isMultiWorkspaceEnabled,
|
||||
isOnAWorkspace,
|
||||
loading,
|
||||
getPublicWorkspaceDataLoading,
|
||||
signInUpStep,
|
||||
workspacePublicData,
|
||||
]);
|
||||
|
||||
@@ -2,7 +2,10 @@ import { getOperationName } from '@apollo/client/utilities';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { fireEvent, within } from '@storybook/test';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import {
|
||||
PageDecorator,
|
||||
@@ -13,9 +16,28 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { SignInUp } from '../SignInUp';
|
||||
|
||||
const CaptchaTokenSetterEffect = () => {
|
||||
const setCaptchaToken = useSetRecoilState(captchaTokenState);
|
||||
|
||||
useEffect(() => {
|
||||
setCaptchaToken('MOCKED_CAPTCHA_TOKEN');
|
||||
}, [setCaptchaToken]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const SignInUpWithCaptcha = () => {
|
||||
return (
|
||||
<>
|
||||
<CaptchaTokenSetterEffect />
|
||||
<SignInUp />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Auth/SignInUp',
|
||||
component: SignInUp,
|
||||
component: SignInUpWithCaptcha,
|
||||
decorators: [PageDecorator],
|
||||
args: { routePath: AppPath.SignInUp },
|
||||
parameters: {
|
||||
@@ -47,7 +69,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof SignInUp>;
|
||||
export type Story = StoryObj<typeof SignInUpWithCaptcha>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
|
||||
@@ -2,7 +2,10 @@ import { getOperationName } from '@apollo/client/utilities';
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { fireEvent, within } from '@storybook/test';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { useEffect } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import { GET_WORKSPACE_FROM_INVITE_HASH } from '@/workspace/graphql/queries/getWorkspaceFromInviteHash';
|
||||
import {
|
||||
@@ -14,9 +17,28 @@ import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { SignInUp } from '../SignInUp';
|
||||
|
||||
const CaptchaTokenSetterEffect = () => {
|
||||
const setCaptchaToken = useSetRecoilState(captchaTokenState);
|
||||
|
||||
useEffect(() => {
|
||||
setCaptchaToken('MOCKED_CAPTCHA_TOKEN');
|
||||
}, [setCaptchaToken]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const SignInUpWithCaptcha = () => {
|
||||
return (
|
||||
<>
|
||||
<CaptchaTokenSetterEffect />
|
||||
<SignInUp />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Auth/Invite',
|
||||
component: SignInUp,
|
||||
component: SignInUpWithCaptcha,
|
||||
decorators: [PageDecorator],
|
||||
args: {
|
||||
routePath: AppPath.Invite,
|
||||
@@ -67,7 +89,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
|
||||
export default meta;
|
||||
|
||||
export type Story = StoryObj<typeof SignInUp>;
|
||||
export type Story = StoryObj<typeof SignInUpWithCaptcha>;
|
||||
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
|
||||
Reference in New Issue
Block a user