Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/modules/billing/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export class Billing implements BillingNamespace {
}

getPlans = async (params?: GetPlansParams): Promise<ClerkPaginatedResponse<BillingPlanResource>> => {
const { for: forParam, ...safeParams } = params || {};
const searchParams = { ...safeParams, payer_type: forParam === 'organization' ? 'org' : 'user' };
const { for: forParam, org_id, min_seats, ...safeParams } = params || {};
const searchParams = { ...safeParams, payer_type: forParam === 'organization' ? 'org' : 'user', org_id, min_seats };
return await BaseResource._fetch({
path: `${Billing.#pathRoot}/plans`,
method: 'GET',
Expand Down
16 changes: 12 additions & 4 deletions packages/clerk-js/src/core/modules/checkout/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ type CheckoutKey = string & { readonly __tag: 'CheckoutKey' };
/**
* Generate cache key for checkout instance
*/
function cacheKey(options: { userId: string; orgId?: string; planId: string; planPeriod: string }): CheckoutKey {
const { userId, orgId, planId, planPeriod } = options;
return `${userId}-${orgId || 'user'}-${planId}-${planPeriod}` as CheckoutKey;
function cacheKey(options: {
userId: string;
orgId?: string;
planId: string;
planPeriod: string;
seatsQuantity?: number;
}): CheckoutKey {
const { userId, orgId, planId, planPeriod, seatsQuantity } = options;
return `${userId}-${orgId || 'user'}-${planId}-${planPeriod}-${seatsQuantity}` as CheckoutKey;
}

/**
Expand All @@ -26,7 +32,7 @@ const CheckoutSignalCache = new Map<
* Create a checkout instance with the given options
*/
function createCheckoutInstance(clerk: Clerk, options: __experimental_CheckoutOptions): CheckoutSignalValue {
const { for: forOrganization, planId, planPeriod } = options;
const { for: forOrganization, planId, planPeriod, seatsQuantity } = options;

if (clerk.user === null) {
throw new Error('Clerk: User is not authenticated');
Expand All @@ -43,6 +49,7 @@ function createCheckoutInstance(clerk: Clerk, options: __experimental_CheckoutOp
orgId: forOrganization === 'organization' ? clerk.organization?.id : undefined,
planId,
planPeriod,
seatsQuantity,
});

const checkoutInstance = CheckoutSignalCache.get(checkoutKey);
Expand All @@ -56,6 +63,7 @@ function createCheckoutInstance(clerk: Clerk, options: __experimental_CheckoutOp
...(forOrganization === 'organization' ? { orgId: clerk.organization?.id } : {}),
planId,
planPeriod,
seatsQuantity,
});

CheckoutSignalCache.set(checkoutKey, { resource: checkout, signals });
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/components/CheckoutButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const CheckoutButton = withClerk(
const {
planId,
planPeriod,
seatsQuantity,
for: _for,
onSubscriptionComplete,
newSubscriptionRedirectUrl,
Expand Down Expand Up @@ -84,6 +85,7 @@ export const CheckoutButton = withClerk(
return clerk.__internal_openCheckout({
planId,
planPeriod,
seatsQuantity,
for: _for,
onSubscriptionComplete,
newSubscriptionRedirectUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ describe('CheckoutButton', () => {
const props = {
planId: 'test_plan',
planPeriod: 'month' as const,
seatsQuantity: 7,
onSubscriptionComplete: vi.fn(),
newSubscriptionRedirectUrl: '/success',
checkoutProps: {
Expand All @@ -121,6 +122,7 @@ describe('CheckoutButton', () => {
onSubscriptionComplete: props.onSubscriptionComplete,
newSubscriptionRedirectUrl: props.newSubscriptionRedirectUrl,
planPeriod: props.planPeriod,
seatsQuantity: props.seatsQuantity,
}),
);
});
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/errors/clerkApiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export class ClerkAPIError<Meta extends ClerkAPIErrorMeta = any> implements Cler
zxcvbn: json.meta?.zxcvbn,
plan: json.meta?.plan,
isPlanUpgradePossible: json.meta?.is_plan_upgrade_possible,
seatsQuantityToAdd: json.meta?.seats_quantity_to_add,
seatsQuantity: json.meta?.seats_quantity,
} as unknown as Meta,
};
this.code = parsedError.code;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/errors/parseError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export function errorToJSON(error: ClerkAPIError | null): ClerkAPIErrorJSON {
zxcvbn: error?.meta?.zxcvbn,
plan: error?.meta?.plan,
is_plan_upgrade_possible: error?.meta?.isPlanUpgradePossible,
seats_quantity_to_add: error?.meta?.seatsQuantityToAdd,
seats_quantity: error?.meta?.seatsQuantity,
},
};
}
1 change: 1 addition & 0 deletions packages/shared/src/react/contexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export type UseCheckoutOptions = {
* The ID of the Subscription Plan to check out (e.g. `cplan_xxx`).
*/
planId: string;
seatsQuantity?: number;
};

const [CheckoutContext, useCheckoutContext] = createContextAndHook<UseCheckoutOptions>('CheckoutContext');
Expand Down
6 changes: 3 additions & 3 deletions packages/shared/src/react/hooks/useCheckout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type UseCheckoutParams = Parameters<typeof __experimental_CheckoutProvider>[0];
*/
export const useCheckout = (options?: UseCheckoutParams): CheckoutSignalValue => {
const contextOptions = useCheckoutContext();
const { for: forOrganization, planId, planPeriod } = options || contextOptions;
const { for: forOrganization, planId, planPeriod, seatsQuantity } = options || contextOptions;
const organization = useOrganizationBase();
const { isLoaded, user } = useUser();
const clerk = useClerkInstanceContext();
Expand All @@ -33,8 +33,8 @@ export const useCheckout = (options?: UseCheckoutParams): CheckoutSignalValue =>
}

const signal = useCallback(() => {
return clerk.__experimental_checkout({ planId, planPeriod, for: forOrganization });
}, [user?.id, organization?.id, planId, planPeriod, forOrganization]);
return clerk.__experimental_checkout({ planId, planPeriod, for: forOrganization, seatsQuantity });
}, [user?.id, organization?.id, planId, planPeriod, forOrganization, seatsQuantity]);

const subscribe = useCallback(
(callback: () => void) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/types/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export type GetPlansParams = ClerkPaginationParams<{
* The type of payer for the Plans.
*/
for?: ForPayerType;
org_id?: string;
min_seats?: number;
}>;

/**
Expand Down Expand Up @@ -893,6 +895,7 @@ export type CreateCheckoutParams = WithOptionalOrgType<{
* The billing period for the Plan.
*/
planPeriod: BillingSubscriptionPlanPeriod;
seatsQuantity?: number;
}>;

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/types/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export type __experimental_CheckoutOptions = {
for?: ForPayerType;
planPeriod: BillingSubscriptionPlanPeriod;
planId: string;
seatsQuantity?: number;
};

export type CheckoutErrors = {
Expand Down Expand Up @@ -2148,6 +2149,7 @@ export type __internal_CheckoutProps = {
appearance?: ClerkAppearanceTheme;
planId?: string;
planPeriod?: BillingSubscriptionPlanPeriod;
seatsQuantity?: number;
for?: ForPayerType;
onSubscriptionComplete?: () => void;
portalId?: string;
Expand All @@ -2168,6 +2170,7 @@ export type __experimental_CheckoutButtonProps = {
planId: string;
planPeriod?: BillingSubscriptionPlanPeriod;
for?: ForPayerType;
seatsQuantity?: number;
onSubscriptionComplete?: () => void;
checkoutProps?: {
appearance?: ClerkAppearanceTheme;
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/types/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface ClerkAPIErrorJSON {
name: string;
};
is_plan_upgrade_possible?: boolean;
seats_quantity_to_add?: number;
seats_quantity?: number;
};
}

Expand Down Expand Up @@ -63,6 +65,8 @@ export interface ClerkAPIError {
name: string;
};
isPlanUpgradePossible?: boolean;
seatQuantityToAdd?: number;
seatQuantity?: number;
};
}

Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/components/Checkout/CheckoutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ const Initiator = () => {

useEffect(() => {
void checkout.start();
}, []);
}, [checkout]);
return null;
};

const Root = ({ children }: { children: React.ReactNode }) => {
const { planId, planPeriod, for: _for } = useCheckoutContext();
const { planId, planPeriod, for: _for, seatsQuantity } = useCheckoutContext();

return (
<CheckoutProvider
Expand All @@ -29,6 +29,7 @@ const Root = ({ children }: { children: React.ReactNode }) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
planPeriod!
}
seatsQuantity={seatsQuantity}
>
<Initiator />
{children}
Expand Down
33 changes: 33 additions & 0 deletions packages/ui/src/components/Checkout/__tests__/Checkout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,39 @@ describe('Checkout', () => {
});
});

it('passes seatsQuantity to checkout initialization', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['test@clerk.com'] });
f.withBilling();
});

fixtures.clerk.billing.startCheckout.mockResolvedValue({} as any);

render(
<Drawer.Root
open
onOpenChange={() => {}}
>
<Checkout
planId='plan_with_seats'
planPeriod='month'
seatsQuantity={7}
/>
</Drawer.Root>,
{ wrapper },
);

await waitFor(() => {
expect(fixtures.clerk.billing.startCheckout).toHaveBeenCalledWith(
expect.objectContaining({
planId: 'plan_with_seats',
planPeriod: 'month',
seatsQuantity: 7,
}),
);
});
});

it('renders drawer structure and localization correctly', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['test@clerk.com'] });
Expand Down
Loading
Loading