Skip to content

feat(clerk-react,clerk-js, types,shared): Introduce Clerk.status #5476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ac9742b
wip
panteliselef Feb 26, 2025
68101bf
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Mar 31, 2025
ca6bf2c
wip
panteliselef Mar 31, 2025
88b85ec
cleanup
panteliselef Mar 31, 2025
51e6905
drop deprecations
panteliselef Mar 31, 2025
1b900be
cleanup clerk-js
panteliselef Mar 31, 2025
7f787ea
revert retries load script
panteliselef Mar 31, 2025
4887605
chore(shared): Set a timeout when loading Clerk from an inline script.
panteliselef Apr 2, 2025
3963216
remove unused code
panteliselef Apr 2, 2025
129d423
wip script
panteliselef Apr 2, 2025
bf643c1
Revert "wip script"
panteliselef Apr 2, 2025
a3c9110
fix loading
panteliselef Apr 2, 2025
31d55bd
chore(types): Add `publish:local` script
panteliselef Apr 2, 2025
d7ad92d
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 2, 2025
713c09e
Improve support for older clekr-js versions
panteliselef Apr 3, 2025
f4b0c7d
patch clerkLoaded
panteliselef Apr 3, 2025
cd20067
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 3, 2025
5ccbcc8
add e2e tests
panteliselef Apr 3, 2025
efa56ff
changeset
panteliselef Apr 3, 2025
8eb1093
bump bundlewatch.config.json
panteliselef Apr 3, 2025
ba06c1c
remove addOnLoaded usage
panteliselef Apr 3, 2025
d767b74
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 3, 2025
d66120e
improve comments
panteliselef Apr 3, 2025
e1c64fc
allow for environment to set degraded instead of throwing
panteliselef Apr 3, 2025
0cd9a6d
Update .changeset/cool-showers-admire.md
panteliselef Apr 4, 2025
4026ab8
Update .changeset/cool-showers-admire.md
panteliselef Apr 4, 2025
60cba76
Update .changeset/cool-showers-admire.md
panteliselef Apr 4, 2025
8537f39
Update .changeset/cool-showers-admire.md
panteliselef Apr 4, 2025
1701d64
Update packages/types/src/clerk.ts
panteliselef Apr 4, 2025
cdd0156
move eventBus to shared and expand it
panteliselef Apr 4, 2025
88e451f
address pr comments
panteliselef Apr 4, 2025
66e3264
remove logs
panteliselef Apr 4, 2025
d8e9f00
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 4, 2025
a8b292a
bundlewatch.config.json
panteliselef Apr 4, 2025
c41d70e
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 4, 2025
b1799a3
fix after merge
panteliselef Apr 4, 2025
e7301dc
fix tests
panteliselef Apr 4, 2025
1a34f00
update snapshots
panteliselef Apr 4, 2025
f1d6081
Update .changeset/olive-onions-do.md
panteliselef Apr 4, 2025
4e33f70
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 4, 2025
15a7529
fix unit tests
panteliselef Apr 5, 2025
4d5694b
isomorphicClerk to have its own bus instance
panteliselef Apr 5, 2025
4bdcceb
bump bundlewatch.config.json
panteliselef Apr 5, 2025
812258e
Update packages/react/src/contexts/ClerkContextProvider.tsx
panteliselef Apr 7, 2025
c6179b5
Apply suggestions from code review
panteliselef Apr 8, 2025
44d0cb1
rename status to ClerkStatus
panteliselef Apr 8, 2025
0fef472
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 8, 2025
3190253
update bundlewatch.config.json
panteliselef Apr 8, 2025
b967f04
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
jacekradko Apr 9, 2025
f25d293
remove console.log
jacekradko Apr 9, 2025
85618d6
format
jacekradko Apr 10, 2025
4db40fd
bundlesize
jacekradko Apr 10, 2025
c1452fd
fix stuff
panteliselef Apr 10, 2025
67fbe8f
fix bundlewatch issues
panteliselef Apr 10, 2025
7955e83
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 10, 2025
e56ee84
update doc snapshot
panteliselef Apr 10, 2025
f95ab78
remove export from types
panteliselef Apr 10, 2025
98d807b
add some typedocs
panteliselef Apr 10, 2025
55da252
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 10, 2025
7094902
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 10, 2025
c8b66a7
bump bundlewatch.config.json
panteliselef Apr 10, 2025
6beffe8
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
jacekradko Apr 12, 2025
1ebd587
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
jacekradko Apr 14, 2025
5a4ecad
remove script timeout
panteliselef Apr 17, 2025
a326237
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 17, 2025
e149714
bump bundlewatch.config.json
panteliselef Apr 17, 2025
6b8aaf1
Merge branch 'main' into elef/sdki-730-introduce-way-to-determine-if-…
panteliselef Apr 18, 2025
b04005d
add todo in test
panteliselef Apr 18, 2025
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
37 changes: 37 additions & 0 deletions .changeset/cool-showers-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
'@clerk/nextjs': minor
'@clerk/clerk-react': minor
---

Introduce `useClerk().status` alongside `<ClerkFailed />` and `<ClerkDegraded />`.

### `useClerk().status`
Possible values for `useClerk().status` are:
- `"loading"`: Set during initialization
- `"error"`: Set when hotloading clerk-js failed or `Clerk.load()` failed
- `"ready"`: Set when Clerk is fully operational
- `"degraded"`: Set when Clerk is partially operational
The computed value of `useClerk().loaded` is:

- `true` when `useClerk().status` is either `"ready"` or `"degraded"`.
- `false` when `useClerk().status` is `"loading"` or `"error"`.

### `<ClerkFailed />`
```tsx
<ClerkLoaded>
<MyCustomSignInForm/>
</ClerkLoaded>
<ClerkFailed>
<ContactSupportBanner/>
</ClerkFailed>
```

### `<ClerkDegraded />`
```tsx
<ClerkLoaded>
<MyCustomPasskeyRegistration/>
<ClerkDegraded>
We are experiencing issues, registering a passkey might fail.
</ClerkDegraded>
</ClerkLoaded>
```
15 changes: 15 additions & 0 deletions .changeset/olive-onions-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Introduce `Clerk.status` for tracking the state of the clerk singleton.
Possible values for `Clerk.status` are:
- `"loading"`: Set during initialization
- `"error"`: Set when hotloading clerk-js failed or `Clerk.load()` failed
- `"ready"`: Set when Clerk is fully operational
- `"degraded"`: Set when Clerk is partially operational

The computed value of `Clerk.loaded` is:
- `true` when `Clerk.status` is either `"ready"` or `"degraded"`.
- `false` when `Clerk.status` is `"loading"` or `"error"`.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"types/clerk-pagination-params.mdx",
"types/clerk-pagination-request.mdx",
"types/clerk-resource.mdx",
"types/clerk-status.mdx",
"types/clerk.mdx",
"types/create-organization-params.mdx",
"types/element-object-key.mdx",
Expand Down
34 changes: 34 additions & 0 deletions integration/templates/react-vite/src/clerk-status/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ClerkLoaded, ClerkLoading, ClerkFailed, ClerkDegraded, useClerk } from '@clerk/clerk-react';

export default function ClerkStatusPage() {
const { loaded, status } = useClerk();

return (
<>
<p>Status: {status}</p>
<p>{status === 'loading' ? 'Clerk is loading' : null}</p>
<p>{status === 'error' ? 'Clerk is out' : null}</p>
<p>{status === 'degraded' ? 'Clerk is degraded' : null}</p>
<p>{status === 'ready' ? 'Clerk is ready' : null}</p>
<p>{status === 'ready' || status === 'degraded' ? 'Clerk is ready or degraded (loaded)' : null}</p>
<p>{loaded ? 'Clerk is loaded' : null}</p>
<p>{!loaded ? 'Clerk is NOT loaded' : null}</p>

<ClerkDegraded>
<p>(comp) Clerk is degraded</p>
</ClerkDegraded>

<ClerkLoaded>
<p>(comp) Clerk is loaded,(ready or degraded)</p>
</ClerkLoaded>

<ClerkFailed>
<p>(comp) Something went wrong with Clerk, refresh your page.</p>
</ClerkFailed>

<ClerkLoading>
<p>(comp) Waiting for clerk to fail, ready or regraded.</p>
</ClerkLoading>
</>
);
}
5 changes: 5 additions & 0 deletions integration/templates/react-vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import OrganizationList from './organization-list';
import CreateOrganization from './create-organization';
import OrganizationSwitcher from './organization-switcher';
import Buttons from './buttons';
import ClerkStatusPage from './clerk-status';

const Root = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -114,6 +115,10 @@ const router = createBrowserRouter([
path: '/create-organization',
element: <CreateOrganization />,
},
{
path: '/clerk-status',
element: <ClerkStatusPage />,
},
],
},
]);
Expand Down
15 changes: 15 additions & 0 deletions integration/testUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ const createClerkUtils = ({ page }: TestArgs) => {
return window.Clerk?.session?.actor;
});
},
toBeLoading: async () => {
return page.waitForFunction(() => {
return window.Clerk?.status === 'loading';
});
},
toBeReady: async () => {
return page.waitForFunction(() => {
return window.Clerk?.status === 'ready';
});
},
toBeDegraded: async () => {
return page.waitForFunction(() => {
return window.Clerk?.status === 'degraded';
});
},
getClientSideUser: () => {
return page.evaluate(() => {
return window.Clerk?.user;
Expand Down
147 changes: 131 additions & 16 deletions integration/tests/resiliency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,27 @@ import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../testUtils';

const make500ClerkResponse = () => ({
status: 500,
body: JSON.stringify({
errors: [
{
message: 'Oops, an unexpected error occurred',
long_message: "There was an internal error on our servers. We've been notified and are working on fixing it.",
code: 'internal_clerk_error',
},
],
clerk_trace_id: 'some-trace-id',
}),
});

testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('resiliency @generic', ({ app }) => {
test.describe.configure({ mode: 'serial' });

if (app.name.includes('next')) {
test.skip();
}

let fakeUser: FakeUser;

test.beforeAll(async () => {
Expand All @@ -32,22 +50,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('resilienc
});

// Simulate developer coming back and client fails to load.
await page.route('**/v1/client?**', route => {
return route.fulfill({
status: 500,
body: JSON.stringify({
errors: [
{
message: 'Oops, an unexpected error occurred',
long_message:
"There was an internal error on our servers. We've been notified and are working on fixing it.",
code: 'internal_clerk_error',
},
],
clerk_trace_id: 'some-trace-id',
}),
});
});
await page.route('**/v1/client?**', route => route.fulfill(make500ClerkResponse()));

await page.waitForTimeout(1_000);
await page.reload();
Expand Down Expand Up @@ -140,4 +143,116 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('resilienc

await u.po.clerk.toBeLoaded();
});

test.describe('Clerk.status', () => {
test('normal flow shows correct states and transitions', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/clerk-status');

// Initial state checks
await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is out')).toBeHidden();
await expect(page.getByText('Clerk is degraded')).toBeHidden();
await expect(page.getByText('(comp) Waiting for clerk to fail, ready or regraded.')).toBeVisible();
await u.po.clerk.toBeLoading();

// Wait for loading to complete and verify final state
await expect(page.getByText('Status: ready', { exact: true })).toBeVisible();
await u.po.clerk.toBeLoaded();
await u.po.clerk.toBeReady();
await expect(page.getByText('Clerk is ready', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();

// Verify loading component is no longer visible
await expect(page.getByText('(comp) Waiting for clerk to fail, ready or regraded.')).toBeHidden();
});

test('clerk-js hotloading failed', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await page.route('**/clerk.browser.js', route => route.abort());

await u.page.goToRelative('/clerk-status');

// Initial state checks
await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();

// Wait for loading to complete and verify final state
await expect(page.getByText('Status: error', { exact: true })).toBeVisible({
timeout: 10_000,
});
await expect(page.getByText('Clerk is out', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeHidden();
await expect(page.getByText('Clerk is loaded', { exact: true })).toBeHidden();
await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeHidden();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();

// Verify loading component is no longer visible
await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
});

// TODO: Fix detection of hotloaded clerk-js failing
test.skip('clerk-js client fails and status degraded', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await page.route('**/v1/client?**', route => route.fulfill(make500ClerkResponse()));

await u.page.goToRelative('/clerk-status');

// Initial state checks
await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();

// Wait for loading to complete and verify final state
await expect(page.getByText('Status: degraded', { exact: true })).toBeVisible({
timeout: 10_000,
});
await u.po.clerk.toBeDegraded();
await expect(page.getByText('Clerk is degraded', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is ready', { exact: true })).toBeHidden();
await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeHidden();
await expect(page.getByText('(comp) Clerk is degraded')).toBeVisible();

// Verify loading component is no longer visible
await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
});

test('clerk-js environment fails and status degraded', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await page.route('**/v1/environment?**', route => route.fulfill(make500ClerkResponse()));

await u.page.goToRelative('/clerk-status');

// Initial state checks
await expect(page.getByText('Status: loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is loading', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeVisible();
await u.po.clerk.toBeLoading();

// Wait for loading to complete and verify final state
await expect(page.getByText('Status: degraded', { exact: true })).toBeVisible();
await u.po.clerk.toBeDegraded();
await expect(page.getByText('Clerk is degraded', { exact: true })).toBeVisible();
await expect(page.getByText('Clerk is ready', { exact: true })).toBeHidden();
await expect(page.getByText('Clerk is ready or degraded (loaded)')).toBeVisible();
await expect(page.getByText('Clerk is loaded', { exact: true })).toBeVisible();
await expect(page.getByText('(comp) Clerk is loaded,(ready or degraded)')).toBeVisible();
await expect(page.getByText('Clerk is NOT loaded', { exact: true })).toBeHidden();
await expect(page.getByText('(comp) Clerk is degraded')).toBeVisible();

// Verify loading component is no longer visible
await expect(page.getByText('Clerk is loading', { exact: true })).toBeHidden();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
exports[`public exports should not include a breaking change 1`] = `
[
"AuthenticateWithRedirectCallback",
"ClerkDegraded",
"ClerkFailed",
"ClerkLoaded",
"ClerkLoading",
"ClerkProvider",
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "593kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "74KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "74.2KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "100KB" },
{ "path": "./dist/vendors*.js", "maxSize": "36KB" },
Expand Down
15 changes: 13 additions & 2 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { createClerkEventBus } from '@clerk/shared/clerkEventBus';
import { clerkEvents } from '@clerk/shared/clerkEventBus';
import { createCookieHandler } from '@clerk/shared/cookie';
import { setDevBrowserJWTInURL } from '@clerk/shared/devBrowser';
import { is4xxError, isClerkAPIResponseError, isClerkRuntimeError, isNetworkError } from '@clerk/shared/error';
Expand Down Expand Up @@ -41,9 +43,14 @@ export class AuthCookieService {
private activeOrgCookie: ReturnType<typeof createCookieHandler>;
private devBrowser: DevBrowser;

public static async create(clerk: Clerk, fapiClient: FapiClient, instanceType: InstanceType) {
public static async create(
clerk: Clerk,
fapiClient: FapiClient,
instanceType: InstanceType,
clerkEventBus: ReturnType<typeof createClerkEventBus>,
) {
const cookieSuffix = await getCookieSuffix(clerk.publishableKey);
const service = new AuthCookieService(clerk, fapiClient, cookieSuffix, instanceType);
const service = new AuthCookieService(clerk, fapiClient, cookieSuffix, instanceType, clerkEventBus);
await service.setup();
return service;
}
Expand All @@ -53,6 +60,7 @@ export class AuthCookieService {
fapiClient: FapiClient,
cookieSuffix: string,
private instanceType: InstanceType,
private clerkEventBus: ReturnType<typeof createClerkEventBus>,
) {
// set cookie on token update
eventBus.on(events.TokenUpdate, ({ token }) => {
Expand Down Expand Up @@ -192,6 +200,9 @@ export class AuthCookieService {
return;
}

// The poller failed to fetch a fresh session token, update status to `degraded`.
this.clerkEventBus.emit(clerkEvents.Status, 'degraded');

// --------
// Treat any other error as a noop
// TODO(debug-logs): Once debug logs is available log this error.
Expand Down
Loading