Skip to content

Commit 4879684

Browse files
authored
feat: recruiter organization payment (#5166)
1 parent cc5a146 commit 4879684

8 files changed

Lines changed: 110 additions & 19 deletions

File tree

packages/shared/src/contexts/RecruiterPaymentContext/RecruiterPaymentPaddleContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export const RecruiterPaymentPaddleContextProvider = ({
5656

5757
const { paddle, openCheckout } = usePaddlePayment({
5858
successCallback: () => {
59-
router.push(
59+
router.replace(
6060
`${webappUrl}recruiter/${router.query.opportunityId}/matches`,
6161
);
6262
},

packages/shared/src/features/opportunity/graphql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ export const OPPORTUNITY_FRAGMENT = gql`
129129
feedbackQuestions {
130130
...OpportunityFeedbackQuestionFragment
131131
}
132-
subscriptionStatus
133132
flags {
134133
batchSize
134+
plan
135135
}
136136
}
137137
${ORGANIZATION_SHORT_FRAGMENT}

packages/shared/src/features/opportunity/mockData.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Opportunity } from './types';
22
import type { OpportunityPreviewContextType } from './context/OpportunityPreviewContext';
33
import { SeniorityLevel } from './protobuf/opportunity';
44
import { SourceMemberRole, SourceType } from '../../graphql/sources';
5-
import { SubscriptionStatus } from '../../lib/plus';
65

76
export const mockOpportunity: Opportunity = {
87
id: '89f3daff-d6bb-4652-8f9c-b9f7254c9af1',
@@ -64,7 +63,6 @@ export const mockOpportunity: Opportunity = {
6463
{ keyword: 'JavaScript' },
6564
{ keyword: 'Tailwind CSS' },
6665
],
67-
subscriptionStatus: SubscriptionStatus.None,
6866
};
6967

7068
export const mockAnonymousUserTableData: OpportunityPreviewContextType = {

packages/shared/src/features/opportunity/types.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { LocationType } from './protobuf/util';
1212
import type { Connection } from '../../graphql/common';
1313
import type { Squad } from '../../graphql/sources';
1414
import type { TopReader } from '../../components/badges/TopReaderBadge';
15-
import type { SubscriptionStatus } from '../../lib/plus';
1615

1716
export enum OpportunityMatchStatus {
1817
Pending = 'pending',
@@ -87,9 +86,10 @@ export type OpportunityScreeningAnswer = {
8786
answer: string;
8887
};
8988

90-
type OpportunityFlagsPublic = {
91-
batchSize?: number;
92-
};
89+
type OpportunityFlagsPublic = Partial<{
90+
batchSize: number;
91+
plan: string;
92+
}>;
9393

9494
export type Opportunity = {
9595
id: string;
@@ -107,7 +107,6 @@ export type Opportunity = {
107107
keywords?: Keyword[];
108108
questions?: OpportunityScreeningQuestion[];
109109
feedbackQuestions?: OpportunityFeedbackQuestion[];
110-
subscriptionStatus: SubscriptionStatus;
111110
flags?: OpportunityFlagsPublic;
112111
};
113112

packages/shared/src/lib/schema/opportunity.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,6 @@ export const opportunityEditInfoSchema = z.object({
6060
),
6161
),
6262
}),
63-
organization: z
64-
.object({
65-
name: z.string().nonempty().max(60),
66-
})
67-
.nullish(),
6863
});
6964

7065
export const createOpportunityEditContentSchema = ({
@@ -111,6 +106,9 @@ export const opportunityEditQuestionsSchema = z.object({
111106

112107
export const opportunityEditStep1Schema = opportunityEditInfoSchema.extend({
113108
content: opportunityEditContentSchema.shape.content,
109+
organization: z.object({
110+
name: z.string().nonempty(),
111+
}),
114112
});
115113

116114
export const opportunityEditStep2Schema = opportunityEditQuestionsSchema.extend(

packages/webapp/pages/recruiter/[opportunityId]/matches/index.tsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReactElement } from 'react';
22
import React from 'react';
33
import { useRouter } from 'next/router';
4-
import { useMutation, useQueryClient } from '@tanstack/react-query';
4+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
55
import { ConnectHeader } from '@dailydotdev/shared/src/components/recruiter/ConnectHeader';
66
import { ConnectProgress } from '@dailydotdev/shared/src/components/recruiter/ConnectProgress';
77
import { Loader } from '@dailydotdev/shared/src/components/Loader';
@@ -25,13 +25,53 @@ import {
2525
import { GenericLoaderSpinner } from '@dailydotdev/shared/src/components/utilities/loaders';
2626
import { IconSize } from '@dailydotdev/shared/src/components/Icon';
2727

28+
import { opportunityByIdOptions } from '@dailydotdev/shared/src/features/opportunity/queries';
29+
import { oneMinute } from '@dailydotdev/shared/src/lib/dateFormat';
30+
import { transactionRefetchIntervalMs } from '@dailydotdev/shared/src/graphql/njord';
31+
import { OpportunityState } from '@dailydotdev/shared/src/features/opportunity/protobuf/opportunity';
2832
import { getLayout } from '../../../../components/layouts/RecruiterSelfServeLayout';
2933

3034
function RecruiterMatchesPage(): ReactElement {
3135
const router = useRouter();
3236
const { opportunityId } = router.query;
3337
const queryClient = useQueryClient();
3438

39+
const { data: opportunity } = useQuery({
40+
...opportunityByIdOptions({
41+
id: opportunityId as string,
42+
}),
43+
refetchInterval: (query) => {
44+
const retries = Math.max(
45+
query.state.dataUpdateCount,
46+
query.state.fetchFailureCount,
47+
);
48+
49+
// transactions are mostly processed withing few seconds
50+
// so for now we stop retrying after 1 minute
51+
const maxRetries = (oneMinute * 1000) / transactionRefetchIntervalMs;
52+
53+
if (retries > maxRetries) {
54+
return false;
55+
}
56+
57+
const queryError = query.state.error;
58+
59+
// in case of query error keep refetching until maxRetries is reached
60+
if (queryError) {
61+
return transactionRefetchIntervalMs;
62+
}
63+
64+
const isReadyForMatches =
65+
query.state.data?.state !== OpportunityState.DRAFT;
66+
67+
if (isReadyForMatches) {
68+
return false;
69+
}
70+
71+
return transactionRefetchIntervalMs;
72+
},
73+
});
74+
3575
const { allMatches, isLoading, data } = useOpportunityMatches({
3676
opportunityId: opportunityId as string,
3777
status: 'candidate_accepted',
@@ -95,6 +135,8 @@ function RecruiterMatchesPage(): ReactElement {
95135
);
96136
}
97137

138+
const isReadyForMatches = opportunity?.state !== OpportunityState.DRAFT;
139+
98140
return (
99141
<OpportunityProvider opportunityId={opportunityId as string}>
100142
<div className="flex flex-1 flex-col">
@@ -142,8 +184,10 @@ function RecruiterMatchesPage(): ReactElement {
142184
color={TypographyColor.Tertiary}
143185
center
144186
>
145-
We’re already talking to the right developers for you — all
146-
opt-in, all high-intent.
187+
{isReadyForMatches &&
188+
"We're already talking to the right developers for you — all opt-in, all high-intent."}
189+
{!isReadyForMatches &&
190+
'We are gonna start reaching to developers soon, we are still processing your data and payment...'}
147191
</Typography>
148192
</div>
149193
</>

packages/webapp/pages/recruiter/[opportunityId]/payment.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,15 @@ import { useAuthContext } from '@dailydotdev/shared/src/contexts/AuthContext';
2525
import { recruiterPricesQueryOptions } from '@dailydotdev/shared/src/features/opportunity/graphql';
2626
import { useQuery } from '@tanstack/react-query';
2727
import { Loader } from '@dailydotdev/shared/src/components/Loader';
28+
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
29+
import { useToastNotification } from '@dailydotdev/shared/src/hooks';
2830

2931
const RecruiterPaymentPage = (): ReactElement => {
3032
const router = useRouter();
3133
const checkoutRef = useRef<HTMLDivElement>(null);
3234
const { openCheckout, selectedProduct } = useRecruiterPaymentContext();
3335
const { opportunity } = useOpportunityPreviewContext();
36+
const { displayToast } = useToastNotification();
3437

3538
useEffect(() => {
3639
if (!opportunity) {
@@ -47,6 +50,20 @@ const RecruiterPaymentPage = (): ReactElement => {
4750
});
4851
}, [selectedProduct, openCheckout, opportunity]);
4952

53+
useEffect(() => {
54+
if (!opportunity) {
55+
return;
56+
}
57+
58+
if (!opportunity.organization) {
59+
router.replace(`${webappUrl}recruiter/${opportunity.id}/prepare`);
60+
61+
displayToast(
62+
'Organization info missing, please enter it to proceed with payment.',
63+
);
64+
}
65+
}, [displayToast, opportunity, router]);
66+
5067
const handleBack = () => {
5168
router.back();
5269
};

packages/webapp/pages/recruiter/[opportunityId]/prepare.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,54 @@ import {
1111
} from '@dailydotdev/shared/src/components/opportunity/OpportunityEditContext';
1212
import { useRouter } from 'next/router';
1313
import { webappUrl } from '@dailydotdev/shared/src/lib/constants';
14+
import { opportunityEditStep1Schema } from '@dailydotdev/shared/src/lib/schema/opportunity';
15+
import { usePrompt } from '@dailydotdev/shared/src/hooks/usePrompt';
16+
import { labels } from '@dailydotdev/shared/src/lib/labels';
1417
import { getLayout } from '../../../components/layouts/RecruiterSelfServeLayout';
1518
import JobPage from '../../jobs/[id]';
1619

1720
function PreparePage(): ReactElement {
1821
const router = useRouter();
19-
const { opportunityId } = useOpportunityEditContext();
22+
const { opportunityId, onValidateOpportunity } = useOpportunityEditContext();
23+
const { showPrompt } = usePrompt();
2024

2125
return (
2226
<div className="flex flex-1 flex-col">
2327
<RecruiterHeader
2428
headerButton={{
2529
text: 'Outreach settings',
26-
onClick: () => {
30+
onClick: async () => {
31+
const result = onValidateOpportunity({
32+
schema: opportunityEditStep1Schema,
33+
});
34+
35+
if (result.error) {
36+
await showPrompt({
37+
title: labels.opportunity.requiredMissingNotice.title,
38+
description: (
39+
<div className="flex flex-col gap-4">
40+
<span>
41+
{labels.opportunity.requiredMissingNotice.description}
42+
</span>
43+
<ul className="text-text-tertiary">
44+
{result.error.issues.map((issue) => {
45+
const path = issue.path.join('.');
46+
47+
return <li key={path}>{path}</li>;
48+
})}
49+
</ul>
50+
</div>
51+
),
52+
okButton: {
53+
className: '!w-full',
54+
title: labels.opportunity.requiredMissingNotice.okButton,
55+
},
56+
cancelButton: null,
57+
});
58+
59+
return;
60+
}
61+
2762
router.push(`${webappUrl}recruiter/${opportunityId}/plans`);
2863
},
2964
}}

0 commit comments

Comments
 (0)