Skip to content

Commit b7cc04d

Browse files
authored
feat: job payment (#5144)
1 parent 61145e6 commit b7cc04d

14 files changed

Lines changed: 215 additions & 21 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ReactElement } from 'react';
2+
import React from 'react';
3+
import type { RecruiterContextProviderProps } from './types';
4+
import { RecruiterPaymentPaddleContextProvider } from './RecruiterPaymentPaddleContext';
5+
6+
export const RecruiterPaymentContext = ({
7+
children,
8+
...props
9+
}: RecruiterContextProviderProps): ReactElement => {
10+
return (
11+
<RecruiterPaymentPaddleContextProvider {...props}>
12+
{children}
13+
</RecruiterPaymentPaddleContextProvider>
14+
);
15+
};
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { ReactElement } from 'react';
2+
import React, { useEffect, useMemo, useRef, useState } from 'react';
3+
import { useQuery } from '@tanstack/react-query';
4+
import { useRouter } from 'next/router';
5+
import { PurchaseType } from '../../graphql/paddle';
6+
import { usePaddlePayment } from '../../hooks/usePaddlePayment';
7+
import { useLogContext } from '../LogContext';
8+
import { useAuthContext } from '../AuthContext';
9+
import type {
10+
RecruiterPaymentContextData,
11+
RecruiterContextProviderProps,
12+
RecruiterProductOption,
13+
} from './types';
14+
import { RecruiterPaymentContext } from './types';
15+
import { webappUrl } from '../../lib/constants';
16+
import { recruiterPricesQueryOptions } from '../../features/opportunity/graphql';
17+
18+
export const RecruiterPaymentPaddleContextProvider = ({
19+
onCompletion,
20+
origin,
21+
children,
22+
}: RecruiterContextProviderProps): ReactElement => {
23+
const router = useRouter();
24+
const { user, isLoggedIn } = useAuthContext();
25+
const { logEvent } = useLogContext();
26+
const [selectedProduct, setSelectedProduct] =
27+
useState<RecruiterProductOption>();
28+
const logRef = useRef<typeof logEvent>();
29+
logRef.current = logEvent;
30+
31+
const { data: prices } = useQuery(
32+
recruiterPricesQueryOptions({
33+
user,
34+
isLoggedIn,
35+
}),
36+
);
37+
38+
useEffect(() => {
39+
if (!prices?.length) {
40+
return;
41+
}
42+
43+
if (selectedProduct) {
44+
return;
45+
}
46+
47+
setSelectedProduct({
48+
id: prices[0].priceId,
49+
});
50+
}, [prices, selectedProduct]);
51+
52+
const { paddle, openCheckout } = usePaddlePayment({
53+
successCallback: () => {
54+
router.push(
55+
`${webappUrl}recruiter/${router.query.opportunityId}/prepare`,
56+
);
57+
},
58+
priceType: PurchaseType.Recruiter,
59+
});
60+
61+
const contextData = useMemo<RecruiterPaymentContextData>(
62+
() => ({
63+
paddle,
64+
onCompletion,
65+
selectedProduct,
66+
setSelectedProduct,
67+
openCheckout,
68+
origin,
69+
}),
70+
[onCompletion, openCheckout, origin, paddle, selectedProduct],
71+
);
72+
73+
return (
74+
<RecruiterPaymentContext.Provider value={contextData}>
75+
{children}
76+
</RecruiterPaymentContext.Provider>
77+
);
78+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Paddle } from '@paddle/paddle-js';
2+
import type { ReactNode } from 'react';
3+
import { createContext, useContext } from 'react';
4+
import type { OpenCheckoutFn } from '../payment/context';
5+
import type { Origin } from '../../lib/log';
6+
7+
export const RecruiterPaymentContext =
8+
createContext<RecruiterPaymentContextData>(undefined);
9+
10+
export const useRecruiterPaymentContext = (): RecruiterPaymentContextData =>
11+
useContext(RecruiterPaymentContext);
12+
13+
export type RecruiterProductOption = {
14+
id: string;
15+
};
16+
17+
export type ProcessingError = {
18+
title: string;
19+
description?: string;
20+
onRequestClose?: () => void;
21+
};
22+
23+
export type RecruiterPaymentContextData = {
24+
paddle?: Paddle | undefined;
25+
openCheckout?: OpenCheckoutFn<{ opportunity_id: string }>;
26+
onCompletion?: () => void;
27+
selectedProduct?: RecruiterProductOption;
28+
setSelectedProduct: (product: RecruiterProductOption) => void;
29+
error?: ProcessingError;
30+
origin?: Origin;
31+
};
32+
33+
export type RecruiterContextProviderProps = {
34+
children?: ReactNode;
35+
origin?: Origin;
36+
onCompletion?: () => void;
37+
};

packages/shared/src/contexts/payment/context.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ import type { Dispatch, ReactNode, SetStateAction } from 'react';
22
import { createContext, useContext } from 'react';
33
import type { ProductPricingPreview, PurchaseType } from '../../graphql/paddle';
44

5-
export interface OpenCheckoutProps {
5+
export interface OpenCheckoutProps<TCustomData = Record<string, unknown>> {
66
priceId: string;
77
giftToUserId?: string;
8-
customData?: Record<string, unknown>;
8+
customData?: TCustomData;
99
discountId?: string;
1010
quantity?: number;
1111
}
1212

13-
export type OpenCheckoutFn = (props: OpenCheckoutProps) => void;
13+
export type OpenCheckoutFn<TCustomData = Record<string, unknown>> = (
14+
props: OpenCheckoutProps<TCustomData>,
15+
) => void;
1416

1517
export interface PaymentContextData {
1618
openCheckout?: OpenCheckoutFn;

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { gql } from 'graphql-request';
22
import { ORGANIZATION_SHORT_FRAGMENT } from '../organizations/graphql';
3+
import { generateQueryKey, RequestKey, StaleTime } from '../../lib/query';
4+
import type { LoggedUser } from '../../lib/user';
5+
import { fetchPricingPreview, PurchaseType } from '../../graphql/paddle';
36

47
export const OPPORTUNITY_CONTENT_FRAGMENT = gql`
58
fragment OpportunityContentFragment on OpportunityContentBlock {
@@ -125,6 +128,7 @@ export const OPPORTUNITY_FRAGMENT = gql`
125128
feedbackQuestions {
126129
...OpportunityFeedbackQuestionFragment
127130
}
131+
subscriptionStatus
128132
}
129133
${ORGANIZATION_SHORT_FRAGMENT}
130134
${OPPORTUNITY_CONTENT_FRAGMENT}
@@ -603,3 +607,24 @@ export const OPPORTUNITY_STATS_QUERY = gql`
603607
}
604608
}
605609
`;
610+
611+
export const recruiterPricesQueryOptions = ({
612+
isLoggedIn,
613+
user,
614+
}: {
615+
isLoggedIn: boolean;
616+
user: LoggedUser;
617+
}) => {
618+
return {
619+
queryKey: generateQueryKey(
620+
RequestKey.PricePreview,
621+
user,
622+
PurchaseType.Recruiter,
623+
),
624+
queryFn: async () => {
625+
return fetchPricingPreview(PurchaseType.Recruiter);
626+
},
627+
enabled: isLoggedIn,
628+
staleTime: StaleTime.Default,
629+
};
630+
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ 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';
56

67
export const mockOpportunity: Opportunity = {
78
id: '89f3daff-d6bb-4652-8f9c-b9f7254c9af1',
@@ -61,6 +62,7 @@ export const mockOpportunity: Opportunity = {
6162
{ keyword: 'JavaScript' },
6263
{ keyword: 'Tailwind CSS' },
6364
],
65+
subscriptionStatus: SubscriptionStatus.None,
6466
};
6567

6668
export const mockAnonymousUserTableData: OpportunityPreviewContextType = {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ 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';
1516

1617
export enum OpportunityMatchStatus {
1718
Pending = 'pending',
@@ -103,6 +104,7 @@ export type Opportunity = {
103104
keywords?: Keyword[];
104105
questions?: OpportunityScreeningQuestion[];
105106
feedbackQuestions?: OpportunityFeedbackQuestion[];
107+
subscriptionStatus: SubscriptionStatus;
106108
};
107109

108110
export type OpportunityMatchDescription = {

packages/shared/src/graphql/paddle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export enum PurchaseType {
4848
Plus = 'plus',
4949
Organization = 'organization',
5050
Cores = 'cores',
51+
Recruiter = 'recruiter',
5152
}
5253

5354
export enum PlusPlanType {

packages/shared/src/hooks/payment/useStoreKitPayment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const typeToHandler: Record<PurchaseType, WebKitMessageHandlers> = {
2222
cores: WebKitMessageHandlers.IAPCoresPurchase,
2323
plus: WebKitMessageHandlers.IAPSubscriptionRequest,
2424
organization: WebKitMessageHandlers.IAPSubscriptionRequest,
25+
recruiter: WebKitMessageHandlers.IAPSubscriptionRequest, // not used for now on apple side
2526
};
2627

2728
export const useStoreKitPayment = ({

packages/shared/src/hooks/usePaddlePayment.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export const usePaddlePayment = ({
150150
target_type: targetType,
151151
event_name: LogEvent.WarningCheckout,
152152
extra: JSON.stringify({
153-
transaction_id: event?.data.transaction_id,
153+
transaction_id: event?.data?.transaction_id,
154154
}),
155155
});
156156
break;
@@ -159,7 +159,7 @@ export const usePaddlePayment = ({
159159
target_type: targetType,
160160
event_name: LogEvent.ErrorCheckout,
161161
extra: JSON.stringify({
162-
transaction_id: event?.data.transaction_id,
162+
transaction_id: event?.data?.transaction_id,
163163
}),
164164
});
165165
break;
@@ -168,7 +168,7 @@ export const usePaddlePayment = ({
168168
target_type: targetType,
169169
event_name: LogEvent.ErrorPayment,
170170
extra: JSON.stringify({
171-
transaction_id: event?.data.transaction_id,
171+
transaction_id: event?.data?.transaction_id,
172172
}),
173173
});
174174
break;
@@ -201,12 +201,13 @@ export const usePaddlePayment = ({
201201
}, [router, disabledEvents, targetType, isOrganization, isPlusPlan]);
202202

203203
const openCheckout = useCallback(
204-
({
204+
<TCustomData>({
205205
priceId,
206206
giftToUserId,
207207
discountId,
208208
quantity = 1,
209-
}: OpenCheckoutProps) => {
209+
customData: customDataProp,
210+
}: OpenCheckoutProps<TCustomData>) => {
210211
const items: CheckoutLineItem[] = [{ priceId, quantity }];
211212
const customer: CheckoutCustomer = {
212213
...(user?.email
@@ -222,6 +223,7 @@ export const usePaddlePayment = ({
222223
};
223224

224225
const customData = {
226+
...customDataProp,
225227
user_id: giftToUserId ?? user?.id,
226228
tracking_id: trackingId,
227229
...(!!giftToUserId && { gifter_id: user?.id }),

0 commit comments

Comments
 (0)