Skip to content
Open
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: 5 additions & 0 deletions .changeset/polite-cars-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/shared': patch
---

Fix `useClearQueriesOnSignOut` hook by removing conditional `useEffect`
5 changes: 5 additions & 0 deletions .changeset/remove-swr-switches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/shared': major
---

Remove SWR hooks and env-based switchovers in favor of the React Query implementations; promote @tanstack/query-core to a runtime dependency.
53 changes: 5 additions & 48 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ jobs:
unit-tests:
needs: [check-permissions, build-packages]
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
name: Unit Tests (${{ matrix.filter-label }}${{ matrix.clerk-use-rq == 'true' && ', RQ' || '' }})
name: Unit Tests (${{ matrix.filter-label }})
permissions:
contents: read
actions: write # needed for actions/upload-artifact
Expand All @@ -220,12 +220,7 @@ jobs:
include:
- node-version: 22
test-filter: "**"
clerk-use-rq: "false"
filter-label: "**"
- node-version: 22
test-filter: "--filter=@clerk/shared --filter=@clerk/clerk-js"
clerk-use-rq: "true"
filter-label: "shared, clerk-js"

steps:
- name: Checkout Repo
Expand All @@ -247,18 +242,6 @@ jobs:
# turbo-team: ${{ vars.TURBO_TEAM }}
# turbo-token: ${{ secrets.TURBO_TOKEN }}

- name: Rebuild @clerk/shared with CLERK_USE_RQ=true
if: ${{ matrix.clerk-use-rq == 'true' }}
run: pnpm turbo build $TURBO_ARGS --filter=@clerk/shared --force
env:
CLERK_USE_RQ: true

- name: Rebuild dependent packages with CLERK_USE_RQ=true
if: ${{ matrix.clerk-use-rq == 'true' }}
run: pnpm turbo build $TURBO_ARGS --filter=@clerk/shared^... --force
env:
CLERK_USE_RQ: true

- name: Run tests in packages
run: |
if [ "${{ matrix.test-filter }}" = "**" ]; then
Expand All @@ -270,7 +253,6 @@ jobs:
fi
env:
NODE_VERSION: ${{ matrix.node-version }}
CLERK_USE_RQ: ${{ matrix.clerk-use-rq }}

- name: Run Typedoc tests
run: |
Expand All @@ -286,15 +268,15 @@ jobs:
if: ${{ env.TURBO_SUMMARIZE == 'true' }}
continue-on-error: true
with:
name: turbo-summary-report-unit-${{ github.run_id }}-${{ github.run_attempt }}-node-${{ matrix.node-version }}${{ matrix.clerk-use-rq == 'true' && '-rq' || '' }}
name: turbo-summary-report-unit-${{ github.run_id }}-${{ github.run_attempt }}-node-${{ matrix.node-version }}
path: .turbo/runs
retention-days: 5

integration-tests:
# needs: [check-permissions, build-packages]
needs: [check-permissions]
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
name: Integration Tests (${{ matrix.test-name }}, ${{ matrix.test-project }}${{ matrix.next-version && format(', {0}', matrix.next-version) || '' }}${{ matrix.clerk-use-rq == 'true' && ', RQ' || '' }})
name: Integration Tests (${{ matrix.test-name }}, ${{ matrix.test-project }}${{ matrix.next-version && format(', {0}', matrix.next-version) || '' }})
permissions:
contents: read
actions: write # needed for actions/upload-artifact
Expand Down Expand Up @@ -328,23 +310,11 @@ jobs:
include:
- test-name: "billing"
test-project: "chrome"
clerk-use-rq: "false"
- test-name: "billing"
test-project: "chrome"
clerk-use-rq: "true"
- test-name: "machine"
test-project: "chrome"
clerk-use-rq: "false"
- test-name: "machine"
test-project: "chrome"
clerk-use-rq: "true"
- test-name: "nextjs"
test-project: "chrome"
next-version: "15"
- test-name: "nextjs"
test-project: "chrome"
next-version: "16"
clerk-use-rq: "true"
- test-name: "nextjs"
test-project: "chrome"
next-version: "16"
Expand Down Expand Up @@ -406,18 +376,6 @@ jobs:
echo "affected=${AFFECTED}"
echo "affected=${AFFECTED}" >> $GITHUB_OUTPUT
- name: Rebuild @clerk/shared with CLERK_USE_RQ=true
if: ${{ steps.task-status.outputs.affected == '1' && matrix.clerk-use-rq == 'true' }}
run: pnpm turbo build $TURBO_ARGS --filter=@clerk/shared --force
env:
CLERK_USE_RQ: true

- name: Rebuild dependent packages with CLERK_USE_RQ=true
if: ${{ steps.task-status.outputs.affected == '1' && matrix.clerk-use-rq == 'true' }}
run: pnpm turbo build $TURBO_ARGS --filter=@clerk/shared^... --force
env:
CLERK_USE_RQ: true

- name: Version packages for snapshot
if: ${{ steps.task-status.outputs.affected == '1' }}
run: npm run version-packages:snapshot ci
Expand All @@ -427,7 +385,7 @@ jobs:
uses: ./.github/actions/verdaccio
with:
publish-cmd: |
if [ "$(pnpm config get registry)" = "https://registry.npmjs.org/" ]; then echo 'Error: Using default registry' && exit 1; else ${{ matrix.clerk-use-rq == 'true' && 'CLERK_USE_RQ=true' || '' }} pnpm turbo build $TURBO_ARGS --only && pnpm changeset publish --no-git-tag --tag latest; fi
if [ "$(pnpm config get registry)" = "https://registry.npmjs.org/" ]; then echo 'Error: Using default registry' && exit 1; else pnpm turbo build $TURBO_ARGS --only && pnpm changeset publish --no-git-tag --tag latest; fi
- name: Edit .npmrc [link-workspace-packages=false]
run: sed -i -E 's/link-workspace-packages=(deep|true)/link-workspace-packages=false/' .npmrc
Expand Down Expand Up @@ -501,7 +459,6 @@ jobs:
E2E_NEXTJS_VERSION: ${{ matrix.next-version }}
E2E_PROJECT: ${{ matrix.test-project }}
E2E_CLERK_ENCRYPTION_KEY: ${{ matrix.clerk-encryption-key }}
CLERK_USE_RQ: ${{ matrix.clerk-use-rq }}
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
Expand All @@ -510,7 +467,7 @@ jobs:
if: ${{ cancelled() || failure() }}
uses: actions/upload-artifact@v4
with:
name: playwright-traces-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.test-name }}${{ matrix.next-version && format('-next{0}', matrix.next-version) || '' }}${{ matrix.clerk-use-rq == 'true' && '-rq' || '' }}
name: playwright-traces-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.test-name }}${{ matrix.next-version && format('-next{0}', matrix.next-version) || '' }}
path: integration/test-results
retention-days: 1

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

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

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

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

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

return (
<CoreClerkContextWrapper
clerk={clerkMock}
// Clear swr cache
swrConfig={swrConfig}
>
<CoreClerkContextWrapper clerk={clerkMock}>
<EnvironmentProvider value={environmentMock}>
<OptionsProvider value={optionsMock}>
<RouteContext.Provider value={routerMock}>
Expand Down
1 change: 0 additions & 1 deletion packages/shared/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ declare const JS_PACKAGE_VERSION: string;
declare const UI_PACKAGE_VERSION: string;
declare const __DEV__: boolean;
declare const __BUILD_DISABLE_RHC__: boolean;
declare const __CLERK_USE_RQ__: boolean;

interface ImportMetaEnv {
readonly [key: string]: string;
Expand Down
5 changes: 2 additions & 3 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@
"test:coverage": "vitest --collectCoverage && open coverage/lcov-report/index.html"
},
"dependencies": {
"@tanstack/query-core": "5.90.16",
"dequal": "2.0.3",
"glob-to-regexp": "0.4.1",
"js-cookie": "3.0.5",
"std-env": "^3.9.0",
"swr": "2.3.4"
"std-env": "^3.9.0"
},
"devDependencies": {
"@base-org/account": "catalog:module-manager",
Expand All @@ -138,7 +138,6 @@
"@solana/wallet-standard": "catalog:module-manager",
"@stripe/react-stripe-js": "3.1.1",
"@stripe/stripe-js": "5.6.0",
"@tanstack/query-core": "5.87.4",
"@types/glob-to-regexp": "0.4.4",
"@types/js-cookie": "3.0.6",
"@wallet-standard/core": "catalog:module-manager",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import {
createMockClerk,
createMockOrganization,
createMockQueryClient,
createMockUser,
} from '../../hooks/__tests__/mocks/clerk';
import { wrapper } from '../../hooks/__tests__/wrapper';
import { __internal_useInitializePaymentMethod as useInitializePaymentMethod } from '../useInitializePaymentMethod';

// Dynamic mock state for contexts
let mockUser: any = createMockUser();
let mockOrganization: any = createMockOrganization();
let userBillingEnabled = true;
let orgBillingEnabled = true;

const initializePaymentMethodSpy = vi.fn(() =>
Promise.resolve({ externalClientSecret: 'secret_123', gateway: 'stripe' }),
);

const defaultQueryClient = createMockQueryClient();

const mockClerk = createMockClerk({
environment: {
commerceSettings: {
billing: {
user: { enabled: userBillingEnabled },
organization: { enabled: orgBillingEnabled },
},
},
},
queryClient: defaultQueryClient,
});

vi.mock('../../contexts', () => {
return {
useAssertWrappedByClerkProvider: () => {},
useClerkInstanceContext: () => mockClerk,
useUserContext: () => (mockClerk.loaded ? mockUser : null),
useOrganizationContext: () => ({ organization: mockClerk.loaded ? mockOrganization : null }),
};
});

describe('useInitializePaymentMethod', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset environment flags and state
userBillingEnabled = true;
orgBillingEnabled = true;
mockUser = createMockUser();
mockUser.initializePaymentMethod = initializePaymentMethodSpy;
mockOrganization = createMockOrganization();
mockOrganization.initializePaymentMethod = initializePaymentMethodSpy;
mockClerk.__internal_environment.commerceSettings.billing.user.enabled = userBillingEnabled;
mockClerk.__internal_environment.commerceSettings.billing.organization.enabled = orgBillingEnabled;
defaultQueryClient.client.clear();
});

it('returns the expected shape', () => {
const { result } = renderHook(() => useInitializePaymentMethod(), { wrapper });

expect(result.current).toHaveProperty('initializedPaymentMethod');
expect(result.current).toHaveProperty('initializePaymentMethod');
expect(result.current.initializePaymentMethod).toBeInstanceOf(Function);
});

it('does not fetch when billing disabled for user', () => {
mockClerk.__internal_environment.commerceSettings.billing.user.enabled = false;

const { result } = renderHook(() => useInitializePaymentMethod(), { wrapper });

expect(initializePaymentMethodSpy).not.toHaveBeenCalled();
expect(result.current.initializedPaymentMethod).toBeUndefined();
});

it('fetches user payment method initialization when billing enabled', async () => {
const { result } = renderHook(() => useInitializePaymentMethod(), { wrapper });

await waitFor(() => expect(result.current.initializedPaymentMethod).toBeDefined());

expect(initializePaymentMethodSpy).toHaveBeenCalledTimes(1);
expect(initializePaymentMethodSpy).toHaveBeenCalledWith({ gateway: 'stripe' });
expect(result.current.initializedPaymentMethod).toEqual({
externalClientSecret: 'secret_123',
gateway: 'stripe',
});
});

it('fetches organization payment method initialization when for=organization', async () => {
const { result } = renderHook(() => useInitializePaymentMethod({ for: 'organization' }), { wrapper });

await waitFor(() => expect(result.current.initializedPaymentMethod).toBeDefined());

expect(initializePaymentMethodSpy).toHaveBeenCalledTimes(1);
expect(initializePaymentMethodSpy).toHaveBeenCalledWith({ gateway: 'stripe' });
expect(result.current.initializedPaymentMethod).toEqual({
externalClientSecret: 'secret_123',
gateway: 'stripe',
});
});

it('clears cached data on sign-out', async () => {
const { result, rerender } = renderHook(() => useInitializePaymentMethod(), { wrapper });

await waitFor(() => expect(result.current.initializedPaymentMethod).toBeDefined());
expect(result.current.initializedPaymentMethod).toEqual({
externalClientSecret: 'secret_123',
gateway: 'stripe',
});
expect(initializePaymentMethodSpy).toHaveBeenCalledTimes(1);

// Simulate sign-out
mockUser = null;
rerender();

await waitFor(() => expect(result.current.initializedPaymentMethod).toBeUndefined());

// Should not have fetched again
expect(initializePaymentMethodSpy).toHaveBeenCalledTimes(1);
});

it('initializePaymentMethod function fetches and updates cache', async () => {
const { result } = renderHook(() => useInitializePaymentMethod(), { wrapper });

await waitFor(() => expect(result.current.initializedPaymentMethod).toBeDefined());

// Reset spy to track new calls
initializePaymentMethodSpy.mockClear();
initializePaymentMethodSpy.mockResolvedValueOnce({
externalClientSecret: 'secret_456',
gateway: 'stripe',
});

const returnedResult = await act(async () => {
return result.current.initializePaymentMethod();
});

expect(initializePaymentMethodSpy).toHaveBeenCalledTimes(1);
expect(initializePaymentMethodSpy).toHaveBeenCalledWith({ gateway: 'stripe' });
expect(returnedResult).toEqual({
externalClientSecret: 'secret_456',
gateway: 'stripe',
});
});

it('uses correct query key format for cache clearing', async () => {
const { result, rerender } = renderHook(() => useInitializePaymentMethod(), { wrapper });

await waitFor(() => expect(result.current.initializedPaymentMethod).toBeDefined());

// Verify cache has the data
const cacheData = defaultQueryClient.client.getQueryData([
'billing-payment-method-initialize',
true,
{ resourceId: 'user_1' },
{},
]);
expect(cacheData).toEqual({
externalClientSecret: 'secret_123',
gateway: 'stripe',
});

// Simulate sign-out
mockUser = null;
rerender();

await waitFor(() => expect(result.current.initializedPaymentMethod).toBeUndefined());

// Verify cache was cleared
const clearedCacheData = defaultQueryClient.client.getQueryData([
'billing-payment-method-initialize',
true,
{ resourceId: 'user_1' },
{},
]);
expect(clearedCacheData).toBeUndefined();
});
});
Loading
Loading