From eb0bae11360e0ca04a05eecc7de36b6e0b1b06b8 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sun, 14 Dec 2025 12:58:30 -0600 Subject: [PATCH 1/3] feat(shared): Remove SWR --- packages/shared/global.d.ts | 1 - packages/shared/package.json | 5 +- .../billing/useInitializePaymentMethod.rq.tsx | 76 ----- .../useInitializePaymentMethod.swr.tsx | 60 ---- .../billing/useInitializePaymentMethod.tsx | 78 +++++- .../react/billing/useStripeClerkLibs.rq.tsx | 37 --- .../react/billing/useStripeClerkLibs.swr.tsx | 39 --- .../src/react/billing/useStripeClerkLibs.tsx | 41 ++- .../src/react/billing/useStripeLoader.rq.tsx | 51 ---- .../src/react/billing/useStripeLoader.swr.tsx | 46 ---- .../src/react/billing/useStripeLoader.tsx | 53 +++- packages/shared/src/react/clerk-swr.ts | 7 - .../createBillingPaginatedHook.spec.tsx | 17 +- .../react/hooks/__tests__/useAPIKeys.spec.tsx | 26 +- .../__tests__/usePagesOrInfinite.spec.ts | 6 +- .../react/hooks/__tests__/usePlans.spec.tsx | 14 +- .../hooks/__tests__/useSubscription.spec.tsx | 26 +- .../hooks/createBillingPaginatedHook.tsx | 2 +- packages/shared/src/react/hooks/index.ts | 2 +- .../shared/src/react/hooks/useAPIKeys.swr.tsx | 141 ---------- packages/shared/src/react/hooks/useAPIKeys.ts | 2 - .../{useAPIKeys.rq.tsx => useAPIKeys.tsx} | 0 .../src/react/hooks/usePagesOrInfinite.rq.tsx | 257 ----------------- .../react/hooks/usePagesOrInfinite.swr.tsx | 236 ---------------- .../src/react/hooks/usePagesOrInfinite.tsx | 259 +++++++++++++++++- .../react/hooks/usePaymentAttemptQuery.rq.tsx | 57 ---- .../hooks/usePaymentAttemptQuery.swr.tsx | 48 ---- .../react/hooks/usePaymentAttemptQuery.tsx | 58 +++- .../react/hooks/usePlanDetailsQuery.rq.tsx | 45 --- .../react/hooks/usePlanDetailsQuery.swr.tsx | 45 --- .../src/react/hooks/usePlanDetailsQuery.tsx | 46 +++- .../src/react/hooks/useStatementQuery.rq.tsx | 59 ---- .../src/react/hooks/useStatementQuery.swr.tsx | 50 ---- .../src/react/hooks/useStatementQuery.tsx | 60 +++- .../src/react/hooks/useSubscription.rq.tsx | 79 ------ .../src/react/hooks/useSubscription.swr.tsx | 74 ----- .../src/react/hooks/useSubscription.tsx | 77 +++++- .../react/providers/SWRConfigCompat.rq.tsx | 8 - .../react/providers/SWRConfigCompat.swr.tsx | 10 - .../src/react/providers/SWRConfigCompat.tsx | 9 +- .../shared/src/types/virtual-data-hooks.d.ts | 12 - packages/shared/tsconfig.json | 14 +- packages/shared/tsdown.config.mts | 41 --- packages/shared/vitest.config.mts | 37 --- packages/shared/vitest.setup.mts | 1 - pnpm-lock.yaml | 9 +- 46 files changed, 688 insertions(+), 1633 deletions(-) delete mode 100644 packages/shared/src/react/billing/useInitializePaymentMethod.rq.tsx delete mode 100644 packages/shared/src/react/billing/useInitializePaymentMethod.swr.tsx delete mode 100644 packages/shared/src/react/billing/useStripeClerkLibs.rq.tsx delete mode 100644 packages/shared/src/react/billing/useStripeClerkLibs.swr.tsx delete mode 100644 packages/shared/src/react/billing/useStripeLoader.rq.tsx delete mode 100644 packages/shared/src/react/billing/useStripeLoader.swr.tsx delete mode 100644 packages/shared/src/react/clerk-swr.ts delete mode 100644 packages/shared/src/react/hooks/useAPIKeys.swr.tsx delete mode 100644 packages/shared/src/react/hooks/useAPIKeys.ts rename packages/shared/src/react/hooks/{useAPIKeys.rq.tsx => useAPIKeys.tsx} (100%) delete mode 100644 packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx delete mode 100644 packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx delete mode 100644 packages/shared/src/react/hooks/usePaymentAttemptQuery.rq.tsx delete mode 100644 packages/shared/src/react/hooks/usePaymentAttemptQuery.swr.tsx delete mode 100644 packages/shared/src/react/hooks/usePlanDetailsQuery.rq.tsx delete mode 100644 packages/shared/src/react/hooks/usePlanDetailsQuery.swr.tsx delete mode 100644 packages/shared/src/react/hooks/useStatementQuery.rq.tsx delete mode 100644 packages/shared/src/react/hooks/useStatementQuery.swr.tsx delete mode 100644 packages/shared/src/react/hooks/useSubscription.rq.tsx delete mode 100644 packages/shared/src/react/hooks/useSubscription.swr.tsx delete mode 100644 packages/shared/src/react/providers/SWRConfigCompat.rq.tsx delete mode 100644 packages/shared/src/react/providers/SWRConfigCompat.swr.tsx delete mode 100644 packages/shared/src/types/virtual-data-hooks.d.ts diff --git a/packages/shared/global.d.ts b/packages/shared/global.d.ts index ffc87dfc746..9bac5865c4b 100644 --- a/packages/shared/global.d.ts +++ b/packages/shared/global.d.ts @@ -4,7 +4,6 @@ declare const JS_PACKAGE_VERSION: string; declare const UI_PACKAGE_VERSION: string; declare const __DEV__: boolean; declare const __BUILD_DISABLE_RHC__: boolean; -declare const __CLERK_USE_RQ__: boolean; interface ImportMetaEnv { readonly [key: string]: string; diff --git a/packages/shared/package.json b/packages/shared/package.json index 9b72d07a26f..49df1d222f5 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -124,18 +124,17 @@ "test:coverage": "vitest --collectCoverage && open coverage/lcov-report/index.html" }, "dependencies": { + "@tanstack/query-core": "5.87.4", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", - "std-env": "^3.9.0", - "swr": "2.3.4" + "std-env": "^3.9.0" }, "devDependencies": { "@base-org/account": "catalog:module-manager", "@coinbase/wallet-sdk": "catalog:module-manager", "@stripe/react-stripe-js": "3.1.1", "@stripe/stripe-js": "5.6.0", - "@tanstack/query-core": "5.87.4", "@types/glob-to-regexp": "0.4.4", "@types/js-cookie": "3.0.6", "@zxcvbn-ts/core": "catalog:module-manager", diff --git a/packages/shared/src/react/billing/useInitializePaymentMethod.rq.tsx b/packages/shared/src/react/billing/useInitializePaymentMethod.rq.tsx deleted file mode 100644 index 5c58075c6b6..00000000000 --- a/packages/shared/src/react/billing/useInitializePaymentMethod.rq.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useCallback, useMemo } from 'react'; - -import type { BillingInitializedPaymentMethodResource, ForPayerType } from '../../types'; -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import { useOrganizationContext, useUserContext } from '../contexts'; -import { useBillingHookEnabled } from '../hooks/useBillingHookEnabled'; - -type InitializePaymentMethodOptions = { - for?: ForPayerType; -}; - -export type UseInitializePaymentMethodResult = { - initializedPaymentMethod: BillingInitializedPaymentMethodResource | undefined; - initializePaymentMethod: () => Promise; -}; - -/** - * @internal - */ -function useInitializePaymentMethod(options?: InitializePaymentMethodOptions): UseInitializePaymentMethodResult { - const { for: forType } = options ?? {}; - const { organization } = useOrganizationContext(); - const user = useUserContext(); - - const resource = forType === 'organization' ? organization : user; - - const billingEnabled = useBillingHookEnabled(options); - - const queryKey = useMemo(() => { - return ['billing-payment-method-initialize', { resourceId: resource?.id }] as const; - }, [resource?.id]); - - const isEnabled = Boolean(resource?.id) && billingEnabled; - - const query = useClerkQuery({ - queryKey, - queryFn: async () => { - if (!resource) { - return undefined; - } - - return resource.initializePaymentMethod({ - gateway: 'stripe', - }); - }, - enabled: isEnabled, - staleTime: 1_000 * 60, - refetchOnWindowFocus: false, - placeholderData: defineKeepPreviousDataFn(true), - }); - - const [queryClient] = useClerkQueryClient(); - - const initializePaymentMethod = useCallback(async () => { - if (!resource) { - return undefined; - } - - const result = await resource.initializePaymentMethod({ - gateway: 'stripe', - }); - - queryClient.setQueryData(queryKey, result); - - return result; - }, [queryClient, queryKey, resource]); - - return { - initializedPaymentMethod: query.data ?? undefined, - initializePaymentMethod, - }; -} - -export { useInitializePaymentMethod as __internal_useInitializePaymentMethod }; diff --git a/packages/shared/src/react/billing/useInitializePaymentMethod.swr.tsx b/packages/shared/src/react/billing/useInitializePaymentMethod.swr.tsx deleted file mode 100644 index 8a4a3df8f35..00000000000 --- a/packages/shared/src/react/billing/useInitializePaymentMethod.swr.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useEffect } from 'react'; -import useSWRMutation from 'swr/mutation'; - -import type { BillingInitializedPaymentMethodResource, ForPayerType } from '../../types'; -import { useOrganizationContext, useUserContext } from '../contexts'; - -type InitializePaymentMethodOptions = { - for?: ForPayerType; -}; - -export type UseInitializePaymentMethodResult = { - initializedPaymentMethod: BillingInitializedPaymentMethodResource | undefined; - initializePaymentMethod: () => Promise; -}; - -/** - * This is the existing implementation of the payment method initializer using SWR. - * It is kept here for backwards compatibility until our next major version. - * - * @internal - */ -function useInitializePaymentMethod(options?: InitializePaymentMethodOptions): UseInitializePaymentMethodResult { - const { for: forType = 'user' } = options ?? {}; - const { organization } = useOrganizationContext(); - const user = useUserContext(); - - const resource = forType === 'organization' ? organization : user; - - const { data, trigger } = useSWRMutation( - resource?.id - ? { - key: 'billing-payment-method-initialize', - resourceId: resource.id, - for: forType, - } - : null, - () => { - return resource?.initializePaymentMethod({ - gateway: 'stripe', - }); - }, - ); - - useEffect(() => { - if (!resource?.id) { - return; - } - - trigger().catch(() => { - // ignore errors - }); - }, [resource?.id, trigger]); - - return { - initializedPaymentMethod: data, - initializePaymentMethod: trigger, - }; -} - -export { useInitializePaymentMethod as __internal_useInitializePaymentMethod }; diff --git a/packages/shared/src/react/billing/useInitializePaymentMethod.tsx b/packages/shared/src/react/billing/useInitializePaymentMethod.tsx index 1373b76c409..5c58075c6b6 100644 --- a/packages/shared/src/react/billing/useInitializePaymentMethod.tsx +++ b/packages/shared/src/react/billing/useInitializePaymentMethod.tsx @@ -1,2 +1,76 @@ -export type { UseInitializePaymentMethodResult } from 'virtual:data-hooks/useInitializePaymentMethod'; -export { __internal_useInitializePaymentMethod } from 'virtual:data-hooks/useInitializePaymentMethod'; +import { useCallback, useMemo } from 'react'; + +import type { BillingInitializedPaymentMethodResource, ForPayerType } from '../../types'; +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import { useOrganizationContext, useUserContext } from '../contexts'; +import { useBillingHookEnabled } from '../hooks/useBillingHookEnabled'; + +type InitializePaymentMethodOptions = { + for?: ForPayerType; +}; + +export type UseInitializePaymentMethodResult = { + initializedPaymentMethod: BillingInitializedPaymentMethodResource | undefined; + initializePaymentMethod: () => Promise; +}; + +/** + * @internal + */ +function useInitializePaymentMethod(options?: InitializePaymentMethodOptions): UseInitializePaymentMethodResult { + const { for: forType } = options ?? {}; + const { organization } = useOrganizationContext(); + const user = useUserContext(); + + const resource = forType === 'organization' ? organization : user; + + const billingEnabled = useBillingHookEnabled(options); + + const queryKey = useMemo(() => { + return ['billing-payment-method-initialize', { resourceId: resource?.id }] as const; + }, [resource?.id]); + + const isEnabled = Boolean(resource?.id) && billingEnabled; + + const query = useClerkQuery({ + queryKey, + queryFn: async () => { + if (!resource) { + return undefined; + } + + return resource.initializePaymentMethod({ + gateway: 'stripe', + }); + }, + enabled: isEnabled, + staleTime: 1_000 * 60, + refetchOnWindowFocus: false, + placeholderData: defineKeepPreviousDataFn(true), + }); + + const [queryClient] = useClerkQueryClient(); + + const initializePaymentMethod = useCallback(async () => { + if (!resource) { + return undefined; + } + + const result = await resource.initializePaymentMethod({ + gateway: 'stripe', + }); + + queryClient.setQueryData(queryKey, result); + + return result; + }, [queryClient, queryKey, resource]); + + return { + initializedPaymentMethod: query.data ?? undefined, + initializePaymentMethod, + }; +} + +export { useInitializePaymentMethod as __internal_useInitializePaymentMethod }; diff --git a/packages/shared/src/react/billing/useStripeClerkLibs.rq.tsx b/packages/shared/src/react/billing/useStripeClerkLibs.rq.tsx deleted file mode 100644 index e2dd394b24c..00000000000 --- a/packages/shared/src/react/billing/useStripeClerkLibs.rq.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import type { loadStripe } from '@stripe/stripe-js'; - -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import { useBillingHookEnabled } from '../hooks/useBillingHookEnabled'; -import { useClerk } from '../hooks/useClerk'; - -type LoadStripeFn = typeof loadStripe; - -type StripeClerkLibs = { - loadStripe: LoadStripeFn; -}; - -/** - * @internal - */ -function useStripeClerkLibs(): StripeClerkLibs | null { - const clerk = useClerk(); - - const billingEnabled = useBillingHookEnabled(); - - const query = useClerkQuery({ - queryKey: ['clerk-stripe-sdk'], - queryFn: async () => { - const loadStripe = (await clerk.__internal_loadStripeJs()) as LoadStripeFn; - return { loadStripe }; - }, - enabled: billingEnabled, - staleTime: Infinity, - refetchOnWindowFocus: false, - placeholderData: defineKeepPreviousDataFn(true), - }); - - return query.data ?? null; -} - -export { useStripeClerkLibs as __internal_useStripeClerkLibs }; diff --git a/packages/shared/src/react/billing/useStripeClerkLibs.swr.tsx b/packages/shared/src/react/billing/useStripeClerkLibs.swr.tsx deleted file mode 100644 index 820144b4dff..00000000000 --- a/packages/shared/src/react/billing/useStripeClerkLibs.swr.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { loadStripe } from '@stripe/stripe-js'; - -import { useSWR } from '../clerk-swr'; -import { useClerk } from '../hooks/useClerk'; - -type LoadStripeFn = typeof loadStripe; - -type StripeClerkLibs = { - loadStripe: LoadStripeFn; -}; - -export type UseStripeClerkLibsResult = StripeClerkLibs | null; - -/** - * This is the existing implementation of the Stripe libraries loader using SWR. - * It is kept here for backwards compatibility until our next major version. - * - * @internal - */ -function useStripeClerkLibs(): UseStripeClerkLibsResult { - const clerk = useClerk(); - - const swr = useSWR( - 'clerk-stripe-sdk', - async () => { - const loadStripe = (await clerk.__internal_loadStripeJs()) as LoadStripeFn; - return { loadStripe }; - }, - { - keepPreviousData: true, - revalidateOnFocus: false, - dedupingInterval: Infinity, - }, - ); - - return swr.data ?? null; -} - -export { useStripeClerkLibs as __internal_useStripeClerkLibs }; diff --git a/packages/shared/src/react/billing/useStripeClerkLibs.tsx b/packages/shared/src/react/billing/useStripeClerkLibs.tsx index 3a55aaca025..197df86743a 100644 --- a/packages/shared/src/react/billing/useStripeClerkLibs.tsx +++ b/packages/shared/src/react/billing/useStripeClerkLibs.tsx @@ -1,2 +1,39 @@ -export type { UseStripeClerkLibsResult } from 'virtual:data-hooks/useStripeClerkLibs'; -export { __internal_useStripeClerkLibs } from 'virtual:data-hooks/useStripeClerkLibs'; +import type { loadStripe } from '@stripe/stripe-js'; + +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import { useBillingHookEnabled } from '../hooks/useBillingHookEnabled'; +import { useClerk } from '../hooks/useClerk'; + +type LoadStripeFn = typeof loadStripe; + +type StripeClerkLibs = { + loadStripe: LoadStripeFn; +}; + +export type UseStripeClerkLibsResult = StripeClerkLibs | null; + +/** + * @internal + */ +function useStripeClerkLibs(): UseStripeClerkLibsResult { + const clerk = useClerk(); + + const billingEnabled = useBillingHookEnabled(); + + const query = useClerkQuery({ + queryKey: ['clerk-stripe-sdk'], + queryFn: async () => { + const loadStripe = (await clerk.__internal_loadStripeJs()) as LoadStripeFn; + return { loadStripe }; + }, + enabled: billingEnabled, + staleTime: Infinity, + refetchOnWindowFocus: false, + placeholderData: defineKeepPreviousDataFn(true), + }); + + return query.data ?? null; +} + +export { useStripeClerkLibs as __internal_useStripeClerkLibs }; diff --git a/packages/shared/src/react/billing/useStripeLoader.rq.tsx b/packages/shared/src/react/billing/useStripeLoader.rq.tsx deleted file mode 100644 index 59dee615f6b..00000000000 --- a/packages/shared/src/react/billing/useStripeLoader.rq.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import type { Stripe } from '@stripe/stripe-js'; -import { useMemo } from 'react'; - -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import { useBillingHookEnabled } from '../hooks/useBillingHookEnabled'; -import type { UseStripeClerkLibsResult } from './useStripeClerkLibs'; - -type StripeLoaderOptions = { - stripeClerkLibs: UseStripeClerkLibsResult; - externalGatewayId?: string; - stripePublishableKey?: string; -}; - -export type UseStripeLoaderResult = Stripe | null | undefined; - -/** - * @internal - */ -function useStripeLoader(options: StripeLoaderOptions): UseStripeLoaderResult { - const { stripeClerkLibs, externalGatewayId, stripePublishableKey } = options; - - const queryKey = useMemo(() => { - return ['stripe-sdk', { externalGatewayId, stripePublishableKey }] as const; - }, [externalGatewayId, stripePublishableKey]); - - const billingEnabled = useBillingHookEnabled({ authenticated: true }); - - const isEnabled = Boolean(stripeClerkLibs && externalGatewayId && stripePublishableKey) && billingEnabled; - - const query = useClerkQuery({ - queryKey, - queryFn: () => { - if (!stripeClerkLibs || !externalGatewayId || !stripePublishableKey) { - return null; - } - - return stripeClerkLibs.loadStripe(stripePublishableKey, { - stripeAccount: externalGatewayId, - }); - }, - enabled: isEnabled, - staleTime: 1_000 * 60, - refetchOnWindowFocus: false, - placeholderData: defineKeepPreviousDataFn(true), - }); - - return query.data; -} - -export { useStripeLoader as __internal_useStripeLoader }; diff --git a/packages/shared/src/react/billing/useStripeLoader.swr.tsx b/packages/shared/src/react/billing/useStripeLoader.swr.tsx deleted file mode 100644 index 57e396dcddc..00000000000 --- a/packages/shared/src/react/billing/useStripeLoader.swr.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import type { Stripe } from '@stripe/stripe-js'; - -import { useSWR } from '../clerk-swr'; -import type { UseStripeClerkLibsResult } from './useStripeClerkLibs'; - -type StripeLoaderOptions = { - stripeClerkLibs: UseStripeClerkLibsResult; - externalGatewayId?: string; - stripePublishableKey?: string; -}; - -export type UseStripeLoaderResult = Stripe | null | undefined; - -/** - * This is the existing implementation of the Stripe instance loader using SWR. - * It is kept here for backwards compatibility until our next major version. - * - * @internal - */ -function useStripeLoader(options: StripeLoaderOptions): UseStripeLoaderResult { - const { stripeClerkLibs, externalGatewayId, stripePublishableKey } = options; - - const swr = useSWR( - stripeClerkLibs && externalGatewayId && stripePublishableKey - ? { - key: 'stripe-sdk', - externalGatewayId, - stripePublishableKey, - } - : null, - ({ stripePublishableKey, externalGatewayId }) => { - return stripeClerkLibs?.loadStripe(stripePublishableKey, { - stripeAccount: externalGatewayId, - }); - }, - { - keepPreviousData: true, - revalidateOnFocus: false, - dedupingInterval: 1_000 * 60, - }, - ); - - return swr.data; -} - -export { useStripeLoader as __internal_useStripeLoader }; diff --git a/packages/shared/src/react/billing/useStripeLoader.tsx b/packages/shared/src/react/billing/useStripeLoader.tsx index 689fed791c4..59dee615f6b 100644 --- a/packages/shared/src/react/billing/useStripeLoader.tsx +++ b/packages/shared/src/react/billing/useStripeLoader.tsx @@ -1,2 +1,51 @@ -export type { UseStripeLoaderResult } from 'virtual:data-hooks/useStripeLoader'; -export { __internal_useStripeLoader } from 'virtual:data-hooks/useStripeLoader'; +import type { Stripe } from '@stripe/stripe-js'; +import { useMemo } from 'react'; + +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import { useBillingHookEnabled } from '../hooks/useBillingHookEnabled'; +import type { UseStripeClerkLibsResult } from './useStripeClerkLibs'; + +type StripeLoaderOptions = { + stripeClerkLibs: UseStripeClerkLibsResult; + externalGatewayId?: string; + stripePublishableKey?: string; +}; + +export type UseStripeLoaderResult = Stripe | null | undefined; + +/** + * @internal + */ +function useStripeLoader(options: StripeLoaderOptions): UseStripeLoaderResult { + const { stripeClerkLibs, externalGatewayId, stripePublishableKey } = options; + + const queryKey = useMemo(() => { + return ['stripe-sdk', { externalGatewayId, stripePublishableKey }] as const; + }, [externalGatewayId, stripePublishableKey]); + + const billingEnabled = useBillingHookEnabled({ authenticated: true }); + + const isEnabled = Boolean(stripeClerkLibs && externalGatewayId && stripePublishableKey) && billingEnabled; + + const query = useClerkQuery({ + queryKey, + queryFn: () => { + if (!stripeClerkLibs || !externalGatewayId || !stripePublishableKey) { + return null; + } + + return stripeClerkLibs.loadStripe(stripePublishableKey, { + stripeAccount: externalGatewayId, + }); + }, + enabled: isEnabled, + staleTime: 1_000 * 60, + refetchOnWindowFocus: false, + placeholderData: defineKeepPreviousDataFn(true), + }); + + return query.data; +} + +export { useStripeLoader as __internal_useStripeLoader }; diff --git a/packages/shared/src/react/clerk-swr.ts b/packages/shared/src/react/clerk-swr.ts deleted file mode 100644 index 5d03ac36156..00000000000 --- a/packages/shared/src/react/clerk-swr.ts +++ /dev/null @@ -1,7 +0,0 @@ -'use client'; - -// TODO: Replace these SWR re-exports with react-query equivalents. -export * from 'swr'; - -export { default as useSWR } from 'swr'; -export { default as useSWRInfinite } from 'swr/infinite'; diff --git a/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx b/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx index 7c9032cb449..1c1394a07b6 100644 --- a/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/createBillingPaginatedHook.spec.tsx @@ -308,16 +308,7 @@ describe('createBillingPaginatedHook', () => { expect(params).toStrictEqual({ initialPage: 1, pageSize: 5 }); }); - if (__CLERK_USE_RQ__) { - expect(result.current.isLoading).toBe(false); - } else { - // Attention: We are forcing fetcher to be executed instead of setting the key to null - // because SWR will continue to display the cached data when the key is null and `keepPreviousData` is true. - // This means that SWR will update the loading state to true even if the fetcher is not called, - // because the key changes from `{..., userId: 'user_1'}` to `{..., userId: undefined}`. - await waitFor(() => expect(result.current.isLoading).toBe(true)); - await waitFor(() => expect(result.current.isLoading).toBe(false)); - } + expect(result.current.isLoading).toBe(false); // Data should be cleared even with keepPreviousData: true // The key difference here vs usePagesOrInfinite test: userId in cache key changes @@ -543,11 +534,7 @@ describe('createBillingPaginatedHook', () => { await result.current.paginated.revalidate(); }); - if (__CLERK_USE_RQ__) { - await waitFor(() => expect(fetcherMock.mock.calls.length).toBeGreaterThanOrEqual(2)); - } else { - await waitFor(() => expect(fetcherMock).toHaveBeenCalledTimes(1)); - } + await waitFor(() => expect(fetcherMock.mock.calls.length).toBeGreaterThanOrEqual(2)); }); }); }); diff --git a/packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx b/packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx index 075b794a406..570a3ce1d3d 100644 --- a/packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/useAPIKeys.spec.tsx @@ -89,13 +89,7 @@ describe('useApiKeys', () => { await result.current.paginated.revalidate(); }); - const isRQ = Boolean((globalThis as any).__CLERK_USE_RQ__); - - if (isRQ) { - await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(2)); - } else { - await waitFor(() => expect(getAllSpy).toHaveBeenCalledTimes(1)); - } + await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(2)); }); it('handles revalidation with different pageSize configurations', async () => { @@ -125,15 +119,8 @@ describe('useApiKeys', () => { await result.current.small.revalidate(); }); - const isRQ = Boolean((globalThis as any).__CLERK_USE_RQ__); - await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(1)); - - if (isRQ) { - await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(2)); - } else { - expect(getAllSpy).toHaveBeenCalledTimes(2); - } + await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(2)); }); it('handles revalidation with different query filters', async () => { @@ -163,15 +150,8 @@ describe('useApiKeys', () => { await result.current.defaultQuery.revalidate(); }); - const isRQ = Boolean((globalThis as any).__CLERK_USE_RQ__); - await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(1)); - - if (isRQ) { - await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(2)); - } else { - expect(getAllSpy).toHaveBeenCalledTimes(2); - } + await waitFor(() => expect(getAllSpy.mock.calls.length).toBeGreaterThanOrEqual(2)); }); it('does not cascade revalidation across different subjects', async () => { diff --git a/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts b/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts index 31e26912eaf..dd0d2420bf2 100644 --- a/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts +++ b/packages/shared/src/react/hooks/__tests__/usePagesOrInfinite.spec.ts @@ -722,11 +722,7 @@ describe('usePagesOrInfinite - revalidate behavior', () => { await result.current.paginated.revalidate(); }); - if (__CLERK_USE_RQ__) { - await waitFor(() => expect(fetcher.mock.calls.length).toBeGreaterThanOrEqual(2)); - } else { - await waitFor(() => expect(fetcher).toHaveBeenCalledTimes(1)); - } + await waitFor(() => expect(fetcher.mock.calls.length).toBeGreaterThanOrEqual(2)); }); }); diff --git a/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx b/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx index 4ba3a45ae30..439b5ff2bfd 100644 --- a/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/usePlans.spec.tsx @@ -261,19 +261,9 @@ describe('usePlans', () => { await result.current.userPlans.revalidate(); }); - const isRQ = Boolean((globalThis as any).__CLERK_USE_RQ__); const calls = getPlansSpy.mock.calls.map(call => call[0]?.for); - if (isRQ) { - await waitFor(() => expect(getPlansSpy.mock.calls.length).toBeGreaterThanOrEqual(1)); - expect(calls.every(value => value === 'user')).toBe(true); - } else { - await waitFor(() => expect(getPlansSpy.mock.calls.length).toBe(1)); - expect(getPlansSpy.mock.calls[0][0]).toEqual( - expect.objectContaining({ - for: 'user', - }), - ); - } + await waitFor(() => expect(getPlansSpy.mock.calls.length).toBeGreaterThanOrEqual(1)); + expect(calls.every(value => value === 'user')).toBe(true); }); }); diff --git a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx index 1563127d543..06bdf71cc85 100644 --- a/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx +++ b/packages/shared/src/react/hooks/__tests__/useSubscription.spec.tsx @@ -105,14 +105,7 @@ describe('useSubscription', () => { mockUser = null; rerender(); - if (__CLERK_USE_RQ__) { - await waitFor(() => expect(result.current.data).toBeUndefined()); - } else { - // Assert that SWR will flip to fetching because the fetcherFN runs, but it forces `null` when userId is falsy. - await waitFor(() => expect(result.current.isFetching).toBe(true)); - // The fetcher returns null when userId is falsy, so data should become null - await waitFor(() => expect(result.current.data).toBeNull()); - } + await waitFor(() => expect(result.current.data).toBeUndefined()); expect(getSubscriptionSpy).toHaveBeenCalledTimes(1); expect(result.current.isFetching).toBe(false); @@ -133,15 +126,7 @@ describe('useSubscription', () => { mockUser = null; rerender({ kp: true }); - if (__CLERK_USE_RQ__) { - await waitFor(() => expect(result.current.data).toBeUndefined()); - } else { - // Assert that SWR will flip to fetching because the fetcherFN runs, but it forces `null` when userId is falsy. - await waitFor(() => expect(result.current.isFetching).toBe(true)); - - // The fetcher returns null when userId is falsy, so data should become null - await waitFor(() => expect(result.current.data).toBeNull()); - } + await waitFor(() => expect(result.current.data).toBeUndefined()); expect(getSubscriptionSpy).toHaveBeenCalledTimes(1); expect(result.current.isFetching).toBe(false); @@ -169,12 +154,7 @@ describe('useSubscription', () => { await waitFor(() => expect(result.current.isFetching).toBe(true)); - // Slight difference in behavior between SWR and React Query, but acceptable for the migration. - if (__CLERK_USE_RQ__) { - await waitFor(() => expect(result.current.isLoading).toBe(false)); - } else { - await waitFor(() => expect(result.current.isLoading).toBe(true)); - } + await waitFor(() => expect(result.current.isLoading).toBe(false)); expect(result.current.data).toEqual({ id: 'sub_org_org_1' }); deferred.resolve({ id: 'sub_org_org_2' }); diff --git a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx index 425b05e97b8..9d658db2969 100644 --- a/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx +++ b/packages/shared/src/react/hooks/createBillingPaginatedHook.tsx @@ -141,7 +141,7 @@ export function createBillingPaginatedHook; - -/** - * @internal - */ -export type UseAPIKeysReturn = PaginatedResources< - APIKeyResource, - T extends { infinite: true } ? true : false ->; - -/** - * @internal - * - * The `useAPIKeys()` hook provides access to paginated API keys for the current user or organization. - * - * @example - * ### Basic usage with default pagination - * - * ```tsx - * const { data, isLoading, page, pageCount, fetchNext, fetchPrevious } = useAPIKeys({ - * subject: 'user_123', - * pageSize: 10, - * initialPage: 1, - * }); - * ``` - * - * @example - * ### With search query - * - * ```tsx - * const [searchValue, setSearchValue] = useState(''); - * const debouncedSearch = useDebounce(searchValue, 500); - * - * const { data, isLoading } = useAPIKeys({ - * subject: 'user_123', - * query: debouncedSearch.trim(), - * pageSize: 10, - * }); - * ``` - * - * @example - * ### Infinite scroll - * - * ```tsx - * const { data, isLoading, fetchNext, hasNextPage } = useAPIKeys({ - * subject: 'user_123', - * infinite: true, - * }); - * ``` - */ -export function useAPIKeys(params?: T): UseAPIKeysReturn { - useAssertWrappedByClerkProvider('useAPIKeys'); - - const safeValues = useWithSafeValues(params, { - initialPage: 1, - pageSize: 10, - keepPreviousData: false, - infinite: false, - subject: '', - query: '', - enabled: true, - } as UseAPIKeysParams); - - const clerk = useClerkInstanceContext(); - - clerk.telemetry?.record(eventMethodCalled('useAPIKeys')); - - const hookParams: GetAPIKeysParams = { - initialPage: safeValues.initialPage, - pageSize: safeValues.pageSize, - ...(safeValues.subject ? { subject: safeValues.subject } : {}), - ...(safeValues.query ? { query: safeValues.query } : {}), - }; - - const isEnabled = (safeValues.enabled ?? true) && clerk.loaded; - - const result = usePagesOrInfinite({ - fetcher: clerk.apiKeys?.getAll - ? (params: GetAPIKeysParams) => clerk.apiKeys.getAll({ ...params, subject: safeValues.subject }) - : undefined, - config: { - keepPreviousData: safeValues.keepPreviousData, - infinite: safeValues.infinite, - enabled: isEnabled, - isSignedIn: Boolean(clerk.user), - initialPage: safeValues.initialPage, - pageSize: safeValues.pageSize, - }, - keys: createCacheKeys({ - stablePrefix: STABLE_KEYS.API_KEYS_KEY, - authenticated: Boolean(clerk.user), - tracked: { - subject: safeValues.subject, - }, - untracked: { - args: hookParams, - }, - }), - }) as UseAPIKeysReturn; - - const { mutate } = useSWRConfig(); - - // Invalidate all cache entries for this user or organization - const invalidateAll = useCallback(() => { - return mutate(key => { - if (!key || typeof key !== 'object') { - return false; - } - // Match all apiKeys cache entries for this user or organization, regardless of page, pageSize, or query - return 'type' in key && key.type === 'apiKeys' && 'subject' in key && key.subject === safeValues.subject; - }); - }, [mutate, safeValues.subject]); - - return { - ...result, - revalidate: invalidateAll as any, - }; -} diff --git a/packages/shared/src/react/hooks/useAPIKeys.ts b/packages/shared/src/react/hooks/useAPIKeys.ts deleted file mode 100644 index cd899c1e737..00000000000 --- a/packages/shared/src/react/hooks/useAPIKeys.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useAPIKeys } from 'virtual:data-hooks/useAPIKeys'; -export type { UseAPIKeysParams, UseAPIKeysReturn } from './useAPIKeys.rq'; diff --git a/packages/shared/src/react/hooks/useAPIKeys.rq.tsx b/packages/shared/src/react/hooks/useAPIKeys.tsx similarity index 100% rename from packages/shared/src/react/hooks/useAPIKeys.rq.tsx rename to packages/shared/src/react/hooks/useAPIKeys.tsx diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx deleted file mode 100644 index f4d01862445..00000000000 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.rq.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; - -import type { ClerkPaginatedResponse } from '../../types'; -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; -import { useClerkInfiniteQuery } from '../clerk-rq/useInfiniteQuery'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import type { CacheSetter, ValueOrSetter } from '../types'; -import { useClearQueriesOnSignOut, withInfiniteKey } from './useClearQueriesOnSignOut'; -import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types'; -import { useWithSafeValues } from './usePagesOrInfinite.shared'; - -export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { - const { fetcher, config, keys } = params; - - const [paginatedPage, setPaginatedPage] = useState(config.initialPage ?? 1); - - // Cache initialPage and initialPageSize until unmount - const initialPageRef = useRef(config.initialPage ?? 1); - const pageSizeRef = useRef(config.pageSize ?? 10); - - const enabled = config.enabled ?? true; - const isSignedIn = config.isSignedIn; - const triggerInfinite = config.infinite ?? false; - const cacheMode = config.__experimental_mode === 'cache'; - const keepPreviousData = config.keepPreviousData ?? false; - - const [queryClient] = useClerkQueryClient(); - - // Compute the actual enabled state for queries (considering all conditions) - const queriesEnabled = enabled && Boolean(fetcher) && !cacheMode && isSignedIn !== false; - - // Force re-render counter for cache-only updates - const [forceUpdateCounter, setForceUpdateCounter] = useState(0); - const forceUpdate = useCallback((updater: (n: number) => number) => { - setForceUpdateCounter(updater); - }, []); - - // Non-infinite mode: single page query - const pagesQueryKey = useMemo(() => { - const [stablePrefix, authenticated, tracked, untracked] = keys.queryKey; - - return [ - stablePrefix, - authenticated, - tracked, - { - ...untracked, - args: { - ...untracked.args, - initialPage: paginatedPage, - pageSize: pageSizeRef.current, - }, - }, - ] as const; - }, [keys.queryKey, paginatedPage]); - - const singlePageQuery = useClerkQuery({ - queryKey: pagesQueryKey, - queryFn: ({ queryKey }) => { - const { args } = queryKey[3]; - - if (!fetcher) { - return undefined as any; - } - - return fetcher(args); - }, - staleTime: 60_000, - enabled: queriesEnabled && !triggerInfinite, - // Use placeholderData to keep previous data while fetching new page - placeholderData: defineKeepPreviousDataFn(keepPreviousData), - }); - - // Infinite mode: accumulate pages - const infiniteQueryKey = useMemo(() => { - const [stablePrefix, authenticated, tracked, untracked] = keys.queryKey; - - return [stablePrefix + '-inf', authenticated, tracked, untracked] as const; - }, [keys.queryKey]); - - const infiniteQuery = useClerkInfiniteQuery, any, any, typeof infiniteQueryKey, any>({ - queryKey: infiniteQueryKey, - initialPageParam: config.initialPage ?? 1, - getNextPageParam: (lastPage, allPages, lastPageParam) => { - const total = lastPage?.total_count ?? 0; - const consumed = (allPages.length + (config.initialPage ? config.initialPage - 1 : 0)) * (config.pageSize ?? 10); - return consumed < total ? (lastPageParam as number) + 1 : undefined; - }, - queryFn: ({ pageParam, queryKey }) => { - const { args } = queryKey[3]; - if (!fetcher) { - return undefined as any; - } - return fetcher({ ...args, initialPage: pageParam, pageSize: pageSizeRef.current }); - }, - staleTime: 60_000, - enabled: queriesEnabled && triggerInfinite, - }); - - useClearQueriesOnSignOut({ - isSignedOut: isSignedIn === false, - authenticated: keys.authenticated, - stableKeys: withInfiniteKey(keys.stableKey), - onCleanup: () => { - // Reset paginated page to initial - setPaginatedPage(initialPageRef.current); - - // Force re-render to reflect cache changes - void Promise.resolve().then(() => forceUpdate(n => n + 1)); - }, - }); - - // Compute data, count and page from the same data source to ensure consistency - const computedValues = useMemo(() => { - if (triggerInfinite) { - // Read from query data first, fallback to cache - const cachedData = queryClient.getQueryData<{ pages?: Array> }>(infiniteQueryKey); - const pages = queriesEnabled ? (infiniteQuery.data?.pages ?? cachedData?.pages ?? []) : (cachedData?.pages ?? []); - - // Ensure pages is always an array and filter out null/undefined pages - const validPages = Array.isArray(pages) ? pages.filter(Boolean) : []; - - return { - data: - validPages - .map((a: ClerkPaginatedResponse) => a?.data) - .flat() - .filter(Boolean) ?? [], - count: validPages[validPages.length - 1]?.total_count ?? 0, - page: validPages.length > 0 ? validPages.length : initialPageRef.current, - }; - } - - // When query is disabled (via enabled flag), the hook's data is stale, so only read from cache - // This ensures that after cache clearing, we return consistent empty state - const pageData = queriesEnabled - ? (singlePageQuery.data ?? queryClient.getQueryData>(pagesQueryKey)) - : queryClient.getQueryData>(pagesQueryKey); - - return { - data: Array.isArray(pageData?.data) ? pageData.data : [], - count: typeof pageData?.total_count === 'number' ? pageData.total_count : 0, - page: paginatedPage, - }; - // eslint-disable-next-line react-hooks/exhaustive-deps -- forceUpdateCounter is used to trigger re-renders for cache updates - }, [ - queriesEnabled, - forceUpdateCounter, - triggerInfinite, - infiniteQuery.data?.pages, - singlePageQuery.data, - queryClient, - infiniteQueryKey, - pagesQueryKey, - paginatedPage, - ]); - - const { data, count, page } = computedValues; - - const fetchPage: ValueOrSetter = useCallback( - numberOrgFn => { - if (triggerInfinite) { - const next = typeof numberOrgFn === 'function' ? (numberOrgFn as (n: number) => number)(page) : numberOrgFn; - const targetCount = Math.max(0, next); - const cachedData = queryClient.getQueryData<{ pages?: Array> }>(infiniteQueryKey); - const pages = infiniteQuery.data?.pages ?? cachedData?.pages ?? []; - const currentCount = pages.length; - const toFetch = targetCount - currentCount; - if (toFetch > 0) { - void infiniteQuery.fetchNextPage({ cancelRefetch: false }); - } - return; - } - return setPaginatedPage(numberOrgFn); - }, - [infiniteQuery, page, triggerInfinite, queryClient, infiniteQueryKey], - ); - - const isLoading = triggerInfinite ? infiniteQuery.isLoading : singlePageQuery.isLoading; - const isFetching = triggerInfinite ? infiniteQuery.isFetching : singlePageQuery.isFetching; - const error = (triggerInfinite ? infiniteQuery.error : singlePageQuery.error) ?? null; - const isError = !!error; - - const fetchNext = useCallback(() => { - if (triggerInfinite) { - void infiniteQuery.fetchNextPage({ cancelRefetch: false }); - return; - } - setPaginatedPage(n => Math.max(0, n + 1)); - }, [infiniteQuery, triggerInfinite]); - - const fetchPrevious = useCallback(() => { - if (triggerInfinite) { - // not natively supported by forward-only pagination; noop - return; - } - setPaginatedPage(n => Math.max(0, n - 1)); - }, [triggerInfinite]); - - const offsetCount = (initialPageRef.current - 1) * pageSizeRef.current; - const pageCount = Math.ceil((count - offsetCount) / pageSizeRef.current); - const hasNextPage = triggerInfinite - ? Boolean(infiniteQuery.hasNextPage) - : count - offsetCount * pageSizeRef.current > page * pageSizeRef.current; - const hasPreviousPage = triggerInfinite - ? Boolean(infiniteQuery.hasPreviousPage) - : (page - 1) * pageSizeRef.current > offsetCount * pageSizeRef.current; - - const setData: CacheSetter = value => { - if (triggerInfinite) { - queryClient.setQueryData(infiniteQueryKey, (prevValue: any = {}) => { - const prevPages = Array.isArray(prevValue?.pages) ? prevValue.pages : []; - const nextPages = (typeof value === 'function' ? value(prevPages) : value) as Array< - ClerkPaginatedResponse - >; - return { ...prevValue, pages: nextPages }; - }); - // Force immediate re-render to reflect cache changes - forceUpdate(n => n + 1); - return Promise.resolve(); - } - queryClient.setQueryData(pagesQueryKey, (prevValue: any = { data: [], total_count: 0 }) => { - const nextValue = (typeof value === 'function' ? value(prevValue) : value) as ClerkPaginatedResponse; - return nextValue; - }); - // Force re-render to reflect cache changes - forceUpdate(n => n + 1); - return Promise.resolve(); - }; - - const revalidate = async () => { - await queryClient.invalidateQueries({ queryKey: keys.invalidationKey }); - const [stablePrefix, ...rest] = keys.invalidationKey; - return queryClient.invalidateQueries({ queryKey: [stablePrefix + '-inf', ...rest] }); - }; - - return { - data, - count, - error, - isLoading, - isFetching, - isError, - page, - pageCount, - fetchPage, - fetchNext, - fetchPrevious, - hasNextPage, - hasPreviousPage, - revalidate: revalidate as any, - setData: setData as any, - }; -}; - -export { useWithSafeValues }; diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx deleted file mode 100644 index d81d5bbbf76..00000000000 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.swr.tsx +++ /dev/null @@ -1,236 +0,0 @@ -'use client'; - -import { useCallback, useMemo, useRef, useState } from 'react'; - -import { useSWR, useSWRInfinite } from '../clerk-swr'; -import type { CacheSetter, ValueOrSetter } from '../types'; -import { toSWRQuery } from './createCacheKeys'; -import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types'; -import { getDifferentKeys, useWithSafeValues } from './usePagesOrInfinite.shared'; -import { usePreviousValue } from './usePreviousValue'; - -const cachingSWROptions = { - dedupingInterval: 1000 * 60, - focusThrottleInterval: 1000 * 60 * 2, -} satisfies Parameters[2]; - -const cachingSWRInfiniteOptions = { - ...cachingSWROptions, - revalidateFirstPage: false, -} satisfies Parameters[2]; - -/** - * A flexible pagination hook that supports both traditional pagination and infinite loading. - * It provides a unified API for handling paginated data fetching, with built-in caching through SWR. - * The hook can operate in two modes: - * - Traditional pagination: Fetches one page at a time with page navigation - * - Infinite loading: Accumulates data as more pages are loaded. - * - * Features: - * - Cache management with SWR - * - Loading and error states - * - Page navigation helpers - * - Data revalidation and updates - * - Support for keeping previous data while loading. - * - * @internal - */ -export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { - const { fetcher, config, keys } = params; - const [paginatedPage, setPaginatedPage] = useState(config.initialPage ?? 1); - - // Cache initialPage and initialPageSize until unmount - const initialPageRef = useRef(config.initialPage ?? 1); - const pageSizeRef = useRef(config.pageSize ?? 10); - - const enabled = config.enabled ?? true; - const cacheMode = config.__experimental_mode === 'cache'; - const triggerInfinite = config.infinite ?? false; - const keepPreviousData = config.keepPreviousData ?? false; - const isSignedIn = config.isSignedIn; - - const pagesCacheKey = { - ...toSWRQuery(keys), - initialPage: paginatedPage, - pageSize: pageSizeRef.current, - }; - - const previousIsSignedIn = usePreviousValue(isSignedIn); - - // cacheMode being `true` indicates that the cache key is defined, but the fetcher is not. - // This allows to ready the cache instead of firing a request. - const shouldFetch = !triggerInfinite && enabled && (!cacheMode ? !!fetcher : true); - - // Attention: - // - // This complex logic is necessary to ensure that the cached data is not used when the user is signed out. - // `useSWR` with `key` set to `null` and `keepPreviousData` set to `true` will return the previous cached data until the hook unmounts. - // So for hooks that render authenticated data, we need to ensure that the cached data is not used when the user is signed out. - // - // 1. Fetcher should not fire if user is signed out on mount. (fetcher does not run, loading states are not triggered) - // 2. If user was signed in and then signed out, cached data should become null. (fetcher runs and returns null, loading states are triggered) - // - // We achieve (2) by setting the key to the cache key when the user transitions to signed out and forcing the fetcher to return null. - const swrKey = - typeof isSignedIn === 'boolean' - ? previousIsSignedIn === true && isSignedIn === false - ? pagesCacheKey - : isSignedIn - ? shouldFetch - ? pagesCacheKey - : null - : null - : shouldFetch - ? pagesCacheKey - : null; - - const swrFetcher = - !cacheMode && !!fetcher - ? (cacheKeyParams: Record) => { - if (isSignedIn === false || shouldFetch === false) { - return null; - } - const requestParams = getDifferentKeys(cacheKeyParams, { type: keys.queryKey[0], ...keys.queryKey[2] }); - // @ts-ignore - fetcher expects Params subset; narrowing at call-site - return fetcher(requestParams); - } - : null; - - const { - data: swrData, - isValidating: swrIsValidating, - isLoading: swrIsLoading, - error: swrError, - mutate: swrMutate, - } = useSWR(swrKey, swrFetcher, { keepPreviousData, ...cachingSWROptions }); - - // Attention: - // - // Cache behavior for infinite loading when signing out: - // - // Unlike `useSWR` above (which requires complex transition handling), `useSWRInfinite` has simpler sign-out semantics: - // 1. When user is signed out on mount, the key getter returns `null`, preventing any fetches. - // 2. When user transitions from signed in to signed out, the key getter returns `null` for all page indices. - // 3. When `useSWRInfinite`'s key getter returns `null`, SWR will not fetch data and considers that page invalid. - // 4. Unlike paginated mode, `useSWRInfinite` does not support `keepPreviousData`, so there's no previous data retention. - // - // This simpler behavior works because: - // - `useSWRInfinite` manages multiple pages internally, each with its own cache key - // - When the key getter returns `null`, all page fetches are prevented and pages become invalid - // - Without `keepPreviousData`, the hook will naturally reflect the empty/invalid state - // - // Result: No special transition logic needed - just return `null` from key getter when `isSignedIn === false`. - const { - data: swrInfiniteData, - isLoading: swrInfiniteIsLoading, - isValidating: swrInfiniteIsValidating, - error: swrInfiniteError, - size, - setSize, - mutate: swrInfiniteMutate, - } = useSWRInfinite( - pageIndex => { - if (!triggerInfinite || !enabled || isSignedIn === false) { - return null; - } - - return { - ...toSWRQuery(keys), - initialPage: initialPageRef.current + pageIndex, - pageSize: pageSizeRef.current, - }; - }, - cacheKeyParams => { - // @ts-ignore - fetcher expects Params subset; narrowing at call-site - const requestParams = getDifferentKeys(cacheKeyParams, { type: keys.queryKey[0], ...keys.queryKey[2] }); - // @ts-ignore - fetcher expects Params subset; narrowing at call-site - return fetcher?.(requestParams); - }, - cachingSWRInfiniteOptions, - ); - - const page = useMemo(() => { - if (triggerInfinite) { - return size; - } - return paginatedPage; - }, [triggerInfinite, size, paginatedPage]); - - const fetchPage: ValueOrSetter = useCallback( - numberOrgFn => { - if (triggerInfinite) { - void setSize(numberOrgFn); - return; - } - return setPaginatedPage(numberOrgFn); - }, - [setSize, triggerInfinite], - ); - - const data = useMemo(() => { - if (triggerInfinite) { - return swrInfiniteData?.map(a => a?.data).flat() ?? []; - } - return swrData?.data ?? []; - }, [triggerInfinite, swrData, swrInfiniteData]); - - const count = useMemo(() => { - if (triggerInfinite) { - return swrInfiniteData?.[swrInfiniteData?.length - 1]?.total_count || 0; - } - return swrData?.total_count ?? 0; - }, [triggerInfinite, swrData, swrInfiniteData]); - - const isLoading = triggerInfinite ? swrInfiniteIsLoading : swrIsLoading; - const isFetching = triggerInfinite ? swrInfiniteIsValidating : swrIsValidating; - const error = (triggerInfinite ? swrInfiniteError : swrError) ?? null; - const isError = !!error; - - const fetchNext = useCallback(() => { - fetchPage(n => Math.max(0, n + 1)); - }, [fetchPage]); - - const fetchPrevious = useCallback(() => { - fetchPage(n => Math.max(0, n - 1)); - }, [fetchPage]); - - const offsetCount = (initialPageRef.current - 1) * pageSizeRef.current; - - const pageCount = Math.ceil((count - offsetCount) / pageSizeRef.current); - const hasNextPage = count - offsetCount * pageSizeRef.current > page * pageSizeRef.current; - const hasPreviousPage = (page - 1) * pageSizeRef.current > offsetCount * pageSizeRef.current; - - const setData: CacheSetter = triggerInfinite - ? value => - swrInfiniteMutate(value, { - revalidate: false, - }) - : value => - swrMutate(value, { - revalidate: false, - }); - - const revalidate = triggerInfinite ? () => swrInfiniteMutate() : () => swrMutate(); - - return { - data, - count, - error, - isLoading, - isFetching, - isError, - page, - pageCount, - fetchPage, - fetchNext, - fetchPrevious, - hasNextPage, - hasPreviousPage, - // Let the hook return type define this type - revalidate: revalidate as any, - // Let the hook return type define this type - setData: setData as any, - }; -}; - -export { useWithSafeValues }; diff --git a/packages/shared/src/react/hooks/usePagesOrInfinite.tsx b/packages/shared/src/react/hooks/usePagesOrInfinite.tsx index 3bb9fe522ff..f4d01862445 100644 --- a/packages/shared/src/react/hooks/usePagesOrInfinite.tsx +++ b/packages/shared/src/react/hooks/usePagesOrInfinite.tsx @@ -1,2 +1,257 @@ -export { usePagesOrInfinite } from 'virtual:data-hooks/usePagesOrInfinite'; -export { useWithSafeValues } from './usePagesOrInfinite.shared'; +import { useCallback, useMemo, useRef, useState } from 'react'; + +import type { ClerkPaginatedResponse } from '../../types'; +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; +import { useClerkInfiniteQuery } from '../clerk-rq/useInfiniteQuery'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import type { CacheSetter, ValueOrSetter } from '../types'; +import { useClearQueriesOnSignOut, withInfiniteKey } from './useClearQueriesOnSignOut'; +import type { UsePagesOrInfiniteSignature } from './usePageOrInfinite.types'; +import { useWithSafeValues } from './usePagesOrInfinite.shared'; + +export const usePagesOrInfinite: UsePagesOrInfiniteSignature = params => { + const { fetcher, config, keys } = params; + + const [paginatedPage, setPaginatedPage] = useState(config.initialPage ?? 1); + + // Cache initialPage and initialPageSize until unmount + const initialPageRef = useRef(config.initialPage ?? 1); + const pageSizeRef = useRef(config.pageSize ?? 10); + + const enabled = config.enabled ?? true; + const isSignedIn = config.isSignedIn; + const triggerInfinite = config.infinite ?? false; + const cacheMode = config.__experimental_mode === 'cache'; + const keepPreviousData = config.keepPreviousData ?? false; + + const [queryClient] = useClerkQueryClient(); + + // Compute the actual enabled state for queries (considering all conditions) + const queriesEnabled = enabled && Boolean(fetcher) && !cacheMode && isSignedIn !== false; + + // Force re-render counter for cache-only updates + const [forceUpdateCounter, setForceUpdateCounter] = useState(0); + const forceUpdate = useCallback((updater: (n: number) => number) => { + setForceUpdateCounter(updater); + }, []); + + // Non-infinite mode: single page query + const pagesQueryKey = useMemo(() => { + const [stablePrefix, authenticated, tracked, untracked] = keys.queryKey; + + return [ + stablePrefix, + authenticated, + tracked, + { + ...untracked, + args: { + ...untracked.args, + initialPage: paginatedPage, + pageSize: pageSizeRef.current, + }, + }, + ] as const; + }, [keys.queryKey, paginatedPage]); + + const singlePageQuery = useClerkQuery({ + queryKey: pagesQueryKey, + queryFn: ({ queryKey }) => { + const { args } = queryKey[3]; + + if (!fetcher) { + return undefined as any; + } + + return fetcher(args); + }, + staleTime: 60_000, + enabled: queriesEnabled && !triggerInfinite, + // Use placeholderData to keep previous data while fetching new page + placeholderData: defineKeepPreviousDataFn(keepPreviousData), + }); + + // Infinite mode: accumulate pages + const infiniteQueryKey = useMemo(() => { + const [stablePrefix, authenticated, tracked, untracked] = keys.queryKey; + + return [stablePrefix + '-inf', authenticated, tracked, untracked] as const; + }, [keys.queryKey]); + + const infiniteQuery = useClerkInfiniteQuery, any, any, typeof infiniteQueryKey, any>({ + queryKey: infiniteQueryKey, + initialPageParam: config.initialPage ?? 1, + getNextPageParam: (lastPage, allPages, lastPageParam) => { + const total = lastPage?.total_count ?? 0; + const consumed = (allPages.length + (config.initialPage ? config.initialPage - 1 : 0)) * (config.pageSize ?? 10); + return consumed < total ? (lastPageParam as number) + 1 : undefined; + }, + queryFn: ({ pageParam, queryKey }) => { + const { args } = queryKey[3]; + if (!fetcher) { + return undefined as any; + } + return fetcher({ ...args, initialPage: pageParam, pageSize: pageSizeRef.current }); + }, + staleTime: 60_000, + enabled: queriesEnabled && triggerInfinite, + }); + + useClearQueriesOnSignOut({ + isSignedOut: isSignedIn === false, + authenticated: keys.authenticated, + stableKeys: withInfiniteKey(keys.stableKey), + onCleanup: () => { + // Reset paginated page to initial + setPaginatedPage(initialPageRef.current); + + // Force re-render to reflect cache changes + void Promise.resolve().then(() => forceUpdate(n => n + 1)); + }, + }); + + // Compute data, count and page from the same data source to ensure consistency + const computedValues = useMemo(() => { + if (triggerInfinite) { + // Read from query data first, fallback to cache + const cachedData = queryClient.getQueryData<{ pages?: Array> }>(infiniteQueryKey); + const pages = queriesEnabled ? (infiniteQuery.data?.pages ?? cachedData?.pages ?? []) : (cachedData?.pages ?? []); + + // Ensure pages is always an array and filter out null/undefined pages + const validPages = Array.isArray(pages) ? pages.filter(Boolean) : []; + + return { + data: + validPages + .map((a: ClerkPaginatedResponse) => a?.data) + .flat() + .filter(Boolean) ?? [], + count: validPages[validPages.length - 1]?.total_count ?? 0, + page: validPages.length > 0 ? validPages.length : initialPageRef.current, + }; + } + + // When query is disabled (via enabled flag), the hook's data is stale, so only read from cache + // This ensures that after cache clearing, we return consistent empty state + const pageData = queriesEnabled + ? (singlePageQuery.data ?? queryClient.getQueryData>(pagesQueryKey)) + : queryClient.getQueryData>(pagesQueryKey); + + return { + data: Array.isArray(pageData?.data) ? pageData.data : [], + count: typeof pageData?.total_count === 'number' ? pageData.total_count : 0, + page: paginatedPage, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- forceUpdateCounter is used to trigger re-renders for cache updates + }, [ + queriesEnabled, + forceUpdateCounter, + triggerInfinite, + infiniteQuery.data?.pages, + singlePageQuery.data, + queryClient, + infiniteQueryKey, + pagesQueryKey, + paginatedPage, + ]); + + const { data, count, page } = computedValues; + + const fetchPage: ValueOrSetter = useCallback( + numberOrgFn => { + if (triggerInfinite) { + const next = typeof numberOrgFn === 'function' ? (numberOrgFn as (n: number) => number)(page) : numberOrgFn; + const targetCount = Math.max(0, next); + const cachedData = queryClient.getQueryData<{ pages?: Array> }>(infiniteQueryKey); + const pages = infiniteQuery.data?.pages ?? cachedData?.pages ?? []; + const currentCount = pages.length; + const toFetch = targetCount - currentCount; + if (toFetch > 0) { + void infiniteQuery.fetchNextPage({ cancelRefetch: false }); + } + return; + } + return setPaginatedPage(numberOrgFn); + }, + [infiniteQuery, page, triggerInfinite, queryClient, infiniteQueryKey], + ); + + const isLoading = triggerInfinite ? infiniteQuery.isLoading : singlePageQuery.isLoading; + const isFetching = triggerInfinite ? infiniteQuery.isFetching : singlePageQuery.isFetching; + const error = (triggerInfinite ? infiniteQuery.error : singlePageQuery.error) ?? null; + const isError = !!error; + + const fetchNext = useCallback(() => { + if (triggerInfinite) { + void infiniteQuery.fetchNextPage({ cancelRefetch: false }); + return; + } + setPaginatedPage(n => Math.max(0, n + 1)); + }, [infiniteQuery, triggerInfinite]); + + const fetchPrevious = useCallback(() => { + if (triggerInfinite) { + // not natively supported by forward-only pagination; noop + return; + } + setPaginatedPage(n => Math.max(0, n - 1)); + }, [triggerInfinite]); + + const offsetCount = (initialPageRef.current - 1) * pageSizeRef.current; + const pageCount = Math.ceil((count - offsetCount) / pageSizeRef.current); + const hasNextPage = triggerInfinite + ? Boolean(infiniteQuery.hasNextPage) + : count - offsetCount * pageSizeRef.current > page * pageSizeRef.current; + const hasPreviousPage = triggerInfinite + ? Boolean(infiniteQuery.hasPreviousPage) + : (page - 1) * pageSizeRef.current > offsetCount * pageSizeRef.current; + + const setData: CacheSetter = value => { + if (triggerInfinite) { + queryClient.setQueryData(infiniteQueryKey, (prevValue: any = {}) => { + const prevPages = Array.isArray(prevValue?.pages) ? prevValue.pages : []; + const nextPages = (typeof value === 'function' ? value(prevPages) : value) as Array< + ClerkPaginatedResponse + >; + return { ...prevValue, pages: nextPages }; + }); + // Force immediate re-render to reflect cache changes + forceUpdate(n => n + 1); + return Promise.resolve(); + } + queryClient.setQueryData(pagesQueryKey, (prevValue: any = { data: [], total_count: 0 }) => { + const nextValue = (typeof value === 'function' ? value(prevValue) : value) as ClerkPaginatedResponse; + return nextValue; + }); + // Force re-render to reflect cache changes + forceUpdate(n => n + 1); + return Promise.resolve(); + }; + + const revalidate = async () => { + await queryClient.invalidateQueries({ queryKey: keys.invalidationKey }); + const [stablePrefix, ...rest] = keys.invalidationKey; + return queryClient.invalidateQueries({ queryKey: [stablePrefix + '-inf', ...rest] }); + }; + + return { + data, + count, + error, + isLoading, + isFetching, + isError, + page, + pageCount, + fetchPage, + fetchNext, + fetchPrevious, + hasNextPage, + hasPreviousPage, + revalidate: revalidate as any, + setData: setData as any, + }; +}; + +export { useWithSafeValues }; diff --git a/packages/shared/src/react/hooks/usePaymentAttemptQuery.rq.tsx b/packages/shared/src/react/hooks/usePaymentAttemptQuery.rq.tsx deleted file mode 100644 index 7a59fc5b282..00000000000 --- a/packages/shared/src/react/hooks/usePaymentAttemptQuery.rq.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import { useClerkInstanceContext, useOrganizationContext, useUserContext } from '../contexts'; -import { useBillingHookEnabled } from './useBillingHookEnabled'; -import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut'; -import { usePaymentAttemptQueryCacheKeys } from './usePaymentAttemptQuery.shared'; -import type { PaymentAttemptQueryResult, UsePaymentAttemptQueryParams } from './usePaymentAttemptQuery.types'; - -/** - * @internal - */ -function usePaymentAttemptQuery(params: UsePaymentAttemptQueryParams): PaymentAttemptQueryResult { - const { paymentAttemptId, keepPreviousData = false, for: forType = 'user' } = params; - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - const organizationId = forType === 'organization' ? (organization?.id ?? null) : null; - const userId = user?.id ?? null; - - const { queryKey, stableKey, authenticated } = usePaymentAttemptQueryCacheKeys({ - paymentAttemptId, - userId, - orgId: organizationId, - for: forType, - }); - - const billingEnabled = useBillingHookEnabled(params); - - const queryEnabled = Boolean(paymentAttemptId) && billingEnabled; - - useClearQueriesOnSignOut({ - isSignedOut: user === null, // works with the transitive state - authenticated, - stableKeys: stableKey, - }); - - const query = useClerkQuery({ - queryKey, - queryFn: ({ queryKey }) => { - const args = queryKey[3].args; - return clerk.billing.getPaymentAttempt(args); - }, - enabled: queryEnabled, - placeholderData: defineKeepPreviousDataFn(keepPreviousData), - staleTime: 1_000 * 60, - }); - - return { - data: query.data, - error: (query.error ?? null) as PaymentAttemptQueryResult['error'], - isLoading: query.isLoading, - isFetching: query.isFetching, - }; -} - -export { usePaymentAttemptQuery as __internal_usePaymentAttemptQuery }; diff --git a/packages/shared/src/react/hooks/usePaymentAttemptQuery.swr.tsx b/packages/shared/src/react/hooks/usePaymentAttemptQuery.swr.tsx deleted file mode 100644 index d47d5d52246..00000000000 --- a/packages/shared/src/react/hooks/usePaymentAttemptQuery.swr.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useSWR } from '../clerk-swr'; -import { useClerkInstanceContext, useOrganizationContext, useUserContext } from '../contexts'; -import { usePaymentAttemptQueryCacheKeys } from './usePaymentAttemptQuery.shared'; -import type { PaymentAttemptQueryResult, UsePaymentAttemptQueryParams } from './usePaymentAttemptQuery.types'; - -/** - * This is the existing implementation of usePaymentAttemptQuery using SWR. - * It is kept here for backwards compatibility until our next major version. - * - * @internal - */ -export function __internal_usePaymentAttemptQuery(params: UsePaymentAttemptQueryParams): PaymentAttemptQueryResult { - const { paymentAttemptId, enabled = true, keepPreviousData = false, for: forType = 'user' } = params; - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - const organizationId = forType === 'organization' ? (organization?.id ?? null) : null; - const userId = user?.id ?? null; - - const { queryKey } = usePaymentAttemptQueryCacheKeys({ - paymentAttemptId, - userId, - orgId: organizationId, - for: forType, - }); - - const queryEnabled = Boolean(paymentAttemptId) && enabled && (forType !== 'organization' || Boolean(organizationId)); - - const swr = useSWR( - queryEnabled ? { queryKey } : null, - ({ queryKey }) => { - const args = queryKey[3].args; - return clerk.billing.getPaymentAttempt(args); - }, - { - dedupingInterval: 1_000 * 60, - keepPreviousData, - }, - ); - - return { - data: swr.data, - error: (swr.error ?? null) as PaymentAttemptQueryResult['error'], - isLoading: swr.isLoading, - isFetching: swr.isValidating, - }; -} diff --git a/packages/shared/src/react/hooks/usePaymentAttemptQuery.tsx b/packages/shared/src/react/hooks/usePaymentAttemptQuery.tsx index ffa7ea1dc6e..7a59fc5b282 100644 --- a/packages/shared/src/react/hooks/usePaymentAttemptQuery.tsx +++ b/packages/shared/src/react/hooks/usePaymentAttemptQuery.tsx @@ -1 +1,57 @@ -export { __internal_usePaymentAttemptQuery } from 'virtual:data-hooks/usePaymentAttemptQuery'; +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import { useClerkInstanceContext, useOrganizationContext, useUserContext } from '../contexts'; +import { useBillingHookEnabled } from './useBillingHookEnabled'; +import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut'; +import { usePaymentAttemptQueryCacheKeys } from './usePaymentAttemptQuery.shared'; +import type { PaymentAttemptQueryResult, UsePaymentAttemptQueryParams } from './usePaymentAttemptQuery.types'; + +/** + * @internal + */ +function usePaymentAttemptQuery(params: UsePaymentAttemptQueryParams): PaymentAttemptQueryResult { + const { paymentAttemptId, keepPreviousData = false, for: forType = 'user' } = params; + const clerk = useClerkInstanceContext(); + const user = useUserContext(); + const { organization } = useOrganizationContext(); + + const organizationId = forType === 'organization' ? (organization?.id ?? null) : null; + const userId = user?.id ?? null; + + const { queryKey, stableKey, authenticated } = usePaymentAttemptQueryCacheKeys({ + paymentAttemptId, + userId, + orgId: organizationId, + for: forType, + }); + + const billingEnabled = useBillingHookEnabled(params); + + const queryEnabled = Boolean(paymentAttemptId) && billingEnabled; + + useClearQueriesOnSignOut({ + isSignedOut: user === null, // works with the transitive state + authenticated, + stableKeys: stableKey, + }); + + const query = useClerkQuery({ + queryKey, + queryFn: ({ queryKey }) => { + const args = queryKey[3].args; + return clerk.billing.getPaymentAttempt(args); + }, + enabled: queryEnabled, + placeholderData: defineKeepPreviousDataFn(keepPreviousData), + staleTime: 1_000 * 60, + }); + + return { + data: query.data, + error: (query.error ?? null) as PaymentAttemptQueryResult['error'], + isLoading: query.isLoading, + isFetching: query.isFetching, + }; +} + +export { usePaymentAttemptQuery as __internal_usePaymentAttemptQuery }; diff --git a/packages/shared/src/react/hooks/usePlanDetailsQuery.rq.tsx b/packages/shared/src/react/hooks/usePlanDetailsQuery.rq.tsx deleted file mode 100644 index c2a7ec96cbd..00000000000 --- a/packages/shared/src/react/hooks/usePlanDetailsQuery.rq.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import { useClerkInstanceContext } from '../contexts'; -import { useBillingHookEnabled } from './useBillingHookEnabled'; -import { usePlanDetailsQueryCacheKeys } from './usePlanDetailsQuery.shared'; -import type { PlanDetailsQueryResult, UsePlanDetailsQueryParams } from './usePlanDetailsQuery.types'; - -/** - * @internal - */ -export function __internal_usePlanDetailsQuery(params: UsePlanDetailsQueryParams = {}): PlanDetailsQueryResult { - const { planId, initialPlan = null, keepPreviousData = true } = params; - const clerk = useClerkInstanceContext(); - - const targetPlanId = planId ?? initialPlan?.id ?? null; - - const { queryKey } = usePlanDetailsQueryCacheKeys({ planId: targetPlanId }); - - const billingEnabled = useBillingHookEnabled({ - authenticated: false, - }); - - const queryEnabled = Boolean(targetPlanId) && billingEnabled; - - const query = useClerkQuery({ - queryKey, - queryFn: () => { - if (!targetPlanId) { - throw new Error('planId is required to fetch plan details'); - } - return clerk.billing.getPlan({ id: targetPlanId }); - }, - enabled: queryEnabled, - initialData: initialPlan ?? undefined, - placeholderData: defineKeepPreviousDataFn(keepPreviousData), - initialDataUpdatedAt: 0, - }); - - return { - data: query.data, - error: (query.error ?? null) as PlanDetailsQueryResult['error'], - isLoading: query.isLoading, - isFetching: query.isFetching, - }; -} diff --git a/packages/shared/src/react/hooks/usePlanDetailsQuery.swr.tsx b/packages/shared/src/react/hooks/usePlanDetailsQuery.swr.tsx deleted file mode 100644 index ce544fce5b4..00000000000 --- a/packages/shared/src/react/hooks/usePlanDetailsQuery.swr.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useSWR } from '../clerk-swr'; -import { useClerkInstanceContext } from '../contexts'; -import { usePlanDetailsQueryCacheKeys } from './usePlanDetailsQuery.shared'; -import type { PlanDetailsQueryResult, UsePlanDetailsQueryParams } from './usePlanDetailsQuery.types'; - -/** - * This is the existing implementation of usePlanDetailsQuery using SWR. - * It is kept here for backwards compatibility until our next major version. - * - * @internal - */ -function usePlanDetailsQuery(params: UsePlanDetailsQueryParams = {}): PlanDetailsQueryResult { - const { planId, initialPlan = null, enabled = true, keepPreviousData = true } = params; - const clerk = useClerkInstanceContext(); - - const targetPlanId = planId ?? initialPlan?.id ?? null; - - const { queryKey } = usePlanDetailsQueryCacheKeys({ planId: targetPlanId }); - - const queryEnabled = Boolean(targetPlanId) && enabled; - - const swr = useSWR( - queryEnabled ? queryKey : null, - () => { - if (!targetPlanId) { - throw new Error('planId is required to fetch plan details'); - } - return clerk.billing.getPlan({ id: targetPlanId }); - }, - { - dedupingInterval: 1_000 * 60, - keepPreviousData, - fallbackData: initialPlan ?? undefined, - }, - ); - - return { - data: swr.data, - error: (swr.error ?? null) as PlanDetailsQueryResult['error'], - isLoading: swr.isLoading, - isFetching: swr.isValidating, - }; -} - -export { usePlanDetailsQuery as __internal_usePlanDetailsQuery }; diff --git a/packages/shared/src/react/hooks/usePlanDetailsQuery.tsx b/packages/shared/src/react/hooks/usePlanDetailsQuery.tsx index 7fb85951400..c2a7ec96cbd 100644 --- a/packages/shared/src/react/hooks/usePlanDetailsQuery.tsx +++ b/packages/shared/src/react/hooks/usePlanDetailsQuery.tsx @@ -1 +1,45 @@ -export { __internal_usePlanDetailsQuery } from 'virtual:data-hooks/usePlanDetailsQuery'; +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import { useClerkInstanceContext } from '../contexts'; +import { useBillingHookEnabled } from './useBillingHookEnabled'; +import { usePlanDetailsQueryCacheKeys } from './usePlanDetailsQuery.shared'; +import type { PlanDetailsQueryResult, UsePlanDetailsQueryParams } from './usePlanDetailsQuery.types'; + +/** + * @internal + */ +export function __internal_usePlanDetailsQuery(params: UsePlanDetailsQueryParams = {}): PlanDetailsQueryResult { + const { planId, initialPlan = null, keepPreviousData = true } = params; + const clerk = useClerkInstanceContext(); + + const targetPlanId = planId ?? initialPlan?.id ?? null; + + const { queryKey } = usePlanDetailsQueryCacheKeys({ planId: targetPlanId }); + + const billingEnabled = useBillingHookEnabled({ + authenticated: false, + }); + + const queryEnabled = Boolean(targetPlanId) && billingEnabled; + + const query = useClerkQuery({ + queryKey, + queryFn: () => { + if (!targetPlanId) { + throw new Error('planId is required to fetch plan details'); + } + return clerk.billing.getPlan({ id: targetPlanId }); + }, + enabled: queryEnabled, + initialData: initialPlan ?? undefined, + placeholderData: defineKeepPreviousDataFn(keepPreviousData), + initialDataUpdatedAt: 0, + }); + + return { + data: query.data, + error: (query.error ?? null) as PlanDetailsQueryResult['error'], + isLoading: query.isLoading, + isFetching: query.isFetching, + }; +} diff --git a/packages/shared/src/react/hooks/useStatementQuery.rq.tsx b/packages/shared/src/react/hooks/useStatementQuery.rq.tsx deleted file mode 100644 index 25f8b4a3908..00000000000 --- a/packages/shared/src/react/hooks/useStatementQuery.rq.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import { useClerkInstanceContext, useOrganizationContext, useUserContext } from '../contexts'; -import { useBillingHookEnabled } from './useBillingHookEnabled'; -import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut'; -import { useStatementQueryCacheKeys } from './useStatementQuery.shared'; -import type { StatementQueryResult, UseStatementQueryParams } from './useStatementQuery.types'; - -/** - * @internal - */ -function useStatementQuery(params: UseStatementQueryParams = {}): StatementQueryResult { - const { statementId = null, keepPreviousData = false, for: forType = 'user' } = params; - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - const organizationId = forType === 'organization' ? (organization?.id ?? null) : null; - const userId = user?.id ?? null; - - const { queryKey, stableKey, authenticated } = useStatementQueryCacheKeys({ - statementId, - userId, - orgId: organizationId, - for: forType, - }); - - const billingEnabled = useBillingHookEnabled(params); - - const queryEnabled = Boolean(statementId) && billingEnabled; - - useClearQueriesOnSignOut({ - isSignedOut: user === null, - authenticated, - stableKeys: stableKey, - }); - - const query = useClerkQuery({ - queryKey, - queryFn: () => { - if (!statementId) { - throw new Error('statementId is required to fetch a statement'); - } - return clerk.billing.getStatement({ id: statementId, orgId: organizationId ?? undefined }); - }, - enabled: queryEnabled, - placeholderData: defineKeepPreviousDataFn(keepPreviousData), - staleTime: 1_000 * 60, - }); - - return { - data: query.data, - error: (query.error ?? null) as StatementQueryResult['error'], - isLoading: query.isLoading, - isFetching: query.isFetching, - }; -} - -export { useStatementQuery as __internal_useStatementQuery }; diff --git a/packages/shared/src/react/hooks/useStatementQuery.swr.tsx b/packages/shared/src/react/hooks/useStatementQuery.swr.tsx deleted file mode 100644 index 8d209d75f66..00000000000 --- a/packages/shared/src/react/hooks/useStatementQuery.swr.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useSWR } from '../clerk-swr'; -import { useClerkInstanceContext, useOrganizationContext, useUserContext } from '../contexts'; -import { useStatementQueryCacheKeys } from './useStatementQuery.shared'; -import type { StatementQueryResult, UseStatementQueryParams } from './useStatementQuery.types'; - -/** - * This is the existing implementation of useStatementQuery using SWR. - * It is kept here for backwards compatibility until our next major version. - * - * @internal - */ -export function __internal_useStatementQuery(params: UseStatementQueryParams = {}): StatementQueryResult { - const { statementId = null, enabled = true, keepPreviousData = false, for: forType = 'user' } = params; - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - const organizationId = forType === 'organization' ? (organization?.id ?? null) : null; - const userId = user?.id ?? null; - - const { queryKey } = useStatementQueryCacheKeys({ - statementId, - userId, - orgId: organizationId, - for: forType, - }); - - const queryEnabled = Boolean(statementId) && enabled && (forType !== 'organization' || Boolean(organizationId)); - - const swr = useSWR( - queryEnabled ? queryKey : null, - () => { - if (!statementId) { - throw new Error('statementId is required to fetch a statement'); - } - return clerk.billing.getStatement({ id: statementId, orgId: organizationId ?? undefined }); - }, - { - dedupingInterval: 1_000 * 60, - keepPreviousData, - }, - ); - - return { - data: swr.data, - error: (swr.error ?? null) as StatementQueryResult['error'], - isLoading: swr.isLoading, - isFetching: swr.isValidating, - }; -} diff --git a/packages/shared/src/react/hooks/useStatementQuery.tsx b/packages/shared/src/react/hooks/useStatementQuery.tsx index 0664eedaefa..25f8b4a3908 100644 --- a/packages/shared/src/react/hooks/useStatementQuery.tsx +++ b/packages/shared/src/react/hooks/useStatementQuery.tsx @@ -1 +1,59 @@ -export { __internal_useStatementQuery } from 'virtual:data-hooks/useStatementQuery'; +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import { useClerkInstanceContext, useOrganizationContext, useUserContext } from '../contexts'; +import { useBillingHookEnabled } from './useBillingHookEnabled'; +import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut'; +import { useStatementQueryCacheKeys } from './useStatementQuery.shared'; +import type { StatementQueryResult, UseStatementQueryParams } from './useStatementQuery.types'; + +/** + * @internal + */ +function useStatementQuery(params: UseStatementQueryParams = {}): StatementQueryResult { + const { statementId = null, keepPreviousData = false, for: forType = 'user' } = params; + const clerk = useClerkInstanceContext(); + const user = useUserContext(); + const { organization } = useOrganizationContext(); + + const organizationId = forType === 'organization' ? (organization?.id ?? null) : null; + const userId = user?.id ?? null; + + const { queryKey, stableKey, authenticated } = useStatementQueryCacheKeys({ + statementId, + userId, + orgId: organizationId, + for: forType, + }); + + const billingEnabled = useBillingHookEnabled(params); + + const queryEnabled = Boolean(statementId) && billingEnabled; + + useClearQueriesOnSignOut({ + isSignedOut: user === null, + authenticated, + stableKeys: stableKey, + }); + + const query = useClerkQuery({ + queryKey, + queryFn: () => { + if (!statementId) { + throw new Error('statementId is required to fetch a statement'); + } + return clerk.billing.getStatement({ id: statementId, orgId: organizationId ?? undefined }); + }, + enabled: queryEnabled, + placeholderData: defineKeepPreviousDataFn(keepPreviousData), + staleTime: 1_000 * 60, + }); + + return { + data: query.data, + error: (query.error ?? null) as StatementQueryResult['error'], + isLoading: query.isLoading, + isFetching: query.isFetching, + }; +} + +export { useStatementQuery as __internal_useStatementQuery }; diff --git a/packages/shared/src/react/hooks/useSubscription.rq.tsx b/packages/shared/src/react/hooks/useSubscription.rq.tsx deleted file mode 100644 index 4ae24e593e1..00000000000 --- a/packages/shared/src/react/hooks/useSubscription.rq.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useCallback } from 'react'; - -import { eventMethodCalled } from '../../telemetry/events'; -import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; -import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; -import { useClerkQuery } from '../clerk-rq/useQuery'; -import { - useAssertWrappedByClerkProvider, - useClerkInstanceContext, - useOrganizationContext, - useUserContext, -} from '../contexts'; -import { useBillingHookEnabled } from './useBillingHookEnabled'; -import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut'; -import { useSubscriptionCacheKeys } from './useSubscription.shared'; -import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; - -const HOOK_NAME = 'useSubscription'; - -/** - * This is the new implementation of useSubscription using React Query. - * It is exported only if the package is build with the `CLERK_USE_RQ` environment variable set to `true`. - * - * @internal - */ -export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { - useAssertWrappedByClerkProvider(HOOK_NAME); - - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - const billingEnabled = useBillingHookEnabled(params); - - clerk.telemetry?.record(eventMethodCalled(HOOK_NAME)); - - const keepPreviousData = params?.keepPreviousData ?? false; - - const [queryClient] = useClerkQueryClient(); - - const { queryKey, invalidationKey, stableKey, authenticated } = useSubscriptionCacheKeys({ - userId: user?.id, - orgId: organization?.id, - for: params?.for, - }); - - const queriesEnabled = Boolean(user?.id && billingEnabled); - useClearQueriesOnSignOut({ - isSignedOut: user === null, - authenticated, - stableKeys: stableKey, - }); - - const query = useClerkQuery({ - queryKey, - queryFn: ({ queryKey }) => { - const obj = queryKey[3]; - return clerk.billing.getSubscription(obj.args); - }, - staleTime: 1_000 * 60, - enabled: queriesEnabled, - placeholderData: defineKeepPreviousDataFn(keepPreviousData && queriesEnabled), - }); - - const revalidate = useCallback( - () => queryClient.invalidateQueries({ queryKey: invalidationKey }), - [queryClient, invalidationKey], - ); - - return { - data: query.data, - // Our existing types for SWR return undefined when there is no error, but React Query returns null. - // So we need to convert the error to undefined, for backwards compatibility. - error: query.error ?? undefined, - isLoading: query.isLoading, - isFetching: query.isFetching, - revalidate, - }; -} diff --git a/packages/shared/src/react/hooks/useSubscription.swr.tsx b/packages/shared/src/react/hooks/useSubscription.swr.tsx deleted file mode 100644 index d418cbe6d77..00000000000 --- a/packages/shared/src/react/hooks/useSubscription.swr.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useCallback } from 'react'; - -import { eventMethodCalled } from '../../telemetry/events'; -import type { EnvironmentResource } from '../../types'; -import { useSWR } from '../clerk-swr'; -import { - useAssertWrappedByClerkProvider, - useClerkInstanceContext, - useOrganizationContext, - useUserContext, -} from '../contexts'; -import { useSubscriptionCacheKeys } from './useSubscription.shared'; -import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; - -const hookName = 'useSubscription'; - -/** - * This is the existing implementation of useSubscription using SWR. - * It is kept here for backwards compatibility until our next major version. - * - * @internal - */ -export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { - useAssertWrappedByClerkProvider(hookName); - - const clerk = useClerkInstanceContext(); - const user = useUserContext(); - const { organization } = useOrganizationContext(); - - // @ts-expect-error `__internal_environment` is not typed - const environment = clerk.__internal_environment as unknown as EnvironmentResource | null | undefined; - - clerk.telemetry?.record(eventMethodCalled(hookName)); - - const isOrganization = params?.for === 'organization'; - const billingEnabled = isOrganization - ? environment?.commerceSettings.billing.organization.enabled - : environment?.commerceSettings.billing.user.enabled; - const isEnabled = (params?.enabled ?? true) && billingEnabled; - - const { queryKey } = useSubscriptionCacheKeys({ - userId: user?.id, - orgId: organization?.id, - for: params?.for, - }); - - const swr = useSWR( - isEnabled ? { queryKey } : null, - ({ queryKey }) => { - const args = queryKey[3].args; - - if (queryKey[2].userId) { - return clerk.billing.getSubscription(args); - } - return null; - }, - { - dedupingInterval: 1_000 * 60, - keepPreviousData: params?.keepPreviousData, - }, - ); - - const revalidate = useCallback(() => { - void swr.mutate(); - }, [swr]); - - return { - data: swr.data, - error: swr.error, - isLoading: swr.isLoading, - isFetching: swr.isValidating, - revalidate, - }; -} diff --git a/packages/shared/src/react/hooks/useSubscription.tsx b/packages/shared/src/react/hooks/useSubscription.tsx index 98cd031a355..154cd626fbc 100644 --- a/packages/shared/src/react/hooks/useSubscription.tsx +++ b/packages/shared/src/react/hooks/useSubscription.tsx @@ -1 +1,76 @@ -export { useSubscription } from 'virtual:data-hooks/useSubscription'; +import { useCallback } from 'react'; + +import { eventMethodCalled } from '../../telemetry/events'; +import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; +import { useClerkQuery } from '../clerk-rq/useQuery'; +import { + useAssertWrappedByClerkProvider, + useClerkInstanceContext, + useOrganizationContext, + useUserContext, +} from '../contexts'; +import { useBillingHookEnabled } from './useBillingHookEnabled'; +import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut'; +import { useSubscriptionCacheKeys } from './useSubscription.shared'; +import type { SubscriptionResult, UseSubscriptionParams } from './useSubscription.types'; + +const HOOK_NAME = 'useSubscription'; + +/** + * @internal + */ +export function useSubscription(params?: UseSubscriptionParams): SubscriptionResult { + useAssertWrappedByClerkProvider(HOOK_NAME); + + const clerk = useClerkInstanceContext(); + const user = useUserContext(); + const { organization } = useOrganizationContext(); + + const billingEnabled = useBillingHookEnabled(params); + + clerk.telemetry?.record(eventMethodCalled(HOOK_NAME)); + + const keepPreviousData = params?.keepPreviousData ?? false; + + const [queryClient] = useClerkQueryClient(); + + const { queryKey, invalidationKey, stableKey, authenticated } = useSubscriptionCacheKeys({ + userId: user?.id, + orgId: organization?.id, + for: params?.for, + }); + + const queriesEnabled = Boolean(user?.id && billingEnabled); + useClearQueriesOnSignOut({ + isSignedOut: user === null, + authenticated, + stableKeys: stableKey, + }); + + const query = useClerkQuery({ + queryKey, + queryFn: ({ queryKey }) => { + const obj = queryKey[3]; + return clerk.billing.getSubscription(obj.args); + }, + staleTime: 1_000 * 60, + enabled: queriesEnabled, + placeholderData: defineKeepPreviousDataFn(keepPreviousData && queriesEnabled), + }); + + const revalidate = useCallback( + () => queryClient.invalidateQueries({ queryKey: invalidationKey }), + [queryClient, invalidationKey], + ); + + return { + data: query.data, + // Our existing types for SWR return undefined when there is no error, but React Query returns null. + // So we need to convert the error to undefined, for backwards compatibility. + error: query.error ?? undefined, + isLoading: query.isLoading, + isFetching: query.isFetching, + revalidate, + }; +} diff --git a/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx b/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx deleted file mode 100644 index 40810747d89..00000000000 --- a/packages/shared/src/react/providers/SWRConfigCompat.rq.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { PropsWithChildren } from 'react'; -import React from 'react'; -/** - * @internal - */ -export function SWRConfigCompat({ children }: PropsWithChildren<{ swrConfig?: any }>) { - return <>{children}; -} diff --git a/packages/shared/src/react/providers/SWRConfigCompat.swr.tsx b/packages/shared/src/react/providers/SWRConfigCompat.swr.tsx deleted file mode 100644 index 97d341456d1..00000000000 --- a/packages/shared/src/react/providers/SWRConfigCompat.swr.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React, { type PropsWithChildren } from 'react'; -import { SWRConfig } from 'swr'; - -/** - * @internal - */ -export function SWRConfigCompat({ swrConfig, children }: PropsWithChildren<{ swrConfig?: any }>) { - // TODO: Replace SWRConfig with the react-query equivalent. - return {children}; -} diff --git a/packages/shared/src/react/providers/SWRConfigCompat.tsx b/packages/shared/src/react/providers/SWRConfigCompat.tsx index 0286d80613d..40810747d89 100644 --- a/packages/shared/src/react/providers/SWRConfigCompat.tsx +++ b/packages/shared/src/react/providers/SWRConfigCompat.tsx @@ -1 +1,8 @@ -export { SWRConfigCompat } from 'virtual:data-hooks/SWRConfigCompat'; +import type { PropsWithChildren } from 'react'; +import React from 'react'; +/** + * @internal + */ +export function SWRConfigCompat({ children }: PropsWithChildren<{ swrConfig?: any }>) { + return <>{children}; +} diff --git a/packages/shared/src/types/virtual-data-hooks.d.ts b/packages/shared/src/types/virtual-data-hooks.d.ts deleted file mode 100644 index 680d0d56269..00000000000 --- a/packages/shared/src/types/virtual-data-hooks.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare module 'virtual:data-hooks/*' { - // Generic export signatures to satisfy type resolution for virtual modules - export const SWRConfigCompat: any; - export const useSubscription: any; - export const usePagesOrInfinite: any; - export const useAPIKeys: any; - export const __internal_useStatementQuery: any; - export const __internal_usePlanDetailsQuery: any; - export const __internal_usePaymentAttemptQuery: any; - const mod: any; - export default mod; -} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index a3540904ce1..05d89438884 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -23,19 +23,9 @@ "declarationMap": true, "allowJs": true, "paths": { - "@/*": ["./src/*"], - "virtual:data-hooks/useAPIKeys": ["./src/react/hooks/useAPIKeys.swr.tsx"], - "virtual:data-hooks/useSubscription": ["./src/react/hooks/useSubscription.swr.tsx"], - "virtual:data-hooks/SWRConfigCompat": ["./src/react/providers/SWRConfigCompat.swr.tsx"], - "virtual:data-hooks/usePagesOrInfinite": ["./src/react/hooks/usePagesOrInfinite.swr.tsx"], - "virtual:data-hooks/useStatementQuery": ["./src/react/hooks/useStatementQuery.swr.tsx"], - "virtual:data-hooks/usePaymentAttemptQuery": ["./src/react/hooks/usePaymentAttemptQuery.swr.tsx"], - "virtual:data-hooks/usePlanDetailsQuery": ["./src/react/hooks/usePlanDetailsQuery.swr.tsx"], - "virtual:data-hooks/useInitializePaymentMethod": ["./src/react/billing/useInitializePaymentMethod.swr.tsx"], - "virtual:data-hooks/useStripeClerkLibs": ["./src/react/billing/useStripeClerkLibs.swr.tsx"], - "virtual:data-hooks/useStripeLoader": ["./src/react/billing/useStripeLoader.swr.tsx"] + "@/*": ["./src/*"] } }, "exclude": ["node_modules"], - "include": ["src", "global.d.ts", "src/types/virtual-data-hooks.d.ts"] + "include": ["src", "global.d.ts"] } diff --git a/packages/shared/tsdown.config.mts b/packages/shared/tsdown.config.mts index 93c58027e0e..178c08e96d8 100644 --- a/packages/shared/tsdown.config.mts +++ b/packages/shared/tsdown.config.mts @@ -1,6 +1,3 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; - import type { Options } from 'tsdown'; import { defineConfig } from 'tsdown'; @@ -25,7 +22,6 @@ export default defineConfig(({ watch }) => { UI_PACKAGE_VERSION: `"${clerkUiPackage.version}"`, __DEV__: `${watch}`, __BUILD_DISABLE_RHC__: JSON.stringify(false), - __CLERK_USE_RQ__: `${process.env.CLERK_USE_RQ === 'true'}`, }, } satisfies Options; @@ -55,43 +51,6 @@ export default defineConfig(({ watch }) => { ], outDir: './dist/runtime', unbundle: false, - plugins: [HookAliasPlugin()], }, ]; }); - -const HookAliasPlugin = () => { - const useRQ = process.env.CLERK_USE_RQ === 'true'; - const rqHooks = new Set((process.env.CLERK_RQ_HOOKS ?? '').split(',').filter(Boolean)); - const baseDir = process.cwd(); - - const resolveImpl = (specifier: string) => { - const name = specifier.replace('virtual:data-hooks/', ''); - const chosenRQ = rqHooks.has(name) || useRQ; - const impl = chosenRQ ? `${name}.rq.tsx` : `${name}.swr.tsx`; - - const candidates = [ - path.join(baseDir, 'src', 'react', 'hooks', impl), - path.join(baseDir, 'src', 'react', 'billing', impl), - path.join(baseDir, 'src', 'react', 'providers', impl), - ]; - - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - // default to first candidate; bundler will emit a clear error if missing - return candidates[0]; - }; - - return { - name: 'hook-alias-plugin', - resolveId(id: string) { - if (!id.startsWith('virtual:data-hooks/')) { - return null; - } - return resolveImpl(id); - }, - } as any; -}; diff --git a/packages/shared/vitest.config.mts b/packages/shared/vitest.config.mts index 56046534aaa..cf88a06605f 100644 --- a/packages/shared/vitest.config.mts +++ b/packages/shared/vitest.config.mts @@ -1,45 +1,8 @@ -import * as fs from 'node:fs'; import * as path from 'node:path'; import { defineConfig } from 'vitest/config'; -function HookAliasPlugin() { - return { - name: 'hook-alias-plugin', - resolveId(id: string) { - if (!id.startsWith('virtual:data-hooks/')) { - return null; - } - - const name = id.replace('virtual:data-hooks/', ''); - const useRQ = process.env.CLERK_USE_RQ === 'true'; - const rqHooks = new Set((process.env.CLERK_RQ_HOOKS ?? '').split(',').filter(Boolean)); - const chosenRQ = rqHooks.has(name) || useRQ; - const impl = `${name}.${chosenRQ ? 'rq' : 'swr'}.tsx`; - - const baseDirs = [process.cwd(), path.join(process.cwd(), 'packages', 'shared')]; - - const candidates: string[] = []; - for (const base of baseDirs) { - candidates.push( - path.join(base, 'src', 'react', 'hooks', impl), - path.join(base, 'src', 'react', 'billing', impl), - path.join(base, 'src', 'react', 'providers', impl), - ); - } - - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - return candidate; - } - } - return candidates[0]; - }, - } as any; -} - export default defineConfig({ - plugins: [HookAliasPlugin()], resolve: { alias: { '@': path.resolve(__dirname, './src'), diff --git a/packages/shared/vitest.setup.mts b/packages/shared/vitest.setup.mts index e418b9f0ddf..90be31f2b5e 100644 --- a/packages/shared/vitest.setup.mts +++ b/packages/shared/vitest.setup.mts @@ -8,7 +8,6 @@ globalThis.PACKAGE_NAME = '@clerk/react'; globalThis.PACKAGE_VERSION = '0.0.0-test'; globalThis.JS_PACKAGE_VERSION = '5.0.0'; globalThis.UI_PACKAGE_VERSION = '1.0.0'; -globalThis.__CLERK_USE_RQ__ = process.env.CLERK_USE_RQ === 'true'; // Setup Web Crypto API for tests (Node.js 18+ compatibility) if (!globalThis.crypto) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bbb3ed5f9a..e1b4ff57760 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -780,6 +780,9 @@ importers: packages/shared: dependencies: + '@tanstack/query-core': + specifier: 5.87.4 + version: 5.87.4 dequal: specifier: 2.0.3 version: 2.0.3 @@ -798,9 +801,6 @@ importers: std-env: specifier: ^3.9.0 version: 3.10.0 - swr: - specifier: 2.3.4 - version: 2.3.4(react@18.3.1) devDependencies: '@base-org/account': specifier: catalog:module-manager @@ -814,9 +814,6 @@ importers: '@stripe/stripe-js': specifier: 5.6.0 version: 5.6.0 - '@tanstack/query-core': - specifier: 5.87.4 - version: 5.87.4 '@types/glob-to-regexp': specifier: 0.4.4 version: 0.4.4 From 6ee75be795743c4165ab6abadbd04f48b22cbf74 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sun, 14 Dec 2025 14:09:11 -0600 Subject: [PATCH 2/3] changeset --- .changeset/remove-swr-switches.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/remove-swr-switches.md diff --git a/.changeset/remove-swr-switches.md b/.changeset/remove-swr-switches.md new file mode 100644 index 00000000000..a8b10845b82 --- /dev/null +++ b/.changeset/remove-swr-switches.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': major +--- + +Remove SWR hooks and env-based switchovers in favor of the React Query implementations; promote @tanstack/query-core to a runtime dependency. From b55dc0e0ac77cd89b769d9c60508f58aaa56cd74 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sun, 14 Dec 2025 14:32:48 -0600 Subject: [PATCH 3/3] upgrade RQ to latest --- packages/clerk-js/package.json | 2 +- packages/shared/package.json | 2 +- pnpm-lock.yaml | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 93e12a0050b..80cec13b897 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -64,7 +64,7 @@ "@coinbase/wallet-sdk": "catalog:module-manager", "@stripe/stripe-js": "5.6.0", "@swc/helpers": "^0.5.17", - "@tanstack/query-core": "5.87.4", + "@tanstack/query-core": "5.90.12", "@zxcvbn-ts/core": "catalog:module-manager", "@zxcvbn-ts/language-common": "catalog:module-manager", "alien-signals": "2.0.6", diff --git a/packages/shared/package.json b/packages/shared/package.json index 49df1d222f5..477884e6e28 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -124,7 +124,7 @@ "test:coverage": "vitest --collectCoverage && open coverage/lcov-report/index.html" }, "dependencies": { - "@tanstack/query-core": "5.87.4", + "@tanstack/query-core": "5.90.12", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1b4ff57760..d744f3c1c73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -454,8 +454,8 @@ importers: specifier: ^0.5.17 version: 0.5.17 '@tanstack/query-core': - specifier: 5.87.4 - version: 5.87.4 + specifier: 5.90.12 + version: 5.90.12 '@zxcvbn-ts/core': specifier: catalog:module-manager version: 3.0.4 @@ -781,8 +781,8 @@ importers: packages/shared: dependencies: '@tanstack/query-core': - specifier: 5.87.4 - version: 5.87.4 + specifier: 5.90.12 + version: 5.90.12 dequal: specifier: 2.0.3 version: 2.0.3 @@ -2352,7 +2352,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -4620,8 +4620,8 @@ packages: resolution: {integrity: sha512-GG2R9I6QSlbNR9fEuX2sQCigY6K28w51h2634TWmkaHXlzQw+rWuIWr4nAGM9doA+kWRi1LFSFMvAiG3cOqjXQ==} engines: {node: '>=12'} - '@tanstack/query-core@5.87.4': - resolution: {integrity: sha512-uNsg6zMxraEPDVO2Bn+F3/ctHi+Zsk+MMpcN8h6P7ozqD088F6mFY5TfGM7zuyIrL7HKpDyu6QHfLWiDxh3cuw==} + '@tanstack/query-core@5.90.12': + resolution: {integrity: sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==} '@tanstack/react-router@1.132.0': resolution: {integrity: sha512-tGNmQrFc4zWQZvjqYnC8ib84H/9QokRl73hr0P2XlxCY2KAgPTk2QjdzW03LqXgQZRXg7++vKznJt4LS9/M3iA==} @@ -10737,6 +10737,7 @@ packages: next@15.2.3: resolution: {integrity: sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + deprecated: This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details. hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -19298,7 +19299,7 @@ snapshots: '@tanstack/history@1.132.0': {} - '@tanstack/query-core@5.87.4': {} + '@tanstack/query-core@5.90.12': {} '@tanstack/react-router@1.132.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: