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
7 changes: 7 additions & 0 deletions .changeset/tasty-bears-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal implementation for SignUp
27 changes: 27 additions & 0 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
SignUpAuthenticateWithWeb3Params,
SignUpCreateParams,
SignUpField,
SignUpFutureResource,
SignUpIdentificationField,
SignUpJSON,
SignUpJSONSnapshot,
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
clerkVerifyEmailAddressCalledBeforeCreate,
clerkVerifyWeb3WalletCalledBeforeCreate,
} from '../errors';
import { eventBus } from '../events';
import { BaseResource, ClerkRuntimeError, SignUpVerifications } from './internal';

declare global {
Expand Down Expand Up @@ -77,6 +79,21 @@ export class SignUp extends BaseResource implements SignUpResource {
abandonAt: number | null = null;
legalAcceptedAt: number | null = null;

/**
* @experimental This experimental API is subject to change.
*
* An instance of `SignUpFuture`, which has a different API than `SignUp`, intended to be used in custom flows.
*/
__internal_future: SignUpFuture | null = new SignUpFuture(this);

/**
* @internal Only used for internal purposes, and is not intended to be used directly.
*
* This property is used to provide access to underlying Client methods to `SignUpFuture`, which wraps an instance
* of `SignUp`.
*/
__internal_basePost = this._basePost.bind(this);

constructor(data: SignUpJSON | SignUpJSONSnapshot | null = null) {
super();
this.fromJSON(data);
Expand Down Expand Up @@ -389,6 +406,8 @@ export class SignUp extends BaseResource implements SignUpResource {
this.web3wallet = data.web3_wallet;
this.legalAcceptedAt = data.legal_accepted_at;
}

eventBus.emit('resource:update', { resource: this });
return this;
}

Expand Down Expand Up @@ -449,3 +468,11 @@ export class SignUp extends BaseResource implements SignUpResource {
return false;
}
}

class SignUpFuture implements SignUpFutureResource {
constructor(readonly resource: SignUp) {}

get status() {
return this.resource.status;
}
}
19 changes: 17 additions & 2 deletions packages/clerk-js/src/core/signals.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import type { Errors, SignInSignal, SignUpSignal } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';
import type { SignUp } from './resources/SignUp';

export const signInResourceSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });
export const signInFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' });

export const signInComputedSignal = computed(() => {
export const signInComputedSignal: SignInSignal = computed(() => {
const signIn = signInResourceSignal().resource;
const error = signInErrorSignal().error;
const fetchStatus = signInFetchSignal().status;
Expand All @@ -18,6 +19,20 @@ export const signInComputedSignal = computed(() => {
return { errors, fetchStatus, signIn: signIn ? signIn.__internal_future : null };
});

export const signUpResourceSignal = signal<{ resource: SignUp | null }>({ resource: null });
export const signUpErrorSignal = signal<{ error: unknown }>({ error: null });
export const signUpFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' });

export const signUpComputedSignal: SignUpSignal = computed(() => {
const signUp = signUpResourceSignal().resource;
const error = signUpErrorSignal().error;
const fetchStatus = signUpFetchSignal().status;

const errors = errorsToParsedErrors(error);

return { errors, fetchStatus, signUp: signUp ? signUp.__internal_future : null };
});
Comment on lines +26 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add tests to cover SignUp signal behavior (parity with SignIn tests)

No tests are shown in this PR. Please add tests for:

  • fetchStatus transitions ('idle' -> 'fetching' -> 'idle')
  • error parsing mapping into field/global buckets
  • exposing null vs non-null signUp values as the resource updates

I can scaffold these tests. Meanwhile, to check current coverage, run:


🏁 Script executed:

#!/bin/bash
# Verify if there are SignUp signal tests
rg -nP -C2 --type=ts "signUp(ResourceSignal|ErrorSignal|FetchSignal|ComputedSignal|Signal)" packages | sed -n '1,200p'

# Look for tests referring to SignUp signals
rg -nP -C2 --type=ts -g '!*node_modules*' "(useSignUpSignal|signUpComputedSignal)" packages | sed -n '1,200p'

Length of output: 5847


Add unit tests for SignUp signal behavior
To achieve parity with existing SignIn tests, please add tests covering the following scenarios for signUpComputedSignal in packages/clerk-js (e.g. under a new __tests__ folder alongside signals.ts):

  • fetchStatus transitions: verify it moves from 'idle''fetching''idle' when a sign-up request starts and finishes.
  • error parsing: exercise errorsToParsedErrors(error) for both field-specific and global errors.
  • resource output: assert that before the SignUp resource arrives you get signUp: null, then once it’s set you get the correct __internal_future value.
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around lines 26 to 34, add Jest unit
tests under packages/clerk-js/src/core/__tests__/signals.test.ts that cover
signUpComputedSignal: write a test that simulates fetchStatus transitions by
setting underlying signUpFetchSignal status from 'idle' → 'fetching' → 'idle'
and asserting computed fetchStatus follows those changes; write tests that call
errorsToParsedErrors with both field-specific and global error shapes and assert
signUpComputedSignal.errors matches parsed output; and write a test that asserts
signUpComputedSignal.signUp is null before signUpResourceSignal.resource is set
and equals the resource's __internal_future after you set the resource. Use the
same mocking/setup utilities and signal setters used by existing SignIn tests to
mutate the underlying signals and ensure tests clean up/reset signal state
between cases.


/**
* 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.
Expand Down
29 changes: 28 additions & 1 deletion packages/clerk-js/src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,29 @@ import { computed, effect } from 'alien-signals';
import { eventBus } from './events';
import type { BaseResource } from './resources/Base';
import { SignIn } from './resources/SignIn';
import { signInComputedSignal, signInErrorSignal, signInFetchSignal, signInResourceSignal } from './signals';
import { SignUp } from './resources/SignUp';
import {
signInComputedSignal,
signInErrorSignal,
signInFetchSignal,
signInResourceSignal,
signUpComputedSignal,
signUpErrorSignal,
signUpFetchSignal,
signUpResourceSignal,
} from './signals';

export class State implements StateInterface {
signInResourceSignal = signInResourceSignal;
signInErrorSignal = signInErrorSignal;
signInFetchSignal = signInFetchSignal;
signInSignal = signInComputedSignal;

signUpResourceSignal = signUpResourceSignal;
signUpErrorSignal = signUpErrorSignal;
signUpFetchSignal = signUpFetchSignal;
signUpSignal = signUpComputedSignal;

__internal_effect = effect;
__internal_computed = computed;

Expand All @@ -25,17 +40,29 @@ export class State implements StateInterface {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ error: payload.error });
}

if (payload.resource instanceof SignUp) {
this.signUpResourceSignal({ resource: payload.resource });
}
};

private onResourceUpdated = (payload: { resource: BaseResource }) => {
if (payload.resource instanceof SignIn) {
this.signInResourceSignal({ resource: payload.resource });
}

if (payload.resource instanceof SignUp) {
this.signUpResourceSignal({ resource: payload.resource });
}
};

private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => {
if (payload.resource instanceof SignIn) {
this.signInFetchSignal({ status: payload.status });
}

if (payload.resource instanceof SignUp) {
this.signUpFetchSignal({ status: payload.status });
}
};
}
17 changes: 15 additions & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { SignInSignal, SignUpSignal } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

function useClerkSignal(signal: 'signIn') {
useAssertWrappedByClerkProvider('useSignInSignal');
function useClerkSignal(signal: 'signIn'): ReturnType<SignInSignal> | null;
function useClerkSignal(signal: 'signUp'): ReturnType<SignUpSignal> | null;
function useClerkSignal(signal: 'signIn' | 'signUp'): ReturnType<SignInSignal> | ReturnType<SignUpSignal> | null {
useAssertWrappedByClerkProvider('useClerkSignal');
Comment on lines +7 to +10
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Ensure hook is re-exported from the package entrypoint and add basic tests

Please confirm useSignUpSignal is exported from the public React package entry and add at least smoke tests covering initial null, update on state change, and error propagation.

Run the following to verify exports and test coverage placeholders:

Also applies to: 58-64


🏁 Script executed:

#!/bin/bash
# Check if useSignUpSignal is exported from the React package entrypoint(s)
fd -t f index.ts packages/react | xargs -I{} rg -nP -C2 "useSignUpSignal" {}

# Check for existing tests referencing the hook
rg -nP -C2 --type=ts "useSignUpSignal" packages/react

# If not present, I can scaffold react-testing-library tests analogous to existing SignIn hook tests.

Length of output: 440


Export hooks from the public React entrypoint and add smoke tests

The useSignInSignal and useSignUpSignal implementations live in packages/react/src/hooks/useClerkSignal.ts but aren’t re-exported from the package’s main entrypoint, and there are no tests validating their behavior. Please:

  • Re-export both hooks in your React package entrypoint (e.g. packages/react/src/index.ts or packages/react/index.ts):
    export { useSignInSignal, useSignUpSignal } from './hooks/useClerkSignal';
  • Add basic smoke tests (e.g. in packages/react/src/hooks/__tests__/useClerkSignal.test.tsx) that assert:
    • The hook returns null before Clerk is initialized
    • It updates when the underlying signal state changes
    • It surfaces errors from the signal

You can mirror the existing Sign-In hook tests to keep consistency.

🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts around lines 7 to 10, the
sign-in/up hooks are implemented but not exported from the React package
entrypoint and there are no smoke tests; re-export useSignInSignal and
useSignUpSignal from the package entrypoint (packages/react/src/index.ts or
packages/react/index.ts) so they are publicly available, and add a new test file
packages/react/src/hooks/__tests__/useClerkSignal.test.tsx containing basic
smoke tests that assert: the hook returns null before Clerk is initialized, the
hook updates when the underlying signal state changes, and the hook surfaces
errors from the signal—mirror the existing Sign-In hook tests for structure and
assertions.


const clerk = useIsomorphicClerkContext();

Expand All @@ -20,6 +23,10 @@ function useClerkSignal(signal: 'signIn') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know that the state is defined
clerk.__internal_state!.signInSignal();
break;
case 'signUp':
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- we know that the state is defined
clerk.__internal_state!.signUpSignal();
break;
default:
throw new Error(`Unknown signal: ${signal}`);
}
Expand All @@ -36,6 +43,8 @@ function useClerkSignal(signal: 'signIn') {
switch (signal) {
case 'signIn':
return clerk.__internal_state.signInSignal();
case 'signUp':
return clerk.__internal_state.signUpSignal();
default:
throw new Error(`Unknown signal: ${signal}`);
}
Expand All @@ -49,3 +58,7 @@ function useClerkSignal(signal: 'signIn') {
export function useSignInSignal() {
return useClerkSignal('signIn');
}

export function useSignUpSignal() {
return useClerkSignal('signUp');
}
Comment on lines +61 to +64
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Expose explicit return type and mark as experimental; consider similar treatment for useSignInSignal

Public hooks should have explicit return types. Also surface the experimental nature of the new hook in JSDoc.

Apply this diff:

-export function useSignUpSignal() {
-  return useClerkSignal('signUp');
-}
+/**
+ * @experimental A React hook that subscribes to the SignUp Signal.
+ * Returns a snapshot with `{ errors, fetchStatus, signUp }`, or `null` before Clerk state is ready.
+ */
+export function useSignUpSignal(): ReturnType<State['signUpSignal']> | null {
+  return useClerkSignal('signUp');
+}

Additionally, for consistency, annotate useSignInSignal (outside this range):

/**
 * A React hook that subscribes to the SignIn Signal.
 * Returns a snapshot with `{ errors, fetchStatus, signIn }`, or `null` before Clerk state is ready.
 */
export function useSignInSignal(): ReturnType<State['signInSignal']> | null {
  return useClerkSignal('signIn');
}

And ensure the type-only import:

import type { State } from '@clerk/types';

Lastly, please add or update tests to cover:

  • Subscribing via useSignUpSignal and receiving updated snapshots on sign-up state mutations.
  • Error propagation in the errors field.
  • Proper null initial snapshot behavior pre-load.

Happy to scaffold react-testing-library tests that simulate state updates via the internal state adapter.

🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts around lines 58 to 61, the
exported hook useSignUpSignal lacks an explicit return type and JSDoc marking it
experimental; update the hook to include an explicit return type using State
(add a type-only import: import type { State } from '@clerk/types') and add an
experimental JSDoc comment describing the snapshot shape (e.g.,
ReturnType<State['signUpSignal']> | null). Also apply the analogous annotation
shown for useSignInSignal elsewhere in the file. Finally, add/update tests
(react-testing-library) to assert subscribing via useSignUpSignal receives
snapshots on sign-up state mutations, that errors propagate into the errors
field, and that the hook returns null as the initial snapshot before Clerk state
is ready.

4 changes: 4 additions & 0 deletions packages/types/src/signUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export interface SignUpResource extends ClerkResource {
__internal_toSnapshot: () => SignUpJSONSnapshot;
}

export interface SignUpFutureResource {
status: SignUpStatus | null;
}

export type SignUpStatus = 'missing_requirements' | 'complete' | 'abandoned';

export type SignUpField = SignUpAttributeField | SignUpIdentificationField;
Expand Down
30 changes: 23 additions & 7 deletions packages/types/src/state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SignInFutureResource } from './signIn';
import type { SignUpFutureResource } from './signUp';

interface FieldError {
code: string;
Expand All @@ -25,17 +26,32 @@ export interface Errors {
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface SignInSignal {
(): {
errors: Errors;
fetchStatus: 'idle' | 'fetching';
signIn: SignInFutureResource | null;
};
}

export interface SignUpSignal {
(): {
errors: Errors;
fetchStatus: 'idle' | 'fetching';
signUp: SignUpFutureResource | null;
};
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: Errors;
fetchStatus: 'idle' | 'fetching';
signIn: SignInFutureResource | null;
};
};
signInSignal: SignInSignal;

/**
* A Signal that updates when the underlying `SignUp` resource changes, including errors.
*/
signUpSignal: SignUpSignal;

/**
* @experimental This experimental API is subject to change.
Expand Down