Upgrade Apollo Client to v4 and refactor error handling (#18584)

## Summary
This PR upgrades Apollo Client from v3.10.0 to v4 and refactors error
handling patterns across the codebase to use a new centralized
`useSnackBarOnQueryError` hook.

## Key Changes

- **Dependency Update**: Upgraded `@apollo/client` from `^3.10.0` to
`^3.11.0` in root package.json
- **New Hook**: Added `useSnackBarOnQueryError` hook for centralized
Apollo query error handling with snack bar notifications
- **Error Handling Refactor**: Updated 100+ files to use the new error
handling pattern:
  - Removed direct `ApolloError` imports where no longer needed
- Replaced manual error handling logic with `useSnackBarOnQueryError`
hook
- Simplified error handling in hooks and components across multiple
modules
- **GraphQL Codegen**: Updated codegen configuration files to work with
Apollo Client v3.11.0
- **Type Definitions**: Added TypeScript declaration file for
`apollo-upload-client` module
- **Test Updates**: Updated test files to reflect new error handling
patterns

## Notable Implementation Details

- The new `useSnackBarOnQueryError` hook provides a consistent way to
handle Apollo query errors with automatic snack bar notifications
- Changes span across multiple feature areas: auth, object records,
settings, workflows, billing, and more
- All changes maintain backward compatibility while improving code
maintainability and reducing duplication
- Jest configuration updated to work with the new Apollo Client version

https://claude.ai/code/session_019WGZ6Rd7sEHuBg9sTrXRqJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-03-13 14:59:46 +01:00
committed by GitHub
parent 172bbd01bc
commit b470cb21a1
386 changed files with 3482 additions and 13593 deletions
@@ -1,6 +1,6 @@
import { useAuth } from '@/auth/hooks/useAuth';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { AppPath } from 'twenty-shared/types';
import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState';
@@ -95,7 +95,7 @@ export const VerifyEmailEffect = () => {
await verifyLoginToken(loginToken.token);
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError
...(CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: t`Email verification failed` }),
options: {
@@ -103,9 +103,8 @@ export const VerifyEmailEffect = () => {
},
});
if (
error instanceof ApolloError &&
error.graphQLErrors[0].extensions?.subCode ===
'EMAIL_ALREADY_VERIFIED'
CombinedGraphQLErrors.is(error) &&
error.errors[0].extensions?.subCode === 'EMAIL_ALREADY_VERIFIED'
) {
navigate(AppPath.SignInUp);
}
@@ -5,8 +5,8 @@ import { supportChatState } from '@/client-config/states/supportChatState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { useApolloClient } from '@apollo/client/react';
import { MockedProvider } from '@apollo/client/testing/react';
import { type ReactNode, act } from 'react';
import { MemoryRouter } from 'react-router-dom';
@@ -73,7 +73,7 @@ jest.mock('@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain', () => ({
}));
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
<MockedProvider mocks={Object.values(mocks)}>
<MemoryRouter>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'test-instance-id' }}
@@ -1,20 +1,25 @@
import { ApolloError, useApolloClient } from '@apollo/client';
import {
useApolloClient,
useLazyQuery,
useMutation,
} from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useCallback } from 'react';
import { AppPath } from 'twenty-shared/types';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
useCheckUserExistsLazyQuery,
useGetAuthTokensFromLoginTokenMutation,
useGetAuthTokensFromOtpMutation,
useGetLoginTokenFromCredentialsMutation,
useSignInMutation,
useSignUpInWorkspaceMutation,
useSignUpMutation,
useVerifyEmailAndGetLoginTokenMutation,
useVerifyEmailAndGetWorkspaceAgnosticTokenMutation,
type AuthToken,
type AuthTokenPair,
CheckUserExistsDocument,
GetAuthTokensFromLoginTokenDocument,
GetAuthTokensFromOtpDocument,
GetLoginTokenFromCredentialsDocument,
SignInDocument,
SignUpInWorkspaceDocument,
SignUpDocument,
VerifyEmailAndGetLoginTokenDocument,
VerifyEmailAndGetWorkspaceAgnosticTokenDocument,
} from '~/generated-metadata/graphql';
import { tokenPairState } from '@/auth/states/tokenPairState';
@@ -91,25 +96,30 @@ export const useAuth = () => {
const { redirect } = useRedirect();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const [getLoginTokenFromCredentials] =
useGetLoginTokenFromCredentialsMutation();
const [signIn] = useSignInMutation();
const [signUp] = useSignUpMutation();
const [signUpInWorkspace] = useSignUpInWorkspaceMutation();
const [getAuthTokensFromLoginToken] =
useGetAuthTokensFromLoginTokenMutation();
const [verifyEmailAndGetLoginToken] =
useVerifyEmailAndGetLoginTokenMutation();
const [verifyEmailAndGetWorkspaceAgnosticToken] =
useVerifyEmailAndGetWorkspaceAgnosticTokenMutation();
const [getAuthTokensFromOtp] = useGetAuthTokensFromOtpMutation();
const [getLoginTokenFromCredentials] = useMutation(
GetLoginTokenFromCredentialsDocument,
);
const [signIn] = useMutation(SignInDocument);
const [signUp] = useMutation(SignUpDocument);
const [signUpInWorkspace] = useMutation(SignUpInWorkspaceDocument);
const [getAuthTokensFromLoginToken] = useMutation(
GetAuthTokensFromLoginTokenDocument,
);
const [verifyEmailAndGetLoginToken] = useMutation(
VerifyEmailAndGetLoginTokenDocument,
);
const [verifyEmailAndGetWorkspaceAgnosticToken] = useMutation(
VerifyEmailAndGetWorkspaceAgnosticTokenDocument,
);
const [getAuthTokensFromOtp] = useMutation(GetAuthTokensFromOtpDocument);
const workspacePublicData = useAtomStateValue(workspacePublicDataState);
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
const [checkUserExistsQuery, { data: checkUserExistsData }] =
useCheckUserExistsLazyQuery();
const [checkUserExistsQuery, { data: checkUserExistsData }] = useLazyQuery(
CheckUserExistsDocument,
);
const client = useApolloClient();
@@ -188,8 +198,8 @@ export const useAuth = () => {
origin,
},
});
if (isDefined(getLoginTokenResult.errors)) {
throw getLoginTokenResult.errors;
if (isDefined(getLoginTokenResult.error)) {
throw getLoginTokenResult.error;
}
if (!getLoginTokenResult.data?.getLoginTokenFromCredentials) {
@@ -200,8 +210,8 @@ export const useAuth = () => {
} catch (error) {
// TODO: Get intellisense for graphql error extensions code (codegen?)
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
@@ -228,8 +238,8 @@ export const useAuth = () => {
},
});
if (isDefined(loginTokenResult.errors)) {
throw loginTokenResult.errors;
if (isDefined(loginTokenResult.error)) {
throw loginTokenResult.error;
}
if (!loginTokenResult.data?.verifyEmailAndGetLoginToken) {
@@ -247,7 +257,7 @@ export const useAuth = () => {
email: string,
captchaToken?: string,
) => {
const { data, errors } = await verifyEmailAndGetWorkspaceAgnosticToken({
const { data, error } = await verifyEmailAndGetWorkspaceAgnosticToken({
variables: {
email,
emailVerificationToken,
@@ -255,8 +265,8 @@ export const useAuth = () => {
},
});
if (isDefined(errors)) {
throw errors;
if (isDefined(error)) {
throw error;
}
if (!data?.verifyEmailAndGetWorkspaceAgnosticToken) {
@@ -315,8 +325,8 @@ export const useAuth = () => {
},
});
if (isDefined(getAuthTokensResult.errors)) {
throw getAuthTokensResult.errors;
if (isDefined(getAuthTokensResult.error)) {
throw getAuthTokensResult.error;
}
if (!getAuthTokensResult.data?.getAuthTokensFromLoginToken) {
@@ -328,8 +338,8 @@ export const useAuth = () => {
);
} catch (error) {
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode ===
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode ===
'TWO_FACTOR_AUTHENTICATION_PROVISION_REQUIRED'
) {
handleSetLoginToken(loginToken);
@@ -338,8 +348,8 @@ export const useAuth = () => {
}
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode ===
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode ===
'TWO_FACTOR_AUTHENTICATION_VERIFICATION_REQUIRED'
) {
handleSetLoginToken(loginToken);
@@ -360,7 +370,7 @@ export const useAuth = () => {
const handleCredentialsSignIn = useCallback(
async (email: string, password: string, captchaToken?: string) => {
signIn({
await signIn({
variables: { email, password, captchaToken },
onCompleted: async (data) => {
handleSetAuthTokens(data.signIn.tokens);
@@ -394,8 +404,8 @@ export const useAuth = () => {
},
onError: (error) => {
if (
error instanceof ApolloError &&
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
CombinedGraphQLErrors.is(error) &&
error.errors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
) {
setSearchParams({ email });
setSignInUpStep(SignInUpStep.EmailVerification);
@@ -427,8 +437,8 @@ export const useAuth = () => {
},
});
if (isDefined(signUpResult.errors)) {
throw signUpResult.errors;
if (isDefined(signUpResult.error)) {
throw signUpResult.error;
}
if (isEmailVerificationRequired) {
@@ -510,8 +520,8 @@ export const useAuth = () => {
},
});
if (isDefined(signUpInWorkspaceResult.errors)) {
throw signUpInWorkspaceResult.errors;
if (isDefined(signUpInWorkspaceResult.error)) {
throw signUpInWorkspaceResult.error;
}
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
@@ -632,8 +642,8 @@ export const useAuth = () => {
},
});
if (isDefined(getAuthTokensFromOtpResult.errors)) {
throw getAuthTokensFromOtpResult.errors;
if (isDefined(getAuthTokensFromOtpResult.error)) {
throw getAuthTokensFromOtpResult.error;
}
if (!getAuthTokensFromOtpResult.data?.getAuthTokensFromOTP) {
@@ -3,11 +3,9 @@ import {
ApolloLink,
HttpLink,
InMemoryCache,
type UriFunction,
} from '@apollo/client';
import { loggerLink } from '@/apollo/utils/loggerLink';
import { isDefined } from 'twenty-shared/utils';
import {
type AuthTokenPair,
RenewTokenDocument,
@@ -19,7 +17,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const logger = loggerLink(() => 'Twenty-Refresh');
const renewTokenMutation = async (
uri: string | UriFunction | undefined,
uri: string | undefined,
refreshToken: string,
) => {
const httpLink = new HttpLink({ uri });
@@ -30,18 +28,24 @@ const renewTokenMutation = async (
cache: new InMemoryCache({}),
});
const { data, errors } = await client.mutate<
RenewTokenMutation,
RenewTokenMutationVariables
>({
mutation: RenewTokenDocument,
variables: {
appToken: refreshToken,
},
fetchPolicy: 'network-only',
});
let data: RenewTokenMutation | null | undefined;
try {
const result = await client.mutate<
RenewTokenMutation,
RenewTokenMutationVariables
>({
mutation: RenewTokenDocument,
variables: {
appToken: refreshToken,
},
fetchPolicy: 'network-only',
});
data = result.data;
} catch {
throw new Error('Something went wrong during token renewal');
}
if (isDefined(errors) || isUndefinedOrNull(data)) {
if (isUndefinedOrNull(data)) {
throw new Error('Something went wrong during token renewal');
}
@@ -49,7 +53,7 @@ const renewTokenMutation = async (
};
export const renewToken = async (
uri: string | UriFunction | undefined,
uri: string | undefined,
tokenPair: AuthTokenPair | undefined | null,
) => {
if (!tokenPair) {
@@ -9,15 +9,13 @@ import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import {
type PublicWorkspaceData,
useEmailPasswordResetLinkMutation,
} from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { type PublicWorkspaceData } from '~/generated-metadata/graphql';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
// Mocks
jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar');
jest.mock('~/generated-metadata/graphql');
jest.mock('@apollo/client/react');
dynamicActivate(SOURCE_LOCALE);
@@ -63,7 +61,7 @@ describe('useHandleResetPassword', () => {
enqueueErrorSnackBar: enqueueErrorSnackBarMock,
enqueueSuccessSnackBar: enqueueSuccessSnackBarMock,
});
(useEmailPasswordResetLinkMutation as jest.Mock).mockReturnValue([
(useMutation as unknown as jest.Mock).mockReturnValue([
emailPasswordResetLinkMock,
]);
});
@@ -127,6 +125,8 @@ describe('useHandleResetPassword', () => {
const { result } = renderHooks();
await act(() => result.current.handleResetPassword('test@example.com')());
expect(enqueueErrorSnackBarMock).toHaveBeenCalledWith({});
expect(enqueueErrorSnackBarMock).toHaveBeenCalledWith({
message: errorMessage,
});
});
});
@@ -2,8 +2,8 @@ import { GET_AUTHORIZATION_URL_FOR_SSO } from '@/auth/graphql/mutations/getAutho
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { MockedProvider } from '@apollo/client/testing/react';
import { renderHook } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
@@ -55,9 +55,7 @@ const apolloMocks = [
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter>
<MockedProvider mocks={apolloMocks} addTypename={false}>
{children}
</MockedProvider>
<MockedProvider mocks={apolloMocks}>{children}</MockedProvider>
</MemoryRouter>
);
@@ -86,8 +84,8 @@ describe('useSSO', () => {
await result.current.redirectToSSOLoginPage(identityProviderId);
expect(mockEnqueueErrorSnackBar).toHaveBeenCalledWith({
apolloError: new ApolloError({
graphQLErrors: [{ message: 'Error message' }],
apolloError: new CombinedGraphQLErrors({
errors: [{ message: 'Error message' }],
}),
});
});
@@ -2,14 +2,16 @@ import { useCallback } from 'react';
import { useOrigin } from '@/domain-manager/hooks/useOrigin';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { t } from '@lingui/core/macro';
import { useResendEmailVerificationTokenMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { ResendEmailVerificationTokenDocument } from '~/generated-metadata/graphql';
export const useHandleResendEmailVerificationToken = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [resendEmailVerificationToken, { loading }] =
useResendEmailVerificationTokenMutation();
const [resendEmailVerificationToken, { loading }] = useMutation(
ResendEmailVerificationTokenDocument,
);
const { origin } = useOrigin();
const handleResendEmailVerificationToken = useCallback(
@@ -38,9 +40,11 @@ export const useHandleResendEmailVerificationToken = () => {
enqueueErrorSnackBar({});
}
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
});
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
};
},
@@ -3,14 +3,15 @@ import { useCallback } from 'react';
import { currentUserState } from '@/auth/states/currentUserState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useLingui } from '@lingui/react/macro';
import { useEmailPasswordResetLinkMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { EmailPasswordResetLinkDocument } from '~/generated-metadata/graphql';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useHandleResetPassword = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
const [emailPasswordResetLink] = useMutation(EmailPasswordResetLinkDocument);
const workspacePublicData = useAtomStateValue(workspacePublicDataState);
const currentUser = useAtomStateValue(currentUserState);
@@ -41,9 +42,11 @@ export const useHandleResetPassword = () => {
enqueueErrorSnackBar({});
}
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
});
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
};
},
@@ -1,10 +1,11 @@
/* @license Enterprise */
import { GET_AUTHORIZATION_URL_FOR_SSO } from '@/auth/graphql/mutations/getAuthorizationUrlForSSO';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError, useApolloClient } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useParams } from 'react-router-dom';
import { GetAuthorizationUrlForSsoDocument } from '~/generated-metadata/graphql';
export const useSSO = () => {
const apolloClient = useApolloClient();
@@ -16,7 +17,7 @@ export const useSSO = () => {
let authorizationUrlForSSOResult;
try {
authorizationUrlForSSOResult = await apolloClient.mutate({
mutation: GET_AUTHORIZATION_URL_FOR_SSO,
mutation: GetAuthorizationUrlForSsoDocument,
variables: {
input: {
identityProviderId,
@@ -24,16 +25,21 @@ export const useSSO = () => {
},
},
});
} catch (error: any) {
return enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
});
} catch (error: unknown) {
return enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
redirect(
const authorizationURL =
authorizationUrlForSSOResult.data?.getAuthorizationUrlForSSO
.authorizationURL,
);
?.authorizationURL;
if (authorizationURL) {
redirect(authorizationURL);
}
};
return {
@@ -16,7 +16,7 @@ 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 { isErrorLike } from '@apollo/client/errors';
import { useLingui } from '@lingui/react/macro';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -178,9 +178,9 @@ export const useSignInUp = (form: UseFormReturn<Form>) => {
captchaToken: token,
verifyEmailRedirectPath,
});
} catch (error: any) {
} catch (error: unknown) {
enqueueErrorSnackBar({
...(error instanceof ApolloError ? { apolloError: error } : {}),
...(isErrorLike(error) ? { apolloError: error } : {}),
});
}
},
@@ -1,8 +1,9 @@
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { AppPath } from 'twenty-shared/types';
import { useSignUpInNewWorkspaceMutation } from '~/generated-metadata/graphql';
import { useMutation } from '@apollo/client/react';
import { SignUpInNewWorkspaceDocument } from '~/generated-metadata/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { useLingui } from '@lingui/react/macro';
@@ -12,7 +13,9 @@ export const useSignUpInNewWorkspace = () => {
const { enqueueErrorSnackBar } = useSnackBar();
const { t } = useLingui();
const [signUpInNewWorkspaceMutation] = useSignUpInNewWorkspaceMutation();
const [signUpInNewWorkspaceMutation] = useMutation(
SignUpInNewWorkspaceDocument,
);
const createWorkspace = async ({ newTab } = { newTab: true }) => {
try {
@@ -27,11 +30,16 @@ export const useSignUpInNewWorkspace = () => {
newTab ? '_blank' : '_self',
);
} catch (error) {
enqueueErrorSnackBar({
...(error instanceof ApolloError
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: t`Workspace creation failed` }),
});
: {
message:
error instanceof Error
? error.message
: t`Workspace creation failed`,
},
);
}
};
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
@@ -9,7 +9,8 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useGetWorkspaceFromInviteHashQuery } from '~/generated-metadata/graphql';
import { useQuery } from '@apollo/client/react';
import { GetWorkspaceFromInviteHashDocument } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useWorkspaceFromInviteHash = () => {
@@ -18,33 +19,52 @@ export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [initiallyLoggedIn] = useState(isDefined(currentWorkspace));
const [hasRedirected, setHasRedirected] = useState(false);
const { data: workspaceFromInviteHash, loading } =
useGetWorkspaceFromInviteHashQuery({
skip: !workspaceInviteHash,
variables: { inviteHash: workspaceInviteHash || '' },
onError: (error) => {
enqueueErrorSnackBar({ apolloError: error });
navigate(AppPath.Index);
},
onCompleted: (data) => {
if (
isDefined(currentWorkspace) &&
isDefined(data?.findWorkspaceFromInviteHash) &&
currentWorkspace.id === data.findWorkspaceFromInviteHash.id
) {
const workspaceDisplayName =
data?.findWorkspaceFromInviteHash?.displayName;
initiallyLoggedIn &&
enqueueInfoSnackBar({
message: workspaceDisplayName
? t`You already belong to the workspace ${workspaceDisplayName}`
: t`You already belong to this workspace`,
});
navigate(AppPath.Index);
}
},
});
const {
data: workspaceFromInviteHash,
loading,
error,
} = useQuery(GetWorkspaceFromInviteHashDocument, {
skip: !workspaceInviteHash,
variables: { inviteHash: workspaceInviteHash || '' },
});
useEffect(() => {
if (error) {
enqueueErrorSnackBar({ apolloError: error });
navigate(AppPath.Index);
}
}, [error, enqueueErrorSnackBar, navigate]);
useEffect(() => {
if (!workspaceFromInviteHash || hasRedirected) return;
const inviteWorkspace = workspaceFromInviteHash.findWorkspaceFromInviteHash;
if (
isDefined(currentWorkspace) &&
isDefined(inviteWorkspace) &&
currentWorkspace.id === inviteWorkspace.id
) {
setHasRedirected(true);
const workspaceDisplayName = inviteWorkspace.displayName;
initiallyLoggedIn &&
enqueueInfoSnackBar({
message: workspaceDisplayName
? t`You already belong to the workspace ${workspaceDisplayName}`
: t`You already belong to this workspace`,
});
navigate(AppPath.Index);
}
}, [
workspaceFromInviteHash,
currentWorkspace,
hasRedirected,
initiallyLoggedIn,
enqueueInfoSnackBar,
navigate,
]);
return {
workspace: workspaceFromInviteHash?.findWorkspaceFromInviteHash,
workspaceInviteHash,