From 68b5dd04fd10ff455266053a5c90e1d9905a2700 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:33:21 -0500 Subject: [PATCH 1/2] feat(clerk-js,clerk-react,types): Signal SignUp --- .changeset/tasty-bears-sip.md | 7 +++++ .../clerk-js/src/core/resources/SignUp.ts | 27 +++++++++++++++++ packages/clerk-js/src/core/signals.ts | 15 ++++++++++ packages/clerk-js/src/core/state.ts | 29 ++++++++++++++++++- packages/react/src/hooks/useClerkSignal.ts | 14 +++++++-- packages/types/src/signUp.ts | 4 +++ packages/types/src/state.ts | 12 ++++++++ 7 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 .changeset/tasty-bears-sip.md diff --git a/.changeset/tasty-bears-sip.md b/.changeset/tasty-bears-sip.md new file mode 100644 index 00000000000..ae198daccb2 --- /dev/null +++ b/.changeset/tasty-bears-sip.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +[Experimental] Signal implementation for SignUp diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 079b6c34d67..df9a24f8afd 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -16,6 +16,7 @@ import type { SignUpAuthenticateWithWeb3Params, SignUpCreateParams, SignUpField, + SignUpFutureResource, SignUpIdentificationField, SignUpJSON, SignUpJSONSnapshot, @@ -45,6 +46,7 @@ import { clerkVerifyEmailAddressCalledBeforeCreate, clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; +import { eventBus } from '../events'; import { BaseResource, ClerkRuntimeError, SignUpVerifications } from './internal'; declare global { @@ -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); @@ -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; } @@ -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; + } +} diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index 88cc6118cde..3504ccab0d2 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -3,6 +3,7 @@ import type { Errors } 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 }); @@ -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 = 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 }; +}); + /** * 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. diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index cdb778bc728..a78586b920c 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -4,7 +4,17 @@ 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; @@ -12,6 +22,11 @@ export class State implements StateInterface { signInFetchSignal = signInFetchSignal; signInSignal = signInComputedSignal; + signUpResourceSignal = signUpResourceSignal; + signUpErrorSignal = signUpErrorSignal; + signUpFetchSignal = signUpFetchSignal; + signUpSignal = signUpComputedSignal; + __internal_effect = effect; __internal_computed = computed; @@ -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 }); + } }; } diff --git a/packages/react/src/hooks/useClerkSignal.ts b/packages/react/src/hooks/useClerkSignal.ts index 25d2b37362a..74d2539046d 100644 --- a/packages/react/src/hooks/useClerkSignal.ts +++ b/packages/react/src/hooks/useClerkSignal.ts @@ -3,8 +3,8 @@ import { useCallback, useSyncExternalStore } from 'react'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider'; -function useClerkSignal(signal: 'signIn') { - useAssertWrappedByClerkProvider('useSignInSignal'); +function useClerkSignal(signal: 'signIn' | 'signUp') { + useAssertWrappedByClerkProvider('useClerkSignal'); const clerk = useIsomorphicClerkContext(); @@ -20,6 +20,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}`); } @@ -36,6 +40,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}`); } @@ -49,3 +55,7 @@ function useClerkSignal(signal: 'signIn') { export function useSignInSignal() { return useClerkSignal('signIn'); } + +export function useSignUpSignal() { + return useClerkSignal('signUp'); +} diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index d9485906aa3..d559c90a041 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -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; diff --git a/packages/types/src/state.ts b/packages/types/src/state.ts index 007bd92f295..e019a069267 100644 --- a/packages/types/src/state.ts +++ b/packages/types/src/state.ts @@ -1,4 +1,5 @@ import type { SignInFutureResource } from './signIn'; +import type { SignUpFutureResource } from './signUp'; interface FieldError { code: string; @@ -37,6 +38,17 @@ export interface State { }; }; + /** + * A Signal that updates when the underlying `SignUp` resource changes, including errors. + */ + signUpSignal: { + (): { + errors: Errors; + fetchStatus: 'idle' | 'fetching'; + signUp: SignUpFutureResource | null; + }; + }; + /** * @experimental This experimental API is subject to change. * From 3cd0af2eef6c776f98cb9121ee88a636498203c9 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Mon, 18 Aug 2025 11:06:52 -0500 Subject: [PATCH 2/2] fix(clerk-js,clerk-react,types): Add explicit return types --- packages/clerk-js/src/core/signals.ts | 6 ++-- packages/react/src/hooks/useClerkSignal.ts | 5 +++- packages/types/src/state.ts | 32 ++++++++++++---------- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index 3504ccab0d2..ca3ac6ecddd 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -1,5 +1,5 @@ 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'; @@ -9,7 +9,7 @@ export const signInResourceSignal = signal<{ resource: SignIn | null }>({ resour 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; @@ -23,7 +23,7 @@ export const signUpResourceSignal = signal<{ resource: SignUp | null }>({ resour export const signUpErrorSignal = signal<{ error: unknown }>({ error: null }); export const signUpFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); -export const signUpComputedSignal = computed(() => { +export const signUpComputedSignal: SignUpSignal = computed(() => { const signUp = signUpResourceSignal().resource; const error = signUpErrorSignal().error; const fetchStatus = signUpFetchSignal().status; diff --git a/packages/react/src/hooks/useClerkSignal.ts b/packages/react/src/hooks/useClerkSignal.ts index 74d2539046d..ceb8f3d9393 100644 --- a/packages/react/src/hooks/useClerkSignal.ts +++ b/packages/react/src/hooks/useClerkSignal.ts @@ -1,9 +1,12 @@ +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' | 'signUp') { +function useClerkSignal(signal: 'signIn'): ReturnType | null; +function useClerkSignal(signal: 'signUp'): ReturnType | null; +function useClerkSignal(signal: 'signIn' | 'signUp'): ReturnType | ReturnType | null { useAssertWrappedByClerkProvider('useClerkSignal'); const clerk = useIsomorphicClerkContext(); diff --git a/packages/types/src/state.ts b/packages/types/src/state.ts index e019a069267..ad59b9d4e72 100644 --- a/packages/types/src/state.ts +++ b/packages/types/src/state.ts @@ -26,28 +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: { - (): { - errors: Errors; - fetchStatus: 'idle' | 'fetching'; - signUp: SignUpFutureResource | null; - }; - }; + signUpSignal: SignUpSignal; /** * @experimental This experimental API is subject to change.