diff --git a/packages/shared/src/components/modals/recruiter/RecruiterSignInModal.tsx b/packages/shared/src/components/modals/recruiter/RecruiterSignInModal.tsx index 1666a9e600..b9f8d18314 100644 --- a/packages/shared/src/components/modals/recruiter/RecruiterSignInModal.tsx +++ b/packages/shared/src/components/modals/recruiter/RecruiterSignInModal.tsx @@ -1,5 +1,6 @@ import type { ReactElement } from 'react'; import React, { useCallback, useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; import type { ModalProps } from '../common/Modal'; import { Modal } from '../common/Modal'; import { @@ -13,6 +14,8 @@ import { AuthTriggers } from '../../../lib/auth'; import type { AuthProps } from '../../auth/common'; import { AuthDisplay } from '../../auth/common'; import AuthOptions from '../../auth/AuthOptions'; +import { claimOpportunitiesMutationOptions } from '../../../features/opportunity/mutations'; +import { useAuthContext } from '../../../contexts/AuthContext'; export type RecruiterSignInModalProps = ModalProps & { onSuccess?: () => void; @@ -23,6 +26,12 @@ export const RecruiterSignInModal = ({ onSuccess, ...modalProps }: RecruiterSignInModalProps): ReactElement => { + const { trackingId } = useAuthContext(); + const [trackingIdState] = useState(trackingId); // save initial trackingId before login/registration + const { mutateAsync: claimOpportunities } = useMutation( + claimOpportunitiesMutationOptions(), + ); + const [authState, setAuthState] = useState(() => { return { isAuthenticating: false, @@ -42,11 +51,19 @@ export const RecruiterSignInModal = ({ onSuccess?.(); }, [onRequestClose, onSuccess]); - const handleSuccessfulLogin = useCallback(() => { + const handleSuccessfulLogin = useCallback(async () => { + try { + if (trackingIdState) { + await claimOpportunities({ identifier: trackingIdState }); + } + } catch { + // if we can't claim at this time we move on + } + // Close the modal and trigger success callback onRequestClose?.(null); onSuccess?.(); - }, [onRequestClose, onSuccess]); + }, [claimOpportunities, onRequestClose, onSuccess, trackingIdState]); // Derive the display from auth state - login flow should show default display const authDisplay = authState.isLoginFlow diff --git a/packages/shared/src/features/opportunity/context/OpportunityPreviewContext.tsx b/packages/shared/src/features/opportunity/context/OpportunityPreviewContext.tsx index bcc3e98a7e..49cbf5d8c8 100644 --- a/packages/shared/src/features/opportunity/context/OpportunityPreviewContext.tsx +++ b/packages/shared/src/features/opportunity/context/OpportunityPreviewContext.tsx @@ -15,6 +15,10 @@ import { OpportunityState } from '../protobuf/opportunity'; import { useAuthContext } from '../../../contexts/AuthContext'; import { oneMinute } from '../../../lib/dateFormat'; import { useUpdateQuery } from '../../../hooks/useUpdateQuery'; +import type { ApiErrorResult } from '../../../graphql/common'; +import { ApiError } from '../../../graphql/common'; +import { getPathnameWithQuery } from '../../../lib'; +import { webappUrl } from '../../../lib/constants'; export type OpportunityPreviewContextType = OpportunityPreviewResponse & { opportunity?: Opportunity; @@ -47,6 +51,24 @@ const [OpportunityPreviewProvider, useOpportunityPreviewContext] = ...opportunityByIdOptions({ id: opportunityIdParam || '' }), enabled: isValidOpportunityId && !mockData, refetchInterval: (query) => { + if (query.state.error) { + const errorCode = (query.state.error as unknown as ApiErrorResult) + .response?.errors?.[0]?.extensions?.code; + + if ([ApiError.Forbidden, ApiError.NotFound].includes(errorCode)) { + router.push( + getPathnameWithQuery( + `${webappUrl}recruiter`, + new URLSearchParams({ + openModal: 'joblink', + }), + ), + ); + + return false; + } + } + const retries = Math.max( query.state.dataUpdateCount, query.state.fetchFailureCount, diff --git a/packages/shared/src/features/opportunity/graphql.ts b/packages/shared/src/features/opportunity/graphql.ts index 43a0c12efc..2793af7695 100644 --- a/packages/shared/src/features/opportunity/graphql.ts +++ b/packages/shared/src/features/opportunity/graphql.ts @@ -779,3 +779,11 @@ export const OPPORTUNITY_FEEDBACK_QUERY = gql` } ${FEEDBACK_CLASSIFICATION_FRAGMENT} `; + +export const CLAIM_OPPORTUNITIES_MUTATION = gql` + mutation ClaimOpportunities($identifier: String!) { + claimOpportunities(identifier: $identifier) { + ids + } + } +`; diff --git a/packages/shared/src/features/opportunity/mutations.ts b/packages/shared/src/features/opportunity/mutations.ts index b4a697dbfc..a4730114a4 100644 --- a/packages/shared/src/features/opportunity/mutations.ts +++ b/packages/shared/src/features/opportunity/mutations.ts @@ -10,6 +10,7 @@ import { ADD_OPPORTUNITY_SEATS_MUTATION, CANDIDATE_KEYWORD_ADD_MUTATION, CANDIDATE_KEYWORD_REMOVE_MUTATION, + CLAIM_OPPORTUNITIES_MUTATION, CLEAR_EMPLOYMENT_AGREEMENT_MUTATION, CLEAR_RECRUITER_ORGANIZATION_IMAGE_MUTATION, CLEAR_RESUME_MUTATION, @@ -31,6 +32,7 @@ import { import type { EmptyResponse } from '../../graphql/emptyResponse'; import type { Opportunity, + OpportunitiesClaim, OpportunityMatch, OpportunityScreeningAnswer, UserCandidatePreferences, @@ -543,3 +545,21 @@ export const addOpportunitySeatsMutationOptions = () => { }, }; }; + +export const claimOpportunitiesMutationOptions = (): MutationOptions< + OpportunitiesClaim, + DefaultError, + { identifier: string } +> => { + return { + mutationFn: async ({ identifier }) => { + const result = await gqlClient.request<{ + claimOpportunities: OpportunitiesClaim; + }>(CLAIM_OPPORTUNITIES_MUTATION, { + identifier, + }); + + return result.claimOpportunities; + }, + }; +}; diff --git a/packages/shared/src/features/opportunity/types.ts b/packages/shared/src/features/opportunity/types.ts index 49837243f6..87b892a8cb 100644 --- a/packages/shared/src/features/opportunity/types.ts +++ b/packages/shared/src/features/opportunity/types.ts @@ -323,3 +323,7 @@ export type FeedbackClassification = { export interface OpportunityFeedbackData { opportunityFeedback: Connection; } + +export type OpportunitiesClaim = { + ids: string[]; +};