From 95a6efe796b879f2066b5a526fb59c73854c0715 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 18 Nov 2025 18:07:21 +0200 Subject: [PATCH 1/3] chore(share): Add stable keys registry for billing and paginated hooks --- .changeset/flat-grapes-visit.md | 2 + .../shared/src/react/hooks/createCacheKeys.ts | 5 +- packages/shared/src/react/hooks/useAPIKeys.ts | 3 +- .../src/react/hooks/useOrganization.tsx | 9 ++-- .../src/react/hooks/useOrganizationList.tsx | 7 +-- .../src/react/hooks/usePaymentAttempts.tsx | 3 +- .../src/react/hooks/usePaymentMethods.tsx | 3 +- packages/shared/src/react/hooks/usePlans.tsx | 3 +- .../shared/src/react/hooks/useStatements.tsx | 3 +- .../src/react/hooks/useSubscription.rq.tsx | 3 +- .../src/react/hooks/useSubscription.swr.tsx | 3 +- packages/shared/src/react/stable-keys.ts | 53 +++++++++++++++++++ 12 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 .changeset/flat-grapes-visit.md create mode 100644 packages/shared/src/react/stable-keys.ts diff --git a/.changeset/flat-grapes-visit.md b/.changeset/flat-grapes-visit.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/flat-grapes-visit.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/shared/src/react/hooks/createCacheKeys.ts b/packages/shared/src/react/hooks/createCacheKeys.ts index 3119107d1c1..73a485c3e25 100644 --- a/packages/shared/src/react/hooks/createCacheKeys.ts +++ b/packages/shared/src/react/hooks/createCacheKeys.ts @@ -1,13 +1,14 @@ +import type { ResourceCacheStableKey } from '../stable-keys'; + /** * @internal */ export function createCacheKeys< Params, - StableKey extends string, T extends Record = Record, U extends Record | undefined = undefined, >(params: { - stablePrefix: StableKey; + stablePrefix: ResourceCacheStableKey; authenticated: boolean; tracked: T; untracked: U extends { args: Params } ? U : never; diff --git a/packages/shared/src/react/hooks/useAPIKeys.ts b/packages/shared/src/react/hooks/useAPIKeys.ts index 21b27d42498..d6fc14fdebc 100644 --- a/packages/shared/src/react/hooks/useAPIKeys.ts +++ b/packages/shared/src/react/hooks/useAPIKeys.ts @@ -3,6 +3,7 @@ import { eventMethodCalled } from '../../telemetry/events/method-called'; import type { APIKeyResource, GetAPIKeysParams } from '../../types'; import { useAssertWrappedByClerkProvider, useClerkInstanceContext } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import type { PaginatedHookConfig, PaginatedResources } from '../types'; import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; @@ -104,7 +105,7 @@ export function useAPIKeys(params?: T): UseAPIKeysRe pageSize: safeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'apiKeys', + stablePrefix: STABLE_KEYS.API_KEYS_KEY, authenticated: Boolean(clerk.user), tracked: { subject: safeValues.subject, diff --git a/packages/shared/src/react/hooks/useOrganization.tsx b/packages/shared/src/react/hooks/useOrganization.tsx index 3def03a8a6c..b6e716324bd 100644 --- a/packages/shared/src/react/hooks/useOrganization.tsx +++ b/packages/shared/src/react/hooks/useOrganization.tsx @@ -17,6 +17,7 @@ import { useOrganizationContext, useSessionContext, } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import type { PaginatedHookConfig, PaginatedResources, PaginatedResourcesWithDefault } from '../types'; import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; @@ -368,7 +369,7 @@ export function useOrganization(params?: T): Us pageSize: domainSafeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'domains', + stablePrefix: STABLE_KEYS.DOMAINS_KEY, authenticated: Boolean(organization), tracked: { organizationId: organization?.id, @@ -390,7 +391,7 @@ export function useOrganization(params?: T): Us pageSize: membershipRequestSafeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'membershipRequests', + stablePrefix: STABLE_KEYS.MEMBERSHIP_REQUESTS_KEY, authenticated: Boolean(organization), tracked: { organizationId: organization?.id, @@ -412,7 +413,7 @@ export function useOrganization(params?: T): Us pageSize: membersSafeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'members', + stablePrefix: STABLE_KEYS.MEMBERSHIPS_KEY, authenticated: Boolean(organization), tracked: { organizationId: organization?.id, @@ -434,7 +435,7 @@ export function useOrganization(params?: T): Us pageSize: invitationsSafeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'invitations', + stablePrefix: STABLE_KEYS.INVITATIONS_KEY, authenticated: Boolean(organization), tracked: { organizationId: organization?.id, diff --git a/packages/shared/src/react/hooks/useOrganizationList.tsx b/packages/shared/src/react/hooks/useOrganizationList.tsx index acf587991a7..280e9b18363 100644 --- a/packages/shared/src/react/hooks/useOrganizationList.tsx +++ b/packages/shared/src/react/hooks/useOrganizationList.tsx @@ -11,6 +11,7 @@ import type { UserOrganizationInvitationResource, } from '../../types'; import { useAssertWrappedByClerkProvider, useClerkInstanceContext, useUserContext } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import type { PaginatedHookConfig, PaginatedResources, PaginatedResourcesWithDefault } from '../types'; import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; @@ -318,7 +319,7 @@ export function useOrganizationList(params? pageSize: userMembershipsSafeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'userMemberships', + stablePrefix: STABLE_KEYS.USER_MEMBERSHIPS_KEY, authenticated: Boolean(user), tracked: { userId: user?.id, @@ -341,7 +342,7 @@ export function useOrganizationList(params? pageSize: userInvitationsSafeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'userInvitations', + stablePrefix: STABLE_KEYS.USER_INVITATIONS_KEY, authenticated: Boolean(user), tracked: { userId: user?.id, @@ -363,7 +364,7 @@ export function useOrganizationList(params? pageSize: userSuggestionsSafeValues.pageSize, }, keys: createCacheKeys({ - stablePrefix: 'userSuggestions', + stablePrefix: STABLE_KEYS.USER_SUGGESTIONS_KEY, authenticated: Boolean(user), tracked: { userId: user?.id, diff --git a/packages/shared/src/react/hooks/usePaymentAttempts.tsx b/packages/shared/src/react/hooks/usePaymentAttempts.tsx index fd0a4953b05..543d64174e0 100644 --- a/packages/shared/src/react/hooks/usePaymentAttempts.tsx +++ b/packages/shared/src/react/hooks/usePaymentAttempts.tsx @@ -1,5 +1,6 @@ import type { BillingPaymentResource, GetPaymentAttemptsParams } from '../../types'; import { useClerkInstanceContext } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import { createBillingPaginatedHook } from './createBillingPaginatedHook'; /** @@ -7,7 +8,7 @@ import { createBillingPaginatedHook } from './createBillingPaginatedHook'; */ export const usePaymentAttempts = createBillingPaginatedHook({ hookName: 'usePaymentAttempts', - resourceType: 'billing-payment-attempts', + resourceType: STABLE_KEYS.PAYMENT_ATTEMPTS_KEY, useFetcher: () => { const clerk = useClerkInstanceContext(); if (clerk.loaded) { diff --git a/packages/shared/src/react/hooks/usePaymentMethods.tsx b/packages/shared/src/react/hooks/usePaymentMethods.tsx index d97ad7c61f3..a678401f36c 100644 --- a/packages/shared/src/react/hooks/usePaymentMethods.tsx +++ b/packages/shared/src/react/hooks/usePaymentMethods.tsx @@ -1,5 +1,6 @@ import type { BillingPaymentMethodResource, GetPaymentMethodsParams } from '../../types'; import { useOrganizationContext, useUserContext } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import { createBillingPaginatedHook } from './createBillingPaginatedHook'; /** @@ -7,7 +8,7 @@ import { createBillingPaginatedHook } from './createBillingPaginatedHook'; */ export const usePaymentMethods = createBillingPaginatedHook({ hookName: 'usePaymentMethods', - resourceType: 'commerce-payment-methods', + resourceType: STABLE_KEYS.PAYMENT_METHODS_KEY, useFetcher: resource => { const { organization } = useOrganizationContext(); const user = useUserContext(); diff --git a/packages/shared/src/react/hooks/usePlans.tsx b/packages/shared/src/react/hooks/usePlans.tsx index db7e73bfe3c..3231f8809b7 100644 --- a/packages/shared/src/react/hooks/usePlans.tsx +++ b/packages/shared/src/react/hooks/usePlans.tsx @@ -1,5 +1,6 @@ import type { BillingPlanResource, GetPlansParams } from '../../types'; import { useClerkInstanceContext } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import { createBillingPaginatedHook } from './createBillingPaginatedHook'; /** @@ -7,7 +8,7 @@ import { createBillingPaginatedHook } from './createBillingPaginatedHook'; */ export const usePlans = createBillingPaginatedHook({ hookName: 'usePlans', - resourceType: 'billing-plans', + resourceType: STABLE_KEYS.PLANS_KEY, useFetcher: _for => { const clerk = useClerkInstanceContext(); if (!clerk.loaded) { diff --git a/packages/shared/src/react/hooks/useStatements.tsx b/packages/shared/src/react/hooks/useStatements.tsx index 162bd563a99..b25def9ea2f 100644 --- a/packages/shared/src/react/hooks/useStatements.tsx +++ b/packages/shared/src/react/hooks/useStatements.tsx @@ -1,5 +1,6 @@ import type { BillingStatementResource, GetStatementsParams } from '../../types'; import { useClerkInstanceContext } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import { createBillingPaginatedHook } from './createBillingPaginatedHook'; /** @@ -7,7 +8,7 @@ import { createBillingPaginatedHook } from './createBillingPaginatedHook'; */ export const useStatements = createBillingPaginatedHook({ hookName: 'useStatements', - resourceType: 'billing-statements', + resourceType: STABLE_KEYS.STATEMENTS_KEY, useFetcher: () => { const clerk = useClerkInstanceContext(); if (clerk.loaded) { diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx index 3f6d75e8952..9efb114e394 100644 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ b/packages/shared/src/react/hooks/useSubscription.rq.tsx @@ -10,6 +10,7 @@ import { useOrganizationContext, useUserContext, } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; const HOOK_NAME = 'useSubscription'; @@ -25,7 +26,7 @@ export const subscriptionQuery = , U extends R trackedKeys: T; untrackedKeys?: U; }) => { - const stableKey = 'commerce-subscription'; + const stableKey = STABLE_KEYS.SUBSCRIPTION_KEY; const { trackedKeys, untrackedKeys } = params; return { queryKey: [stableKey, trackedKeys, untrackedKeys] as const, diff --git a/packages/shared/src/react/hooks/useSubscription.swr.tsx b/packages/shared/src/react/hooks/useSubscription.swr.tsx index 792074818b9..5a08ddcfc37 100644 --- a/packages/shared/src/react/hooks/useSubscription.swr.tsx +++ b/packages/shared/src/react/hooks/useSubscription.swr.tsx @@ -9,6 +9,7 @@ import { useOrganizationContext, useUserContext, } from '../contexts'; +import { STABLE_KEYS } from '../stable-keys'; import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; const hookName = 'useSubscription'; @@ -40,7 +41,7 @@ export function useSubscription(params?: UseSubscriptionParams): SubscriptionRes const swr = useSWR( isEnabled ? { - type: 'commerce-subscription', + type: STABLE_KEYS.SUBSCRIPTION_KEY, userId: user?.id, args: { orgId: isOrganization ? organization?.id : undefined }, } diff --git a/packages/shared/src/react/stable-keys.ts b/packages/shared/src/react/stable-keys.ts new file mode 100644 index 00000000000..d126bef9ef4 --- /dev/null +++ b/packages/shared/src/react/stable-keys.ts @@ -0,0 +1,53 @@ +// Keys for `useOrganizationList` +const USER_MEMBERSHIPS_KEY = 'userMemberships'; +const USER_INVITATIONS_KEY = 'userInvitations'; +const USER_SUGGESTIONS_KEY = 'userSuggestions'; + +// Keys for `useOrganization` +const DOMAINS_KEY = 'domains'; +const MEMBERSHIP_REQUESTS_KEY = 'membershipRequests'; +const MEMBERSHIPS_KEY = 'memberships'; +const INVITATIONS_KEY = 'invitations'; + +// Keys for `useAPIKeys` +const API_KEYS_KEY = 'apiKeys'; + +// Keys for `usePlans` +const PLANS_KEY = 'plans'; + +// Keys for `useSubscription` +const SUBSCRIPTION_KEY = 'commerce-subscription'; + +// Keys for `usePaymentMethods` +const PAYMENT_METHODS_KEY = 'commerce-payment-methods'; + +// Keys for `usePaymentAttempts` +const PAYMENT_ATTEMPTS_KEY = 'billing-payment-attempts'; + +// Keys for `useStatements` +const STATEMENTS_KEY = 'billing-statements'; + +export const STABLE_KEYS = { + // Keys for `useOrganizationList` + USER_MEMBERSHIPS_KEY, + USER_INVITATIONS_KEY, + USER_SUGGESTIONS_KEY, + + // Keys for `useOrganization` + DOMAINS_KEY, + MEMBERSHIP_REQUESTS_KEY, + MEMBERSHIPS_KEY, + INVITATIONS_KEY, + + // Keys for billing + PLANS_KEY, + SUBSCRIPTION_KEY, + PAYMENT_METHODS_KEY, + PAYMENT_ATTEMPTS_KEY, + STATEMENTS_KEY, + + // Keys for `useAPIKeys` + API_KEYS_KEY, +} as const; + +export type ResourceCacheStableKey = (typeof STABLE_KEYS)[keyof typeof STABLE_KEYS]; From 719bb7cbd727f92de44300360db2325054ad15db Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 18 Nov 2025 18:16:13 +0200 Subject: [PATCH 2/3] fix test --- .../src/react/hooks/__tests__/usePagesOrInfinite.spec.ts | 7 ++++++- .../shared/src/react/hooks/createBillingPaginatedHook.tsx | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts b/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts index 7d456bd4316..19cf0d3ce95 100644 --- a/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts +++ b/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts @@ -2,6 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createDeferredPromise } from '../../../utils/createDeferredPromise'; +import type { ResourceCacheStableKey } from '../../stable-keys'; import { createCacheKeys } from '../createCacheKeys'; import { usePagesOrInfinite } from '../usePagesOrInfinite'; import { createMockClerk, createMockQueryClient } from './mocks/clerk'; @@ -43,7 +44,11 @@ const buildKeys = >( authenticated = true, ) => createCacheKeys({ - stablePrefix, + // Casting to ResourceCacheStableKey to satisfy the type checker, + // it is fine because we only want to limit the types to ensure our stable keys + // do not diverge when consumed from other pacakges. + // Since this is a test mocking most things we can safely ignore the type checker. + stablePrefix: stablePrefix as ResourceCacheStableKey, authenticated, tracked, untracked: { args: params }, diff --git a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx index 8af8b6c3956..ffd07b42e30 100644 --- a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx +++ b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx @@ -6,6 +6,7 @@ import { useOrganizationContext, useUserContext, } from '../contexts'; +import type { ResourceCacheStableKey } from '../stable-keys'; import type { PagesOrInfiniteOptions, PaginatedHookConfig, PaginatedResources } from '../types'; import { createCacheKeys } from './createCacheKeys'; import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; @@ -15,7 +16,7 @@ import { usePagesOrInfinite, useWithSafeValues } from './usePagesOrInfinite'; */ type BillingHookConfig = { hookName: string; - resourceType: string; + resourceType: ResourceCacheStableKey; useFetcher: ( param: ForPayerType, ) => ((params: TParams & { orgId?: string }) => Promise>) | undefined; From 42530a0e71ab24315719cea2efe5877d53667c1a Mon Sep 17 00:00:00 2001 From: panteliselef Date: Tue, 18 Nov 2025 18:26:36 +0200 Subject: [PATCH 3/3] update billing keys --- .../hooks/__tests__/createBillingPaginatedHook.spec.tsx | 5 +++-- packages/shared/src/react/stable-keys.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx b/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx index d482c968cdb..6431b98a428 100644 --- a/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx @@ -2,6 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ClerkResource } from '../../../types'; +import type { ResourceCacheStableKey } from '../../stable-keys'; import { createBillingPaginatedHook } from '../createBillingPaginatedHook'; import { createMockClerk, createMockOrganization, createMockQueryClient, createMockUser } from './mocks/clerk'; import { wrapper } from './wrapper'; @@ -33,13 +34,13 @@ const useFetcherMock = vi.fn(() => fetcherMock); const useDummyAuth = createBillingPaginatedHook({ hookName: 'useDummyAuth', - resourceType: 'dummy', + resourceType: 'dummy' as ResourceCacheStableKey, useFetcher: useFetcherMock, }); const useDummyUnauth = createBillingPaginatedHook({ hookName: 'useDummyUnauth', - resourceType: 'dummy', + resourceType: 'dummy' as ResourceCacheStableKey, useFetcher: useFetcherMock, options: { unauthenticated: true }, }); diff --git a/packages/shared/src/react/stable-keys.ts b/packages/shared/src/react/stable-keys.ts index d126bef9ef4..bf1a5e73b63 100644 --- a/packages/shared/src/react/stable-keys.ts +++ b/packages/shared/src/react/stable-keys.ts @@ -13,13 +13,13 @@ const INVITATIONS_KEY = 'invitations'; const API_KEYS_KEY = 'apiKeys'; // Keys for `usePlans` -const PLANS_KEY = 'plans'; +const PLANS_KEY = 'billing-plans'; // Keys for `useSubscription` -const SUBSCRIPTION_KEY = 'commerce-subscription'; +const SUBSCRIPTION_KEY = 'billing-subscription'; // Keys for `usePaymentMethods` -const PAYMENT_METHODS_KEY = 'commerce-payment-methods'; +const PAYMENT_METHODS_KEY = 'billing-payment-methods'; // Keys for `usePaymentAttempts` const PAYMENT_ATTEMPTS_KEY = 'billing-payment-attempts';