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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
Copy Markdown
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