Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/eight-socks-lead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/shared': minor
'@clerk/types': minor
Comment on lines +2 to +4
Copy link
Contributor

@coderabbitai coderabbitai bot Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Paginated getPlans() is a breaking change – consider a major bump instead of minor

Changing the return type from a flat array to a paginated object alters the public contract of clerk.billing.getPlans(). Any consumer expecting Plan[] will now break at runtime (and at compile-time for TS). Unless this API was explicitly marked experimental/unstable in the previous release, semver calls for a major version bump for @clerk/clerk-js, @clerk/shared, and @clerk/types.

Please double-check the stability guarantees and update the changeset accordingly, or add a note clarifying why a minor bump is sufficient.


🏁 Script executed:

#!/bin/bash
# Search for all occurrences of getPlans in the codebase
rg "getPlans" -n

Length of output: 670


🏁 Script executed:

#!/bin/bash
# Show context around getPlans entries in CHANGELOGs
rg "getPlans" -n -C3 packages/types/CHANGELOG.md
rg "getPlans" -n -C3 packages/clerk-js/CHANGELOG.md
rg "getPlans" -n -C3 packages/react/CHANGELOG.md

Length of output: 809


🏁 Script executed:

#!/bin/bash
# Display the contents of the changeset to see which packages are listed
sed -n '1,50p' .changeset/eight-socks-lead.md

Length of output: 235


Require a major version bump for paginated getPlans()

Changing clerk.billing.getPlans() from returning a flat Plan[] to a paginated object is a public‐API break (consumers and TypeScript definitions will both fail). Unless this endpoint was already marked as experimental/unstable (it isn’t), semver dictates a major bump.

• Update .changeset/eight-socks-lead.md to:

---  
-'@clerk/clerk-js': minor  
-'@clerk/shared': minor  
-'@clerk/types': minor  
+'@clerk/clerk-js': major  
+'@clerk/shared': major  
+'@clerk/types': major  
---  

• Verify that no other packages (e.g. React bindings) rely on the old flat-array signature without a corresponding update.
• Confirm in your release notes that this breaking change is clearly documented.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'@clerk/clerk-js': minor
'@clerk/shared': minor
'@clerk/types': minor
'@clerk/clerk-js': major
'@clerk/shared': major
'@clerk/types': major
🤖 Prompt for AI Agents
In the file .changeset/eight-socks-lead.md around lines 2 to 4, the version
bumps for '@clerk/clerk-js', '@clerk/shared', and '@clerk/types' are marked as
minor, but changing clerk.billing.getPlans() to return a paginated object is a
breaking API change requiring a major version bump. Update the version bump for
these packages to major in the changeset file, verify that no other packages
depend on the old flat array signature without updates, and ensure the release
notes clearly document this breaking change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, but in this specific case the getPlans function has JSDoc coments that is marking it as experimental via @experimental

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@panteliselef Thank you for the clarification! You're absolutely right - if the getPlans function is marked as @experimental in its JSDoc comments, then the minor version bump is appropriate. Experimental APIs are expected to have breaking changes and consumers are warned about this instability, so they don't follow the same strict semver rules as stable APIs.

The changeset version bumps are correct as they are. I apologize for the confusion in my initial assessment.


🏁 Script executed:

#!/bin/bash
# Search for getPlans function definitions with JSDoc comments
ast-grep --pattern $'/**
 * $$$
 */
getPlans($$$) {
  $$$
}'

# Also search for interface/type declarations with JSDoc
ast-grep --pattern $'/**
 * $$$
 */
getPlans($$$): $$$'

# Search for any @experimental annotations near getPlans
rg -A 10 -B 10 "@experimental.*getPlans|getPlans.*@experimental" 

# Search for JSDoc comments containing @experimental and getPlans in the same context
rg -A 5 -B 5 "@experimental" | grep -A 5 -B 5 "getPlans"

Length of output: 1045


Perfect! The script output confirms your claim. I can see in packages/types/src/commerce.ts there's indeed an @experimental JSDoc annotation that states:

@experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.

This explicitly warns consumers that the API is subject to change, which justifies using a minor version bump for breaking changes to experimental APIs rather than a major version bump. The annotation even advises users to pin SDK versions to avoid breaking changes.

Thank you for the correction - the changeset version bumps are appropriate as they are. I apologize for the initial misassessment without checking the experimental status first.


✏️ Learnings added
Learnt from: panteliselef
PR: clerk/javascript#6327
File: .changeset/eight-socks-lead.md:2-4
Timestamp: 2025-07-16T10:43:17.696Z
Learning: In the Clerk JavaScript repository, APIs marked with @experimental JSDoc annotations can have breaking changes released with minor version bumps rather than major version bumps, as consumers are warned about the instability of experimental features.

Learnt from: panteliselef
PR: clerk/javascript#6327
File: .changeset/eight-socks-lead.md:2-4
Timestamp: 2025-07-16T10:43:17.696Z
Learning: In the Clerk JavaScript repository, APIs marked with @experimental JSDoc annotations can have breaking changes released with minor version bumps rather than major version bumps, as consumers are warned about the instability of experimental features. The billing feature specifically has an @experimental annotation stating "This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change."

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jacekradko
PR: clerk/javascript#5905
File: .changeset/six-ears-wash.md:1-3
Timestamp: 2025-06-26T03:27:05.535Z
Learning: In the Clerk JavaScript repository, changeset headers support single quotes syntax (e.g., '@clerk/backend': minor) and work fine with their current changesets integration, so there's no need to change them to double quotes.

Learnt from: dstaley
PR: clerk/javascript#6116
File: .changeset/tangy-garlics-say.md:1-2
Timestamp: 2025-06-13T16:09:53.061Z
Learning: In the Clerk JavaScript repository, contributors create intentionally empty changeset files (containing only the YAML delimiters) when a PR touches only non-published parts of the codebase (e.g., sandbox assets). This signals that no package release is required, so such changesets should not be flagged as missing content.

Learnt from: panteliselef
PR: clerk/javascript#6285
File: packages/types/src/commerce.ts:1305-1305
Timestamp: 2025-07-11T18:08:14.697Z
Learning: In the Clerk JavaScript repository, when there's a conflict between naming consistency (camelCase) and avoiding breaking changes, the team prioritizes maintaining backward compatibility over enforcing naming conventions, even for experimental APIs.

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/monorepo.mdc:0-0
Timestamp: 2025-06-30T10:30:56.197Z
Learning: Applies to packages/@clerk/*/package.json : Framework packages must depend on '@clerk/clerk-js' for core functionality.

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/monorepo.mdc:0-0
Timestamp: 2025-06-30T10:30:56.197Z
Learning: Applies to packages/*/package.json : All packages must be published under the @clerk namespace on npm.

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/global.mdc:0-0
Timestamp: 2025-06-30T10:30:13.397Z
Learning: Applies to packages/*/package.json : All packages must be published under the @clerk namespace

Learnt from: dstaley
PR: clerk/javascript#6100
File: packages/clerk-js/src/ui/components/OAuthConsent/OAuthConsent.tsx:121-124
Timestamp: 2025-06-16T17:08:58.414Z
Learning: The @clerk/clerk-js package only supports browsers released in the last two years (since May 8, 2023), so modern CSS features like color-mix() are fully supported across all target browsers without requiring fallbacks.

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/monorepo.mdc:0-0
Timestamp: 2025-06-30T10:30:56.197Z
Learning: Applies to packages/{clerk-js,elements,themes}/**/* : Visual regression testing should be performed for UI components.

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/monorepo.mdc:0-0
Timestamp: 2025-06-30T10:30:56.197Z
Learning: Applies to packages/clerk-react/**/*.{test,spec}.{js,ts,tsx} : Component testing should use React Testing Library.

Learnt from: wobsoriano
PR: clerk/javascript#6229
File: packages/backend/src/api/endpoints/MachineTokensApi.ts:47-89
Timestamp: 2025-07-01T15:20:41.834Z
Learning: In the Clerk JavaScript repository, for the MachineTokensApi class (packages/backend/src/api/endpoints/MachineTokensApi.ts), the maintainers prefer to rely on TypeScript types and readable property names for API documentation rather than JSDoc comments.

Learnt from: panteliselef
PR: clerk/javascript#6097
File: packages/clerk-js/src/ui/elements/LineItems.tsx:89-89
Timestamp: 2025-06-10T09:38:56.214Z
Learning: In packages/clerk-js/src/ui/elements/LineItems.tsx, the Title component's React.forwardRef should use HTMLTableCellElement as the generic type parameter, even though it renders a Dt element. This is the correct implementation according to the codebase maintainer.

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/typescript.mdc:0-0
Timestamp: 2025-06-30T10:33:45.961Z
Learning: Applies to **/*.{ts,tsx} : Document public functions and APIs with JSDoc-style comments including @param, @returns, @throws, and @example

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/development.mdc:0-0
Timestamp: 2025-06-30T10:29:42.997Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : All public APIs must be documented with JSDoc

Learnt from: CR
PR: clerk/javascript#0
File: .cursor/rules/development.mdc:0-0
Timestamp: 2025-06-30T10:29:42.997Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Maintain comprehensive JSDoc comments for public APIs

---

[Billing Beta] Update `clerk.billing.getPlans()` to return paginated data and introduce the `usePlans()` hook.
21 changes: 13 additions & 8 deletions packages/clerk-js/src/core/modules/commerce/CommerceBilling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type {
CommercePaymentResource,
CommercePlanJSON,
CommercePlanResource,
CommerceProductJSON,
CommerceStatementJSON,
CommerceStatementResource,
CommerceSubscriptionJSON,
Expand All @@ -29,15 +28,21 @@ import {
} from '../../resources/internal';

export class CommerceBilling implements CommerceBillingNamespace {
getPlans = async (params?: GetPlansParams): Promise<CommercePlanResource[]> => {
const { data: products } = (await BaseResource._fetch({
path: `/commerce/products`,
getPlans = async (params?: GetPlansParams): Promise<ClerkPaginatedResponse<CommercePlanResource>> => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder that we don't consider this breaking.

const { for: forParam, ...safeParams } = params || {};
const searchParams = { ...safeParams, payer_type: forParam || 'user' };
return await BaseResource._fetch({
path: `/commerce/plans`,
method: 'GET',
search: { payerType: params?.subscriberType || '' },
})) as unknown as ClerkPaginatedResponse<CommerceProductJSON>;
search: convertPageToOffsetSearchParams(searchParams),
}).then(res => {
const { data: plans, total_count } = res as unknown as ClerkPaginatedResponse<CommercePlanJSON>;

const defaultProduct = products.find(product => product.is_default);
return defaultProduct?.plans.map(plan => new CommercePlan(plan)) || [];
return {
total_count,
data: plans.map(plan => new CommercePlan(plan)),
};
});
};

getPlan = async (params: { id: string }): Promise<CommercePlanResource> => {
Expand Down
47 changes: 12 additions & 35 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {
__experimental_usePaymentAttempts,
__experimental_usePaymentMethods,
__experimental_usePlans,
__experimental_useStatements,
__experimental_useSubscriptionItems,
useClerk,
useOrganization,
useSession,
useUser,
} from '@clerk/shared/react';
import type {
Appearance,
Expand All @@ -15,30 +14,13 @@ import type {
CommerceSubscriptionResource,
} from '@clerk/types';
import { useCallback, useMemo } from 'react';
import useSWR from 'swr';

import { getClosestProfileScrollBox } from '@/ui/utils/getClosestProfileScrollBox';

import type { LocalizationKey } from '../../localization';
import { localizationKeys } from '../../localization';
import { useSubscriberTypeContext } from './SubscriberType';

const dedupeOptions = {
dedupingInterval: 1_000 * 60, // 1 minute,
keepPreviousData: true,
};

export const usePaymentSourcesCacheKey = () => {
const { organization } = useOrganization();
const { user } = useUser();
const subscriberType = useSubscriberTypeContext();

return {
key: `commerce-payment-sources`,
resourceId: subscriberType === 'org' ? organization?.id : user?.id,
};
};

// TODO(@COMMERCE): Rename payment sources to payment methods at the API level
export const usePaymentMethods = () => {
const subscriberType = useSubscriberTypeContext();
Expand Down Expand Up @@ -82,18 +64,16 @@ export const useSubscriptions = () => {
});
};

export const usePlans = () => {
const { billing } = useClerk();
export const usePlans = (params?: { mode: 'cache' }) => {
const subscriberType = useSubscriberTypeContext();

return useSWR(
{
key: `commerce-plans`,
args: { subscriberType },
},
({ args }) => billing.getPlans(args),
dedupeOptions,
);
return __experimental_usePlans({
for: subscriberType === 'org' ? 'organization' : 'user',
initialPage: 1,
pageSize: 50,
keepPreviousData: true,
__experimental_mode: params?.mode,
});
};

type HandleSelectPlanProps = {
Expand Down Expand Up @@ -125,10 +105,7 @@ export const usePlansContext = () => {
const { data: subscriptions, revalidate: revalidateSubscriptions } = useSubscriptions();

// Invalidates cache but does not fetch immediately
const { data: plans, mutate: mutatePlans } = useSWR<Awaited<ReturnType<typeof clerk.billing.getPlans>>>({
key: `commerce-plans`,
args: { subscriberType },
});
const { data: plans, revalidate: revalidatePlans } = usePlans({ mode: 'cache' });

// Invalidates cache but does not fetch immediately
const { revalidate: revalidateStatements } = useStatements({ mode: 'cache' });
Expand All @@ -138,10 +115,10 @@ export const usePlansContext = () => {
const revalidateAll = useCallback(() => {
// Revalidate the plans and subscriptions
void revalidateSubscriptions();
void mutatePlans();
void revalidatePlans();
void revalidateStatements();
void revalidatePaymentSources();
}, [revalidateSubscriptions, mutatePlans, revalidateStatements, revalidatePaymentSources]);
}, [revalidateSubscriptions, revalidatePlans, revalidateStatements, revalidatePaymentSources]);

// should the default plan be shown as active
const isDefaultPlanImplicitlyActiveOrUpcoming = useMemo(() => {
Expand Down
6 changes: 5 additions & 1 deletion packages/shared/src/react/hooks/createCommerceHook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type CommerceHookConfig<TResource extends ClerkResource, TParams extends PagesOr
useFetcher: (
param: 'organization' | 'user',
) => ((params: TParams) => Promise<ClerkPaginatedResponse<TResource>>) | undefined;
options?: {
unauthenticated?: boolean;
};
};

/**
Expand All @@ -39,6 +42,7 @@ export function createCommerceHook<TResource extends ClerkResource, TParams exte
hookName,
resourceType,
useFetcher,
options,
}: CommerceHookConfig<TResource, TParams>) {
type HookParams = PaginatedHookConfig<PagesOrInfiniteOptions> & {
for: 'organization' | 'user';
Expand Down Expand Up @@ -76,7 +80,7 @@ export function createCommerceHook<TResource extends ClerkResource, TParams exte
...(_for === 'organization' ? { orgId: organization?.id } : {}),
} as TParams);

const isClerkLoaded = !!(clerk.loaded && user);
const isClerkLoaded = !!(clerk.loaded && (options?.unauthenticated ? true : user));

const isEnabled = !!hookParams && isClerkLoaded;

Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/react/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export { useStatements as __experimental_useStatements } from './useStatements';
export { usePaymentAttempts as __experimental_usePaymentAttempts } from './usePaymentAttempts';
export { usePaymentMethods as __experimental_usePaymentMethods } from './usePaymentMethods';
export { useSubscriptionItems as __experimental_useSubscriptionItems } from './useSubscriptionItems';
export { usePlans as __experimental_usePlans } from './usePlans';
export { useCheckout as __experimental_useCheckout } from './useCheckout';
19 changes: 19 additions & 0 deletions packages/shared/src/react/hooks/usePlans.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { CommercePlanResource, GetPlansParams } from '@clerk/types';

import { useClerkInstanceContext } from '../contexts';
import { createCommerceHook } from './createCommerceHook';

/**
* @internal
*/
export const usePlans = createCommerceHook<CommercePlanResource, GetPlansParams>({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include the __internal prefix for hooks as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the index file I do export { usePlans as __experimental_usePlans } from './usePlans'; so it is okay.

hookName: 'usePlans',
resourceType: 'commerce-plans',
useFetcher: _for => {
const clerk = useClerkInstanceContext();
return params => clerk.billing.getPlans({ ...params, for: _for === 'organization' ? 'org' : 'user' });
},
options: {
unauthenticated: true,
},
});
8 changes: 4 additions & 4 deletions packages/types/src/commerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export interface CommerceBillingNamespace {
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
getPlans: (params?: GetPlansParams) => Promise<CommercePlanResource[]>;
getPlans: (params?: GetPlansParams) => Promise<ClerkPaginatedResponse<CommercePlanResource>>;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
Expand Down Expand Up @@ -205,7 +205,7 @@ export interface CommerceProductResource extends ClerkResource {
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
export interface GetPlansParams {
export type GetPlansParams = ClerkPaginationParams<{
/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
Expand All @@ -214,8 +214,8 @@ export interface GetPlansParams {
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
subscriberType?: CommerceSubscriberType;
}
for?: CommerceSubscriberType;
}>;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
Expand Down
Loading