diff --git a/.changeset/young-areas-divide.md b/.changeset/young-areas-divide.md
new file mode 100644
index 00000000000..d68ddff805c
--- /dev/null
+++ b/.changeset/young-areas-divide.md
@@ -0,0 +1,7 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/shared': minor
+'@clerk/react': minor
+---
+
+Introduce `useWaitlist()` hook
diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts
index b85cc6926ce..9dc71de2c77 100644
--- a/integration/presets/envs.ts
+++ b/integration/presets/envs.ts
@@ -117,9 +117,9 @@ const withLegalConsent = base
.setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-legal-consent').sk)
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-legal-consent').pk);
-const withWaitlistdMode = withEmailCodes
+const withWaitlistMode = withEmailCodes
.clone()
- .setId('withWaitlistdMode')
+ .setId('withWaitlistMode')
.setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-waitlist-mode').sk)
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-waitlist-mode').pk);
@@ -213,7 +213,7 @@ export const envs = {
withSignInOrUpEmailLinksFlow,
withSignInOrUpFlow,
withSignInOrUpwithRestrictedModeFlow,
- withWaitlistdMode,
+ withWaitlistMode,
withWhatsappPhoneCode,
withProtectService,
} as const;
diff --git a/integration/templates/custom-flows-react-vite/src/main.tsx b/integration/templates/custom-flows-react-vite/src/main.tsx
index f89e557a8dc..7f8b6058c2c 100644
--- a/integration/templates/custom-flows-react-vite/src/main.tsx
+++ b/integration/templates/custom-flows-react-vite/src/main.tsx
@@ -7,6 +7,7 @@ import { Home } from './routes/Home';
import { SignIn } from './routes/SignIn';
import { SignUp } from './routes/SignUp';
import { Protected } from './routes/Protected';
+import { Waitlist } from './routes/Waitlist';
// Import your Publishable Key
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
@@ -43,6 +44,10 @@ createRoot(document.getElementById('root')!).render(
path='/sign-up'
element={}
/>
+ }
+ />
}
diff --git a/integration/templates/custom-flows-react-vite/src/routes/Waitlist.tsx b/integration/templates/custom-flows-react-vite/src/routes/Waitlist.tsx
new file mode 100644
index 00000000000..59fd25015de
--- /dev/null
+++ b/integration/templates/custom-flows-react-vite/src/routes/Waitlist.tsx
@@ -0,0 +1,112 @@
+'use client';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { useWaitlist } from '@clerk/react';
+import { NavLink } from 'react-router';
+
+export function Waitlist({ className, ...props }: React.ComponentProps<'div'>) {
+ const { waitlist, errors, fetchStatus } = useWaitlist();
+
+ const handleSubmit = async (formData: FormData) => {
+ const emailAddress = formData.get('emailAddress') as string | null;
+
+ if (!emailAddress) {
+ return;
+ }
+
+ await waitlist.join({ emailAddress });
+ };
+
+ if (waitlist?.id) {
+ return (
+
+
+
+ Successfully joined!
+ You're on the waitlist
+
+
+
+
+ Already have an account?{' '}
+
+ Sign in
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Join the Waitlist
+ Enter your email address to join the waitlist
+
+
+
+
+
+
+ );
+}
diff --git a/integration/testUtils/index.ts b/integration/testUtils/index.ts
index 53ee484f8a8..8aef94cccd0 100644
--- a/integration/testUtils/index.ts
+++ b/integration/testUtils/index.ts
@@ -8,6 +8,7 @@ import { createInvitationService } from './invitationsService';
import { createOrganizationsService } from './organizationsService';
import type { FakeAPIKey, FakeOrganization, FakeUser, FakeUserWithEmail } from './usersService';
import { createUserService } from './usersService';
+import { createWaitlistService } from './waitlistService';
export type { FakeAPIKey, FakeOrganization, FakeUser, FakeUserWithEmail };
@@ -40,6 +41,7 @@ export const createTestUtils = <
users: createUserService(clerkClient),
invitations: createInvitationService(clerkClient),
organizations: createOrganizationsService(clerkClient),
+ waitlist: createWaitlistService(clerkClient),
};
if (!params.page) {
diff --git a/integration/testUtils/waitlistService.ts b/integration/testUtils/waitlistService.ts
new file mode 100644
index 00000000000..b858059ca0e
--- /dev/null
+++ b/integration/testUtils/waitlistService.ts
@@ -0,0 +1,19 @@
+import type { ClerkClient } from '@clerk/backend';
+
+export type WaitlistService = {
+ clearWaitlistByEmail: (email: string) => Promise;
+};
+
+export const createWaitlistService = (clerkClient: ClerkClient) => {
+ const self: WaitlistService = {
+ clearWaitlistByEmail: async (email: string) => {
+ const { data: entries } = await clerkClient.waitlistEntries.list({ query: email, status: 'pending' });
+
+ if (entries.length > 0) {
+ await clerkClient.waitlistEntries.delete(entries[0].id);
+ }
+ },
+ };
+
+ return self;
+};
diff --git a/integration/tests/custom-flows/waitlist.test.ts b/integration/tests/custom-flows/waitlist.test.ts
new file mode 100644
index 00000000000..06288ca48b6
--- /dev/null
+++ b/integration/tests/custom-flows/waitlist.test.ts
@@ -0,0 +1,100 @@
+import { parsePublishableKey } from '@clerk/shared/keys';
+import { clerkSetup } from '@clerk/testing/playwright';
+import { expect, test } from '@playwright/test';
+
+import type { Application } from '../../models/application';
+import { hash } from '../../models/helpers';
+import { appConfigs } from '../../presets';
+import { createTestUtils } from '../../testUtils';
+
+test.describe('Custom Flows Waitlist @custom', () => {
+ test.describe.configure({ mode: 'parallel' });
+ let app: Application;
+ const fakeEmails: string[] = [];
+
+ test.beforeAll(async () => {
+ app = await appConfigs.customFlows.reactVite.clone().commit();
+ await app.setup();
+ await app.withEnv(appConfigs.envs.withWaitlistMode);
+ await app.dev();
+
+ const publishableKey = appConfigs.envs.withWaitlistMode.publicVariables.get('CLERK_PUBLISHABLE_KEY');
+ const secretKey = appConfigs.envs.withWaitlistMode.privateVariables.get('CLERK_SECRET_KEY');
+ const apiUrl = appConfigs.envs.withWaitlistMode.privateVariables.get('CLERK_API_URL');
+ const { frontendApi: frontendApiUrl } = parsePublishableKey(publishableKey);
+
+ await clerkSetup({
+ publishableKey,
+ frontendApiUrl,
+ secretKey,
+ // @ts-expect-error
+ apiUrl,
+ dotenv: false,
+ });
+ });
+
+ test.afterAll(async () => {
+ const u = createTestUtils({ app });
+ await Promise.all(fakeEmails.map(email => u.services.waitlist.clearWaitlistByEmail(email)));
+ await app.teardown();
+ });
+
+ test('can join waitlist with email', async ({ page, context }) => {
+ const fakeEmail = `${hash()}+clerk_test@clerkcookie.com`;
+ fakeEmails.push(fakeEmail);
+
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/waitlist');
+ await u.page.waitForClerkJsLoaded();
+ await expect(u.page.getByText('Join the Waitlist', { exact: true })).toBeVisible();
+
+ const emailInput = u.page.getByTestId('email-input');
+ const submitButton = u.page.getByTestId('submit-button');
+
+ await emailInput.fill(fakeEmail);
+ await submitButton.click();
+
+ await expect(u.page.getByText('Successfully joined!')).toBeVisible();
+ await expect(u.page.getByText("You're on the waitlist")).toBeVisible();
+ });
+
+ test('renders error with invalid email', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/waitlist');
+ await u.page.waitForClerkJsLoaded();
+ await expect(u.page.getByText('Join the Waitlist', { exact: true })).toBeVisible();
+
+ const emailInput = u.page.getByTestId('email-input');
+ const submitButton = u.page.getByTestId('submit-button');
+
+ await emailInput.fill('invalid-email@com');
+ await submitButton.click();
+
+ await expect(u.page.getByTestId('email-error')).toBeVisible();
+ });
+
+ test('displays loading state while joining', async ({ page, context }) => {
+ const fakeEmail = `${hash()}+clerk_test@clerkcookie.com`;
+ fakeEmails.push(fakeEmail);
+
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/waitlist');
+ await u.page.waitForClerkJsLoaded();
+ await expect(u.page.getByText('Join the Waitlist', { exact: true })).toBeVisible();
+
+ const emailInput = u.page.getByTestId('email-input');
+ const submitButton = u.page.getByTestId('submit-button');
+
+ await emailInput.fill(fakeEmail);
+
+ const submitPromise = submitButton.click();
+
+ // Check that button is disabled during fetch
+ await expect(submitButton).toBeDisabled();
+
+ await submitPromise;
+
+ // Wait for success state
+ await expect(u.page.getByText('Successfully joined!')).toBeVisible();
+ });
+});
diff --git a/integration/tests/waitlist-mode.test.ts b/integration/tests/waitlist-mode.test.ts
index 3880f898c48..79e867cc31c 100644
--- a/integration/tests/waitlist-mode.test.ts
+++ b/integration/tests/waitlist-mode.test.ts
@@ -78,7 +78,7 @@ test.describe('Waitlist mode', () => {
)
.commit();
await app.setup();
- await app.withEnv(appConfigs.envs.withWaitlistdMode);
+ await app.withEnv(appConfigs.envs.withWaitlistMode);
await app.dev();
const m = createTestUtils({ app });
diff --git a/packages/clerk-js/src/core/resources/Waitlist.ts b/packages/clerk-js/src/core/resources/Waitlist.ts
index 3dd13ebbbc8..f12617bbc30 100644
--- a/packages/clerk-js/src/core/resources/Waitlist.ts
+++ b/packages/clerk-js/src/core/resources/Waitlist.ts
@@ -1,6 +1,9 @@
+import type { ClerkError } from '@clerk/shared/error';
import type { JoinWaitlistParams, WaitlistJSON, WaitlistResource } from '@clerk/shared/types';
import { unixEpochToDate } from '../../utils/date';
+import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask';
+import { eventBus } from '../events';
import { BaseResource } from './internal';
export class Waitlist extends BaseResource implements WaitlistResource {
@@ -10,7 +13,7 @@ export class Waitlist extends BaseResource implements WaitlistResource {
updatedAt: Date | null = null;
createdAt: Date | null = null;
- constructor(data: WaitlistJSON) {
+ constructor(data: WaitlistJSON | null = null) {
super();
this.fromJSON(data);
}
@@ -23,18 +26,24 @@ export class Waitlist extends BaseResource implements WaitlistResource {
this.id = data.id;
this.updatedAt = unixEpochToDate(data.updated_at);
this.createdAt = unixEpochToDate(data.created_at);
+
+ eventBus.emit('resource:update', { resource: this });
return this;
}
+ async join(params: JoinWaitlistParams): Promise<{ error: ClerkError | null }> {
+ return runAsyncResourceTask(this, async () => {
+ await Waitlist.join(params);
+ });
+ }
+
static async join(params: JoinWaitlistParams): Promise {
- const json = (
- await BaseResource._fetch({
- path: '/waitlist',
- method: 'POST',
- body: params as any,
- })
- )?.response as unknown as WaitlistJSON;
-
- return new Waitlist(json);
+ const json = await BaseResource._fetch({
+ path: '/waitlist',
+ method: 'POST',
+ body: params as any,
+ });
+
+ return new Waitlist(json as unknown as WaitlistJSON);
}
}
diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts
index b200154be15..f6f50cf720f 100644
--- a/packages/clerk-js/src/core/signals.ts
+++ b/packages/clerk-js/src/core/signals.ts
@@ -1,11 +1,20 @@
import type { ClerkAPIError, ClerkError } from '@clerk/shared/error';
import { createClerkGlobalHookError, isClerkAPIResponseError } from '@clerk/shared/error';
-import type { Errors, SignInErrors, SignInSignal, SignUpErrors, SignUpSignal } from '@clerk/shared/types';
+import type {
+ Errors,
+ SignInErrors,
+ SignInSignal,
+ SignUpErrors,
+ SignUpSignal,
+ WaitlistErrors,
+ WaitlistSignal,
+} from '@clerk/shared/types';
import { snakeToCamel } from '@clerk/shared/underscore';
import { computed, signal } from 'alien-signals';
import type { SignIn } from './resources/SignIn';
import type { SignUp } from './resources/SignUp';
+import type { Waitlist } from './resources/Waitlist';
export const signInResourceSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ error: ClerkError | null }>({ error: null });
@@ -35,6 +44,20 @@ export const signUpComputedSignal: SignUpSignal = computed(() => {
return { errors, fetchStatus, signUp: signUp ? signUp.__internal_future : null };
});
+export const waitlistResourceSignal = signal<{ resource: Waitlist | null }>({ resource: null });
+export const waitlistErrorSignal = signal<{ error: ClerkError | null }>({ error: null });
+export const waitlistFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' });
+
+export const waitlistComputedSignal: WaitlistSignal = computed(() => {
+ const waitlist = waitlistResourceSignal().resource;
+ const error = waitlistErrorSignal().error;
+ const fetchStatus = waitlistFetchSignal().status;
+
+ const errors = errorsToWaitlistErrors(error);
+
+ return { errors, fetchStatus, waitlist };
+});
+
/**
* Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put
* generic non-API errors into the global array.
@@ -111,3 +134,9 @@ function errorsToSignUpErrors(error: ClerkError | null): SignUpErrors {
legalAccepted: null,
});
}
+
+function errorsToWaitlistErrors(error: ClerkError | null): WaitlistErrors {
+ return errorsToParsedErrors(error, {
+ emailAddress: null,
+ });
+}
diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts
index 411325c068b..d601cdc581d 100644
--- a/packages/clerk-js/src/core/state.ts
+++ b/packages/clerk-js/src/core/state.ts
@@ -6,6 +6,7 @@ import { eventBus } from './events';
import type { BaseResource } from './resources/Base';
import { SignIn } from './resources/SignIn';
import { SignUp } from './resources/SignUp';
+import { Waitlist } from './resources/Waitlist';
import {
signInComputedSignal,
signInErrorSignal,
@@ -15,6 +16,10 @@ import {
signUpErrorSignal,
signUpFetchSignal,
signUpResourceSignal,
+ waitlistComputedSignal,
+ waitlistErrorSignal,
+ waitlistFetchSignal,
+ waitlistResourceSignal,
} from './signals';
export class State implements StateInterface {
@@ -28,6 +33,13 @@ export class State implements StateInterface {
signUpFetchSignal = signUpFetchSignal;
signUpSignal = signUpComputedSignal;
+ waitlistResourceSignal = waitlistResourceSignal;
+ waitlistErrorSignal = waitlistErrorSignal;
+ waitlistFetchSignal = waitlistFetchSignal;
+ waitlistSignal = waitlistComputedSignal;
+
+ private _waitlistInstance: Waitlist;
+
__internal_effect = effect;
__internal_computed = computed;
@@ -35,6 +47,13 @@ export class State implements StateInterface {
eventBus.on('resource:update', this.onResourceUpdated);
eventBus.on('resource:error', this.onResourceError);
eventBus.on('resource:fetch', this.onResourceFetch);
+
+ this._waitlistInstance = new Waitlist(null);
+ this.waitlistResourceSignal({ resource: this._waitlistInstance });
+ }
+
+ get __internal_waitlist() {
+ return this._waitlistInstance;
}
private onResourceError = (payload: { resource: BaseResource; error: ClerkError | null }) => {
@@ -45,6 +64,10 @@ export class State implements StateInterface {
if (payload.resource instanceof SignUp) {
this.signUpErrorSignal({ error: payload.error });
}
+
+ if (payload.resource instanceof Waitlist) {
+ this.waitlistErrorSignal({ error: payload.error });
+ }
};
private onResourceUpdated = (payload: { resource: BaseResource }) => {
@@ -63,6 +86,11 @@ export class State implements StateInterface {
}
this.signUpResourceSignal({ resource: payload.resource });
}
+
+ if (payload.resource instanceof Waitlist) {
+ this._waitlistInstance = payload.resource;
+ this.waitlistResourceSignal({ resource: payload.resource });
+ }
};
private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => {
@@ -73,6 +101,10 @@ export class State implements StateInterface {
if (payload.resource instanceof SignUp) {
this.signUpFetchSignal({ status: payload.status });
}
+
+ if (payload.resource instanceof Waitlist) {
+ this.waitlistFetchSignal({ status: payload.status });
+ }
};
}
diff --git a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
index b1fb6544b7b..e8f2ed3c6c2 100644
--- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -64,6 +64,7 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"useSignIn",
"useSignUp",
"useUser",
+ "useWaitlist",
]
`;
diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts
index 8beaba1c56f..b2ff54e467d 100644
--- a/packages/react/src/hooks/index.ts
+++ b/packages/react/src/hooks/index.ts
@@ -1,6 +1,6 @@
export { useAuth } from './useAuth';
export { useEmailLink } from './useEmailLink';
-export { useSignIn, useSignUp } from './useClerkSignal';
+export { useSignIn, useSignUp, useWaitlist } from './useClerkSignal';
export {
useClerk,
useOrganization,
diff --git a/packages/react/src/hooks/useClerkSignal.ts b/packages/react/src/hooks/useClerkSignal.ts
index a3aa9cbe7c6..54976b6ab79 100644
--- a/packages/react/src/hooks/useClerkSignal.ts
+++ b/packages/react/src/hooks/useClerkSignal.ts
@@ -1,5 +1,5 @@
import { eventMethodCalled } from '@clerk/shared/telemetry';
-import type { SignInSignalValue, SignUpSignalValue } from '@clerk/shared/types';
+import type { SignInSignalValue, SignUpSignalValue, WaitlistSignalValue } from '@clerk/shared/types';
import { useCallback, useSyncExternalStore } from 'react';
import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
@@ -7,7 +7,10 @@ import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvid
function useClerkSignal(signal: 'signIn'): SignInSignalValue;
function useClerkSignal(signal: 'signUp'): SignUpSignalValue;
-function useClerkSignal(signal: 'signIn' | 'signUp'): SignInSignalValue | SignUpSignalValue {
+function useClerkSignal(signal: 'waitlist'): WaitlistSignalValue;
+function useClerkSignal(
+ signal: 'signIn' | 'signUp' | 'waitlist',
+): SignInSignalValue | SignUpSignalValue | WaitlistSignalValue {
useAssertWrappedByClerkProvider('useClerkSignal');
const clerk = useIsomorphicClerkContext();
@@ -19,6 +22,9 @@ function useClerkSignal(signal: 'signIn' | 'signUp'): SignInSignalValue | SignUp
case 'signUp':
clerk.telemetry?.record(eventMethodCalled('useSignUp', { apiVersion: '2025-11' }));
break;
+ case 'waitlist':
+ clerk.telemetry?.record(eventMethodCalled('useWaitlist', { apiVersion: '2025-11' }));
+ break;
default:
break;
}
@@ -37,6 +43,9 @@ function useClerkSignal(signal: 'signIn' | 'signUp'): SignInSignalValue | SignUp
case 'signUp':
clerk.__internal_state.signUpSignal();
break;
+ case 'waitlist':
+ clerk.__internal_state.waitlistSignal();
+ break;
default:
throw new Error(`Unknown signal: ${signal}`);
}
@@ -51,6 +60,8 @@ function useClerkSignal(signal: 'signIn' | 'signUp'): SignInSignalValue | SignUp
return clerk.__internal_state.signInSignal() as SignInSignalValue;
case 'signUp':
return clerk.__internal_state.signUpSignal() as SignUpSignalValue;
+ case 'waitlist':
+ return clerk.__internal_state.waitlistSignal() as WaitlistSignalValue;
default:
throw new Error(`Unknown signal: ${signal}`);
}
@@ -65,7 +76,7 @@ function useClerkSignal(signal: 'signIn' | 'signUp'): SignInSignalValue | SignUp
* This hook allows you to access the Signal-based `SignIn` resource.
*
* @example
- * import { useSignInSignal } from "@clerk/react/experimental";
+ * import { useSignIn } from "@clerk/react";
*
* function SignInForm() {
* const { signIn, errors, fetchStatus } = useSignInSignal();
@@ -82,7 +93,7 @@ export function useSignIn() {
* This hook allows you to access the Signal-based `SignUp` resource.
*
* @example
- * import { useSignUpSignal } from "@clerk/react/experimental";
+ * import { useSignUp } from "@clerk/react";
*
* function SignUpForm() {
* const { signUp, errors, fetchStatus } = useSignUpSignal();
@@ -94,3 +105,20 @@ export function useSignIn() {
export function useSignUp() {
return useClerkSignal('signUp');
}
+
+/**
+ * This hook allows you to access the Signal-based `Waitlist` resource.
+ *
+ * @example
+ * import { useWaitlist } from "@clerk/react";
+ *
+ * function WaitlistForm() {
+ * const { waitlist, errors, fetchStatus } = useWaitlist();
+ * //
+ * }
+ *
+ * @experimental This experimental API is subject to change.
+ */
+export function useWaitlist() {
+ return useClerkSignal('waitlist');
+}
diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts
index e4e9afc9667..046545303da 100644
--- a/packages/react/src/stateProxy.ts
+++ b/packages/react/src/stateProxy.ts
@@ -7,6 +7,8 @@ import type {
SignInErrors,
SignUpErrors,
State,
+ WaitlistErrors,
+ WaitlistResource,
} from '@clerk/shared/types';
import { errorThrower } from './errors/errorThrower';
@@ -38,6 +40,14 @@ const defaultSignUpErrors = (): SignUpErrors => ({
global: null,
});
+const defaultWaitlistErrors = (): WaitlistErrors => ({
+ fields: {
+ emailAddress: null,
+ },
+ raw: null,
+ global: null,
+});
+
type CheckoutSignalProps = {
for?: ForPayerType;
planPeriod: BillingSubscriptionPlanPeriod;
@@ -49,6 +59,7 @@ export class StateProxy implements State {
private readonly signInSignalProxy = this.buildSignInProxy();
private readonly signUpSignalProxy = this.buildSignUpProxy();
+ private readonly waitlistSignalProxy = this.buildWaitlistProxy();
signInSignal() {
return this.signInSignalProxy;
@@ -56,6 +67,13 @@ export class StateProxy implements State {
signUpSignal() {
return this.signUpSignalProxy;
}
+ waitlistSignal() {
+ return this.waitlistSignalProxy;
+ }
+
+ get __internal_waitlist() {
+ return this.state.__internal_waitlist;
+ }
checkoutSignal(params: CheckoutSignalProps) {
return this.buildCheckoutProxy(params);
@@ -259,6 +277,34 @@ export class StateProxy implements State {
};
}
+ private buildWaitlistProxy() {
+ const gateProperty = this.gateProperty.bind(this);
+ const gateMethod = this.gateMethod.bind(this);
+ const target = (): WaitlistResource => {
+ return this.state.__internal_waitlist;
+ };
+
+ return {
+ errors: defaultWaitlistErrors(),
+ fetchStatus: 'idle' as const,
+ waitlist: {
+ pathRoot: '/waitlist',
+ get id() {
+ return gateProperty(target, 'id', '');
+ },
+ get createdAt() {
+ return gateProperty(target, 'createdAt', null);
+ },
+ get updatedAt() {
+ return gateProperty(target, 'updatedAt', null);
+ },
+
+ join: gateMethod(target, 'join'),
+ reload: gateMethod(target, 'reload'),
+ },
+ };
+ }
+
private buildCheckoutProxy(params: CheckoutSignalProps): CheckoutSignalValue {
const gateProperty = this.gateProperty.bind(this);
const targetCheckout = () => this.checkout(params);
@@ -322,6 +368,14 @@ export class StateProxy implements State {
throw new Error('__internal_computed called before Clerk is loaded');
}
+ private get state() {
+ const s = this.isomorphicClerk.__internal_state;
+ if (!s) {
+ throw new Error('Clerk state not ready');
+ }
+ return s;
+ }
+
private get client() {
const c = this.isomorphicClerk.client;
if (!c) {
diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts
index e0163926bb8..33b15d3bfe8 100644
--- a/packages/shared/src/types/clerk.ts
+++ b/packages/shared/src/types/clerk.ts
@@ -43,7 +43,7 @@ import type { Web3Strategy } from './strategies';
import type { TelemetryCollector } from './telemetry';
import type { UserResource } from './user';
import type { Autocomplete, DeepPartial, DeepSnakeToCamel, Without } from './utils';
-import type { WaitlistResource } from './waitlist';
+import type { JoinWaitlistParams, WaitlistResource } from './waitlist';
/**
* Global appearance type registry that can be augmented by packages that depend on `@clerk/ui`.
@@ -2271,10 +2271,6 @@ export interface ClerkAuthenticateWithWeb3Params {
walletName?: string;
}
-export type JoinWaitlistParams = {
- emailAddress: string;
-};
-
export interface AuthenticateWithMetamaskParams {
customNavigate?: (to: string) => Promise;
redirectUrl?: string;
diff --git a/packages/shared/src/types/state.ts b/packages/shared/src/types/state.ts
index 0a36fb1e6a4..7c065226454 100644
--- a/packages/shared/src/types/state.ts
+++ b/packages/shared/src/types/state.ts
@@ -1,6 +1,7 @@
import type { ClerkGlobalHookError } from '../errors/globalHookError';
import type { SignInFutureResource } from './signInFuture';
import type { SignUpFutureResource } from './signUpFuture';
+import type { WaitlistResource } from './waitlist';
/**
* Represents an error on a specific field.
@@ -99,6 +100,16 @@ export interface SignUpFields {
legalAccepted: FieldError | null;
}
+/**
+ * Fields available for Waitlist errors.
+ */
+export interface WaitlistFields {
+ /**
+ * The error for the email address field.
+ */
+ emailAddress: FieldError | null;
+}
+
/**
* Errors type for SignIn operations.
*/
@@ -109,6 +120,11 @@ export type SignInErrors = Errors;
*/
export type SignUpErrors = Errors;
+/**
+ * Errors type for Waitlist operations.
+ */
+export type WaitlistErrors = Errors;
+
/**
* The value returned by the `useSignInSignal` hook.
*/
@@ -154,6 +170,27 @@ export interface SignUpSignal {
(): NullableSignUpSignal;
}
+export interface WaitlistSignalValue {
+ /**
+ * The errors that occurred during the last fetch of the underlying `Waitlist` resource.
+ */
+ errors: WaitlistErrors;
+ /**
+ * The fetch status of the underlying `Waitlist` resource.
+ */
+ fetchStatus: 'idle' | 'fetching';
+ /**
+ * The underlying `Waitlist` resource.
+ */
+ waitlist: WaitlistResource;
+}
+export type NullableWaitlistSignal = Omit & {
+ waitlist: WaitlistResource | null;
+};
+export interface WaitlistSignal {
+ (): NullableWaitlistSignal;
+}
+
export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
@@ -165,6 +202,11 @@ export interface State {
*/
signUpSignal: SignUpSignal;
+ /**
+ * A Signal that updates when the underlying `Waitlist` resource changes, including errors.
+ */
+ waitlistSignal: WaitlistSignal;
+
/**
* An alias for `effect()` from `alien-signals`, which can be used to subscribe to changes from Signals.
*
@@ -183,4 +225,8 @@ export interface State {
* @experimental This experimental API is subject to change.
*/
__internal_computed: (getter: (previousValue?: T) => T) => () => T;
+ /**
+ * An instance of the Waitlist resource.
+ */
+ __internal_waitlist: WaitlistResource;
}
diff --git a/packages/shared/src/types/waitlist.ts b/packages/shared/src/types/waitlist.ts
index 8b8fe3a7ee1..52f16a427b6 100644
--- a/packages/shared/src/types/waitlist.ts
+++ b/packages/shared/src/types/waitlist.ts
@@ -1,7 +1,28 @@
+import type { ClerkError } from '../error';
import type { ClerkResource } from './resource';
export interface WaitlistResource extends ClerkResource {
- id: string;
- createdAt: Date | null;
- updatedAt: Date | null;
+ /**
+ * The unique identifier for the waitlist entry. `''` if the user has not joined the waitlist yet.
+ */
+ readonly id: string;
+
+ /**
+ * The date and time the waitlist entry was created. `null` if the user has not joined the waitlist yet.
+ */
+ readonly createdAt: Date | null;
+
+ /**
+ * The date and time the waitlist entry was last updated. `null` if the user has not joined the waitlist yet.
+ */
+ readonly updatedAt: Date | null;
+
+ /**
+ * Used to add the provided `emailAddress` to the waitlist.
+ */
+ join: (params: JoinWaitlistParams) => Promise<{ error: ClerkError | null }>;
}
+
+export type JoinWaitlistParams = {
+ emailAddress: string;
+};
diff --git a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap
index eaba504c812..21c0511015a 100644
--- a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -69,6 +69,7 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"useSignIn",
"useSignUp",
"useUser",
+ "useWaitlist",
]
`;