fix: address PR review comments for Apollo v4 migration

- Add missing await on signIn mutation in handleCredentialsSignIn (Sentry)
- Preserve plain Error messages in useSnackBarOnQueryError instead of
  dropping to generic text
- Handle non-GraphQL errors in catch blocks across auth hooks
  (useHandleResetPassword, useSignUpInNewWorkspace,
  useHandleResendEmailVerificationToken, useSSO) by passing error.message
- Gate Apollo DevTools with IS_DEBUG_MODE in metadata ApolloProvider
- Wrap client.mutate in try/catch in AuthService for Apollo v4 compat
- Handle ApolloError in PromiseRejectionEffect alongside CombinedGraphQLErrors
- Prevent duplicate redirects in useWorkspaceFromInviteHash effect
- Wire billing query error to useSnackBarOnQueryError
- Remove unreachable renewResult.error branch in
  useRequestApplicationTokenRefresh (catch block handles it)

https://claude.ai/code/session_017uafSHYDZYzv7Tp6qnVDbB
This commit is contained in:
Claude
2026-03-13 10:56:29 +00:00
parent 5051c3c7cc
commit e0baef94c9
12 changed files with 67 additions and 53 deletions
@@ -12,7 +12,7 @@ export const ApolloProvider = ({ children }: React.PropsWithChildren) => {
const apolloClient = useApolloFactory({
uri: `${REACT_APP_SERVER_BASE_URL}/metadata`,
devtools: { enabled: true }, // should this be default , ie dependant on IS_DEBUG_MODE?
devtools: { enabled: process.env.IS_DEBUG_MODE === 'true' },
extraLinks: [captchaRefreshLink],
});
@@ -22,9 +22,9 @@ export const useSnackBarOnQueryError = (
enqueueErrorSnackBar(
message
? { message }
: {
apolloError: CombinedGraphQLErrors.is(error) ? error : undefined,
},
: CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error.message },
);
}, [error, enqueueErrorSnackBar, message]);
};
@@ -370,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);
@@ -6,7 +6,6 @@ import {
} from '@apollo/client';
import { loggerLink } from '@/apollo/utils/loggerLink';
import { isDefined } from 'twenty-shared/utils';
import {
type AuthTokenPair,
RenewTokenDocument,
@@ -29,18 +28,24 @@ const renewTokenMutation = async (
cache: new InMemoryCache({}),
});
const { data, error } = 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(error) || isUndefinedOrNull(data)) {
if (isUndefinedOrNull(data)) {
throw new Error('Something went wrong during token renewal');
}
@@ -40,9 +40,11 @@ export const useHandleResendEmailVerificationToken = () => {
enqueueErrorSnackBar({});
}
} catch (error) {
enqueueErrorSnackBar({
...(CombinedGraphQLErrors.is(error) ? { apolloError: error } : {}),
});
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
};
},
@@ -42,9 +42,11 @@ export const useHandleResetPassword = () => {
enqueueErrorSnackBar({});
}
} catch (error) {
enqueueErrorSnackBar({
...(CombinedGraphQLErrors.is(error) ? { apolloError: error } : {}),
});
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
};
},
@@ -25,10 +25,12 @@ export const useSSO = () => {
},
},
});
} catch (error: any) {
return enqueueErrorSnackBar({
...(CombinedGraphQLErrors.is(error) ? { apolloError: error } : {}),
});
} catch (error: unknown) {
return enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: error instanceof Error ? error.message : undefined },
);
}
const authorizationURL =
@@ -30,11 +30,16 @@ export const useSignUpInNewWorkspace = () => {
newTab ? '_blank' : '_self',
);
} catch (error) {
enqueueErrorSnackBar({
...(CombinedGraphQLErrors.is(error)
enqueueErrorSnackBar(
CombinedGraphQLErrors.is(error)
? { apolloError: error }
: { message: t`Workspace creation failed` }),
});
: {
message:
error instanceof Error
? error.message
: t`Workspace creation failed`,
},
);
}
};
@@ -19,6 +19,7 @@ export const useWorkspaceFromInviteHash = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [initiallyLoggedIn] = useState(isDefined(currentWorkspace));
const [hasRedirected, setHasRedirected] = useState(false);
const {
data: workspaceFromInviteHash,
@@ -37,16 +38,17 @@ export const useWorkspaceFromInviteHash = () => {
}, [error, enqueueErrorSnackBar, navigate]);
useEffect(() => {
if (!workspaceFromInviteHash) return;
if (!workspaceFromInviteHash || hasRedirected) return;
const inviteWorkspace = workspaceFromInviteHash.findWorkspaceFromInviteHash;
const data = workspaceFromInviteHash;
if (
isDefined(currentWorkspace) &&
isDefined(data?.findWorkspaceFromInviteHash) &&
currentWorkspace.id === data.findWorkspaceFromInviteHash.id
isDefined(inviteWorkspace) &&
currentWorkspace.id === inviteWorkspace.id
) {
const workspaceDisplayName =
data?.findWorkspaceFromInviteHash?.displayName;
setHasRedirected(true);
const workspaceDisplayName = inviteWorkspace.displayName;
initiallyLoggedIn &&
enqueueInfoSnackBar({
message: workspaceDisplayName
@@ -58,6 +60,7 @@ export const useWorkspaceFromInviteHash = () => {
}, [
workspaceFromInviteHash,
currentWorkspace,
hasRedirected,
initiallyLoggedIn,
enqueueInfoSnackBar,
navigate,
@@ -1,6 +1,7 @@
import { useCallback, useEffect } from 'react';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { isDefined, type CustomError } from 'twenty-shared/utils';
@@ -16,7 +17,7 @@ export const PromiseRejectionEffect = () => {
const handlePromiseRejection = useCallback(
async (event: PromiseRejectionEvent) => {
const error = event.reason;
if (CombinedGraphQLErrors.is(error)) {
if (CombinedGraphQLErrors.is(error) || error instanceof ApolloError) {
enqueueErrorSnackBar({
apolloError: error,
});
@@ -28,7 +29,9 @@ export const PromiseRejectionEffect = () => {
error?.name === 'AbortError';
if (!isAbortError) {
enqueueErrorSnackBar({});
enqueueErrorSnackBar(
error instanceof Error ? { message: error.message } : {},
);
}
try {
@@ -77,6 +77,8 @@ export const useRequestApplicationTokenRefresh = ({
// If the refresh token itself is expired, fall back to refetching
// the front component which issues a fresh token pair server-side.
try {
// With the default errorPolicy ('none'), client.mutate() rejects on
// GraphQL/network errors, so error handling happens in the catch block.
const renewResult = await apolloClient.mutate<
RenewApplicationTokenMutation,
RenewApplicationTokenMutationVariables
@@ -88,19 +90,6 @@ export const useRequestApplicationTokenRefresh = ({
},
});
if (isDefined(renewResult.error)) {
if (
CombinedGraphQLErrors.is(renewResult.error) &&
hasApplicationRefreshTokenInvalidOrExpiredSubCode(
renewResult.error.errors,
)
) {
return await refetchFrontComponentForNewTokenPair();
}
throw new Error(`Token renewal failed: ${renewResult.error.message}`);
}
const renewedTokenPair = renewResult.data?.renewApplicationToken;
if (!isDefined(renewedTokenPair)) {
@@ -1,3 +1,4 @@
import { useSnackBarOnQueryError } from '@/apollo/hooks/useSnackBarOnQueryError';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { usePermissionFlagMap } from '@/settings/roles/hooks/usePermissionFlagMap';
@@ -13,12 +14,14 @@ import {
export const InformationBannerBillingSubscriptionPaused = () => {
const { redirect } = useRedirect();
const { data, loading } = useQuery(BillingPortalSessionDocument, {
const { data, loading, error } = useQuery(BillingPortalSessionDocument, {
variables: {
returnUrlPath: getSettingsPath(SettingsPath.Billing),
},
});
useSnackBarOnQueryError(error);
const {
[PermissionFlagType.WORKSPACE]: hasPermissionToUpdateBillingDetails,
} = usePermissionFlagMap();