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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .changeset/remove-swr-switches.md

This file was deleted.

2 changes: 1 addition & 1 deletion packages/clerk-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"@solana/wallet-standard": "catalog:module-manager",
"@stripe/stripe-js": "5.6.0",
"@swc/helpers": "catalog:repo",
"@tanstack/query-core": "5.90.16",
"@tanstack/query-core": "5.87.4",
"@wallet-standard/core": "catalog:module-manager",
"@zxcvbn-ts/core": "catalog:module-manager",
"@zxcvbn-ts/language-common": "catalog:module-manager",
Expand Down
8 changes: 7 additions & 1 deletion packages/clerk-js/src/test/create-fixtures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// @ts-nocheck

import type { ClerkOptions, ClientJSON, EnvironmentJSON, LoadedClerk } from '@clerk/shared/types';
import { useState } from 'react';
import { vi } from 'vitest';

import { Clerk as ClerkCtor } from '@/core/clerk';
Expand Down Expand Up @@ -86,6 +87,7 @@ const unboundCreateFixtures = (

const MockClerkProvider = (props: any) => {
const { children } = props;
const [swrConfig] = useState(() => ({ provider: () => new Map() }));

const componentsWithoutContext = [
'UsernameSection',
Expand All @@ -106,7 +108,11 @@ const unboundCreateFixtures = (
);

return (
<CoreClerkContextWrapper clerk={clerkMock}>
<CoreClerkContextWrapper
clerk={clerkMock}
// Clear swr cache
swrConfig={swrConfig}
>
<EnvironmentProvider value={environmentMock}>
<OptionsProvider value={optionsMock}>
<RouteContext.Provider value={routerMock}>
Expand Down
1 change: 1 addition & 0 deletions packages/shared/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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;
Expand Down
5 changes: 3 additions & 2 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@
"test:coverage": "vitest --collectCoverage && open coverage/lcov-report/index.html"
},
"dependencies": {
"@tanstack/query-core": "5.90.16",
"dequal": "2.0.3",
"glob-to-regexp": "0.4.1",
"js-cookie": "3.0.5",
"std-env": "^3.9.0"
"std-env": "^3.9.0",
"swr": "2.3.4"
},
"devDependencies": {
"@base-org/account": "catalog:module-manager",
Expand All @@ -138,6 +138,7 @@
"@solana/wallet-standard": "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",
"@wallet-standard/core": "catalog:module-manager",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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<BillingInitializedPaymentMethodResource | undefined>;
};

/**
* @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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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<BillingInitializedPaymentMethodResource | undefined>;
};

/**
* 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 };
78 changes: 2 additions & 76 deletions packages/shared/src/react/billing/useInitializePaymentMethod.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,2 @@
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<BillingInitializedPaymentMethodResource | undefined>;
};

/**
* @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 };
export type { UseInitializePaymentMethodResult } from 'virtual:data-hooks/useInitializePaymentMethod';
export { __internal_useInitializePaymentMethod } from 'virtual:data-hooks/useInitializePaymentMethod';
37 changes: 37 additions & 0 deletions packages/shared/src/react/billing/useStripeClerkLibs.rq.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 };
39 changes: 39 additions & 0 deletions packages/shared/src/react/billing/useStripeClerkLibs.swr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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;
}
Comment on lines +20 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Behavioral inconsistency: Missing billingEnabled guard.

The React Query implementation (useStripeClerkLibs.rq.tsx) uses useBillingHookEnabled() to conditionally enable the query, but this SWR implementation fetches unconditionally. This means:

  • RQ: Only loads Stripe SDK when billing is enabled
  • SWR: Always attempts to load Stripe SDK

This behavioral difference could cause unexpected network requests or errors when billing is disabled.

Proposed fix to align behavior
 import type { loadStripe } from '@stripe/stripe-js';
 
 import { useSWR } from '../clerk-swr';
 import { useClerk } from '../hooks/useClerk';
+import { useBillingHookEnabled } from './useBillingHookEnabled';
 
 // ... types ...
 
 function useStripeClerkLibs(): UseStripeClerkLibsResult {
   const clerk = useClerk();
+  const billingEnabled = useBillingHookEnabled();
 
   const swr = useSWR(
-    'clerk-stripe-sdk',
+    billingEnabled ? 'clerk-stripe-sdk' : null,
     async () => {
       const loadStripe = (await clerk.__internal_loadStripeJs()) as LoadStripeFn;
       return { loadStripe };
     },
     {
       keepPreviousData: true,
       revalidateOnFocus: false,
       dedupingInterval: Infinity,
     },
   );
 
   return swr.data ?? null;
 }
🤖 Prompt for AI Agents
In @packages/shared/src/react/billing/useStripeClerkLibs.swr.tsx around lines 20
- 37, The SWR hook useStripeClerkLibs currently always invokes the Stripe
loader; add the same billing gate used in the React Query version by importing
and calling useBillingHookEnabled() and only enable the useSWR fetch when
billingEnabled is true (e.g., use a null key or conditional fetch function) so
that clerk.__internal_loadStripeJs() is not called when billing is disabled;
update useStripeClerkLibs to return null when billing is disabled and keep
existing SWR options (keepPreviousData, revalidateOnFocus, dedupingInterval)
when enabled.


export { useStripeClerkLibs as __internal_useStripeClerkLibs };
Loading
Loading