diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index e98538e1ba5..88892ff381c 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -96,6 +96,7 @@ import { eventBus } from '../events'; import { BaseResource, UserData, Verification } from './internal'; export class SignIn extends BaseResource implements SignInResource { + static readonly __internal_resourceName = 'signIn' as const; pathRoot = '/client/sign_ins'; id?: string; diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index fbc697a210f..2d81fcb515b 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -76,6 +76,7 @@ declare global { } export class SignUp extends BaseResource implements SignUpResource { + static readonly __internal_resourceName = 'signUp' as const; pathRoot = '/client/sign_ups'; id: string | undefined; diff --git a/packages/clerk-js/src/core/resources/Waitlist.ts b/packages/clerk-js/src/core/resources/Waitlist.ts index 044f3888061..f5ca4d820b5 100644 --- a/packages/clerk-js/src/core/resources/Waitlist.ts +++ b/packages/clerk-js/src/core/resources/Waitlist.ts @@ -6,6 +6,7 @@ import { eventBus } from '../events'; import { BaseResource } from './internal'; export class Waitlist extends BaseResource implements WaitlistResource { + static readonly __internal_resourceName = 'waitlist' as const; pathRoot = '/waitlist'; id = ''; diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index e3f98ad4787..ce23d258616 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -3,51 +3,115 @@ import { snakeToCamel } from '@clerk/shared/underscore'; import type { Errors, SignInSignal, SignUpSignal, WaitlistSignal } from '@clerk/types'; 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: unknown }>({ error: null }); -export const signInFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); - -export const signInComputedSignal: SignInSignal = computed(() => { - const signIn = signInResourceSignal().resource; - const error = signInErrorSignal().error; - const fetchStatus = signInFetchSignal().status; - - const errors = errorsToParsedErrors(error); - - 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 }; -}); +import { SignIn } from './resources/SignIn'; +import { SignUp } from './resources/SignUp'; +import { Waitlist } from './resources/Waitlist'; +import type { BaseResource } from './resources/Base'; + +interface ResourceSignalSet { + resourceSignal: ReturnType>; + errorSignal: ReturnType>; + fetchSignal: ReturnType>; + computedSignal: TComputedSignal; +} -export const waitlistResourceSignal = signal<{ resource: Waitlist | null }>({ resource: null }); -export const waitlistErrorSignal = signal<{ error: unknown }>({ error: null }); -export const waitlistFetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); +type ResourceClass = new (...args: any[]) => T & { + __internal_future: any; + static __internal_resourceName: string; +}; + +type ResourceName = T['__internal_resourceName']; + +function createResourceSignalSet< + TResource extends { __internal_future: any }, + TSignalName extends string, + TComputedSignal extends () => { errors: Errors; fetchStatus: 'idle' | 'fetching'; [K in TSignalName]: any }, +>( + resourceName: TSignalName, +): ResourceSignalSet { + const resourceSignal = signal<{ resource: TResource | null }>({ resource: null }); + const errorSignal = signal<{ error: unknown }>({ error: null }); + const fetchSignal = signal<{ status: 'idle' | 'fetching' }>({ status: 'idle' }); + + const computedSignal = computed(() => { + const resource = resourceSignal().resource; + const error = errorSignal().error; + const fetchStatus = fetchSignal().status; + const errors = errorsToParsedErrors(error); + + return { + errors, + fetchStatus, + [resourceName]: resource ? resource.__internal_future : null, + } as ReturnType; + }) as TComputedSignal; + + return { + resourceSignal, + errorSignal, + fetchSignal, + computedSignal, + }; +} -export const waitlistComputedSignal: WaitlistSignal = computed(() => { - const waitlist = waitlistResourceSignal().resource; - const error = waitlistErrorSignal().error; - const fetchStatus = waitlistFetchSignal().status; +const resourceSignalRegistry = new Map< + ResourceClass, + ResourceSignalSet +>(); + +const resourceNameToSignalSet = new Map>(); + +function registerResourceSignals< + TResourceClass extends ResourceClass, + TResource extends InstanceType, + TSignalName extends ResourceName, +>( + ResourceClass: TResourceClass & { __internal_resourceName: TSignalName }, + signalType: () => { errors: Errors; fetchStatus: 'idle' | 'fetching'; [K in TSignalName]: any }, +): ResourceSignalSet> { + const resourceName = ResourceClass.__internal_resourceName; + const signalSet = createResourceSignalSet>( + resourceName, + ); + resourceSignalRegistry.set(ResourceClass as ResourceClass, signalSet); + resourceNameToSignalSet.set(resourceName, signalSet); + return signalSet; +} - const errors = errorsToParsedErrors(error); +export function getSignalSetByResourceName( + resourceName: string, +): ResourceSignalSet | undefined { + return resourceNameToSignalSet.get(resourceName); +} - return { errors, fetchStatus, waitlist: waitlist ? waitlist.__internal_future : null }; -}); +const signInSignals = registerResourceSignals(SignIn, (() => ({})) as SignInSignal); +export const signInResourceSignal = signInSignals.resourceSignal; +export const signInErrorSignal = signInSignals.errorSignal; +export const signInFetchSignal = signInSignals.fetchSignal; +export const signInComputedSignal = signInSignals.computedSignal; + +const signUpSignals = registerResourceSignals(SignUp, (() => ({})) as SignUpSignal); +export const signUpResourceSignal = signUpSignals.resourceSignal; +export const signUpErrorSignal = signUpSignals.errorSignal; +export const signUpFetchSignal = signUpSignals.fetchSignal; +export const signUpComputedSignal = signUpSignals.computedSignal; + +const waitlistSignals = registerResourceSignals(Waitlist, (() => ({})) as WaitlistSignal); +export const waitlistResourceSignal = waitlistSignals.resourceSignal; +export const waitlistErrorSignal = waitlistSignals.errorSignal; +export const waitlistFetchSignal = waitlistSignals.fetchSignal; +export const waitlistComputedSignal = waitlistSignals.computedSignal; + +export function getResourceSignalSet( + resource: BaseResource, +): ResourceSignalSet | undefined { + for (const [ResourceClass, signalSet] of resourceSignalRegistry) { + if (resource instanceof ResourceClass) { + return signalSet; + } + } + return undefined; +} /** * Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index 1977947bf0e..bea45d72a92 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -3,40 +3,18 @@ import { computed, effect } from 'alien-signals'; 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, - signInFetchSignal, - signInResourceSignal, - signUpComputedSignal, - signUpErrorSignal, - signUpFetchSignal, - signUpResourceSignal, - waitlistComputedSignal, - waitlistErrorSignal, - waitlistFetchSignal, - waitlistResourceSignal, + getResourceSignalSet, + getSignalSetByResourceName, } from './signals'; -export class State implements StateInterface { - signInResourceSignal = signInResourceSignal; - signInErrorSignal = signInErrorSignal; - signInFetchSignal = signInFetchSignal; - signInSignal = signInComputedSignal; - - signUpResourceSignal = signUpResourceSignal; - signUpErrorSignal = signUpErrorSignal; - signUpFetchSignal = signUpFetchSignal; - signUpSignal = signUpComputedSignal; - - waitlistResourceSignal = waitlistResourceSignal; - waitlistErrorSignal = waitlistErrorSignal; - waitlistFetchSignal = waitlistFetchSignal; - waitlistSignal = waitlistComputedSignal; +type ResourceClassWithName = new (...args: any[]) => BaseResource & { + __internal_future: any; + static __internal_resourceName: string; +}; +export class State implements StateInterface { private _waitlistInstance: Waitlist | null = null; __internal_effect = effect; @@ -48,52 +26,63 @@ export class State implements StateInterface { eventBus.on('resource:fetch', this.onResourceFetch); this._waitlistInstance = new Waitlist(null); - this.waitlistResourceSignal({ resource: this._waitlistInstance }); + const waitlistSignalSet = getSignalSetByResourceName('waitlist'); + if (waitlistSignalSet) { + waitlistSignalSet.resourceSignal({ resource: this._waitlistInstance }); + } } get __internal_waitlist() { return this._waitlistInstance; } - private onResourceError = (payload: { resource: BaseResource; error: unknown }) => { - if (payload.resource instanceof SignIn) { - this.signInErrorSignal({ error: payload.error }); - } + getSignalsForResource(resource: BaseResource) { + return getResourceSignalSet(resource); + } - if (payload.resource instanceof SignUp) { - this.signUpErrorSignal({ error: payload.error }); - } + getSignalsByName(resourceName: string) { + return getSignalSetByResourceName(resourceName); + } - if (payload.resource instanceof Waitlist) { - this.waitlistErrorSignal({ error: payload.error }); - } - }; + getSignalsForClass(ResourceClass: T) { + return getSignalSetByResourceName(ResourceClass.__internal_resourceName); + } - private onResourceUpdated = (payload: { resource: BaseResource }) => { - if (payload.resource instanceof SignIn) { - this.signInResourceSignal({ resource: payload.resource }); - } + getSignalForResourceName( + resourceName: T, + ): (() => { errors: any; fetchStatus: 'idle' | 'fetching'; [K in T]: any }) | undefined { + const signalSet = getSignalSetByResourceName(resourceName); + return signalSet?.computedSignal; + } - if (payload.resource instanceof SignUp) { - this.signUpResourceSignal({ resource: payload.resource }); - } + getSignalForResource(resource: BaseResource) { + const signalSet = getResourceSignalSet(resource); + return signalSet?.computedSignal; + } - if (payload.resource instanceof Waitlist) { - this.waitlistResourceSignal({ resource: payload.resource }); - } - }; + getSignalForClass(ResourceClass: T) { + const signalSet = getSignalSetByResourceName(ResourceClass.__internal_resourceName); + return signalSet?.computedSignal; + } - private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => { - if (payload.resource instanceof SignIn) { - this.signInFetchSignal({ status: payload.status }); + private onResourceError = (payload: { resource: BaseResource; error: unknown }) => { + const signalSet = getResourceSignalSet(payload.resource); + if (signalSet) { + signalSet.errorSignal({ error: payload.error }); } + }; - if (payload.resource instanceof SignUp) { - this.signUpFetchSignal({ status: payload.status }); + private onResourceUpdated = (payload: { resource: BaseResource }) => { + const signalSet = getResourceSignalSet(payload.resource); + if (signalSet) { + signalSet.resourceSignal({ resource: payload.resource }); } + }; - if (payload.resource instanceof Waitlist) { - this.waitlistFetchSignal({ status: payload.status }); + private onResourceFetch = (payload: { resource: BaseResource; status: 'idle' | 'fetching' }) => { + const signalSet = getResourceSignalSet(payload.resource); + if (signalSet) { + signalSet.fetchSignal({ status: payload.status }); } }; } diff --git a/packages/react/src/hooks/useClerkSignal.ts b/packages/react/src/hooks/useClerkSignal.ts index 617261f3932..e1442f0eaae 100644 --- a/packages/react/src/hooks/useClerkSignal.ts +++ b/packages/react/src/hooks/useClerkSignal.ts @@ -1,17 +1,27 @@ import type { SignInSignalValue, SignUpSignalValue, WaitlistSignalValue } from '@clerk/types'; -import { useCallback, useSyncExternalStore } from 'react'; +import { useCallback, useMemo, useSyncExternalStore } from 'react'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider'; +type SignalName = 'signIn' | 'signUp' | 'waitlist'; + function useClerkSignal(signal: 'signIn'): SignInSignalValue; function useClerkSignal(signal: 'signUp'): SignUpSignalValue; function useClerkSignal(signal: 'waitlist'): WaitlistSignalValue; -function useClerkSignal(signal: 'signIn' | 'signUp' | 'waitlist'): SignInSignalValue | SignUpSignalValue | WaitlistSignalValue { +function useClerkSignal(signal: SignalName): SignInSignalValue | SignUpSignalValue | WaitlistSignalValue { useAssertWrappedByClerkProvider('useClerkSignal'); const clerk = useIsomorphicClerkContext(); + const signalGetter = useMemo(() => { + const signalFn = clerk.__internal_state.getSignalForResourceName(signal); + if (!signalFn) { + throw new Error(`Signal not found for resource: ${signal}`); + } + return signalFn as () => SignInSignalValue | SignUpSignalValue | WaitlistSignalValue; + }, [clerk.__internal_state, signal]); + const subscribe = useCallback( (callback: () => void) => { if (!clerk.loaded) { @@ -19,36 +29,16 @@ function useClerkSignal(signal: 'signIn' | 'signUp' | 'waitlist'): SignInSignalV } return clerk.__internal_state.__internal_effect(() => { - switch (signal) { - case 'signIn': - clerk.__internal_state.signInSignal(); - break; - case 'signUp': - clerk.__internal_state.signUpSignal(); - break; - case 'waitlist': - clerk.__internal_state.waitlistSignal(); - break; - default: - throw new Error(`Unknown signal: ${signal}`); - } + signalGetter(); callback(); }); }, - [clerk, clerk.loaded, clerk.__internal_state], + [clerk, clerk.loaded, clerk.__internal_state, signalGetter], ); + const getSnapshot = useCallback(() => { - switch (signal) { - case 'signIn': - 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}`); - } - }, [clerk.__internal_state]); + return signalGetter(); + }, [signalGetter]); const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot); diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index e7b4113d9d4..3b1fe2bac33 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -21,12 +21,204 @@ const defaultErrors = (): Errors => ({ global: null, }); +interface PropertyConfig { + [key: string]: { key: keyof TTarget; defaultValue: any }; +} + +interface MethodConfig { + [key: string]: keyof TTarget & string; +} + +interface StructConfig { + [key: string]: { + getTarget: () => TTarget; + methods: readonly (keyof TTarget & string)[]; + getters?: readonly (keyof TTarget)[]; + fallbacks?: Partial; + }; +} + +interface ResourceProxyConfig { + resourceName: TResourceName; + target: () => TTarget; + resourceProperties?: PropertyConfig; + resourceMethods?: MethodConfig; + resourceStructs?: StructConfig; + resourceDefaults?: Partial; +} + export class StateProxy implements State { constructor(private isomorphicClerk: IsomorphicClerk) {} - private readonly signInSignalProxy = this.buildSignInProxy(); - private readonly signUpSignalProxy = this.buildSignUpProxy(); - private readonly waitlistSignalProxy = this.buildWaitlistProxy(); + private readonly signInSignalProxy = this.createResourceProxy({ + resourceName: 'signIn', + target: () => this.client.signIn.__internal_future, + resourceProperties: { + id: { key: 'id', defaultValue: undefined }, + supportedFirstFactors: { key: 'supportedFirstFactors', defaultValue: [] }, + supportedSecondFactors: { key: 'supportedSecondFactors', defaultValue: [] }, + secondFactorVerification: { + key: 'secondFactorVerification', + defaultValue: { + status: null, + error: null, + expireAt: null, + externalVerificationRedirectURL: null, + nonce: null, + attempts: null, + message: null, + strategy: null, + verifiedAtClient: null, + verifiedFromTheSameClient: () => false, + __internal_toSnapshot: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + pathRoot: '', + reload: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + }, + }, + identifier: { key: 'identifier', defaultValue: null }, + createdSessionId: { key: 'createdSessionId', defaultValue: null }, + userData: { key: 'userData', defaultValue: {} }, + firstFactorVerification: { + key: 'firstFactorVerification', + defaultValue: { + status: null, + error: null, + expireAt: null, + externalVerificationRedirectURL: null, + nonce: null, + attempts: null, + message: null, + strategy: null, + verifiedAtClient: null, + verifiedFromTheSameClient: () => false, + __internal_toSnapshot: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + pathRoot: '', + reload: () => { + throw new Error('__internal_toSnapshot called before Clerk is loaded'); + }, + }, + }, + }, + resourceMethods: { + create: 'create', + password: 'password', + sso: 'sso', + finalize: 'finalize', + ticket: 'ticket', + passkey: 'passkey', + web3: 'web3', + }, + resourceStructs: { + emailCode: { + getTarget: () => this.client.signIn.__internal_future.emailCode, + methods: ['sendCode', 'verifyCode'] as const, + }, + emailLink: { + getTarget: () => this.client.signIn.__internal_future.emailLink, + methods: ['sendLink', 'waitForVerification'] as const, + getters: ['verification'] as const, + fallbacks: { verification: null }, + }, + resetPasswordEmailCode: { + getTarget: () => this.client.signIn.__internal_future.resetPasswordEmailCode, + methods: ['sendCode', 'verifyCode', 'submitPassword'] as const, + }, + phoneCode: { + getTarget: () => this.client.signIn.__internal_future.phoneCode, + methods: ['sendCode', 'verifyCode'] as const, + }, + mfa: { + getTarget: () => this.client.signIn.__internal_future.mfa, + methods: ['sendPhoneCode', 'verifyPhoneCode', 'verifyTOTP', 'verifyBackupCode'] as const, + }, + }, + resourceDefaults: { + status: 'needs_identifier', + availableStrategies: [], + isTransferable: false, + }, + }); + + private readonly signUpSignalProxy = this.createResourceProxy({ + resourceName: 'signUp', + target: () => this.client.signUp.__internal_future, + resourceProperties: { + id: { key: 'id', defaultValue: undefined }, + requiredFields: { key: 'requiredFields', defaultValue: [] }, + optionalFields: { key: 'optionalFields', defaultValue: [] }, + missingFields: { key: 'missingFields', defaultValue: [] }, + username: { key: 'username', defaultValue: null }, + firstName: { key: 'firstName', defaultValue: null }, + lastName: { key: 'lastName', defaultValue: null }, + emailAddress: { key: 'emailAddress', defaultValue: null }, + phoneNumber: { key: 'phoneNumber', defaultValue: null }, + web3Wallet: { key: 'web3Wallet', defaultValue: null }, + hasPassword: { key: 'hasPassword', defaultValue: false }, + unsafeMetadata: { key: 'unsafeMetadata', defaultValue: {} }, + createdSessionId: { key: 'createdSessionId', defaultValue: null }, + createdUserId: { key: 'createdUserId', defaultValue: null }, + abandonAt: { key: 'abandonAt', defaultValue: null }, + legalAcceptedAt: { key: 'legalAcceptedAt', defaultValue: null }, + locale: { key: 'locale', defaultValue: null }, + status: { key: 'status', defaultValue: 'missing_requirements' }, + unverifiedFields: { key: 'unverifiedFields', defaultValue: [] }, + isTransferable: { key: 'isTransferable', defaultValue: false }, + }, + resourceMethods: { + create: 'create', + update: 'update', + sso: 'sso', + password: 'password', + ticket: 'ticket', + web3: 'web3', + finalize: 'finalize', + }, + resourceStructs: { + verifications: { + getTarget: () => this.client.signUp.__internal_future.verifications, + methods: ['sendEmailCode', 'verifyEmailCode', 'sendPhoneCode', 'verifyPhoneCode'] as const, + }, + }, + }); + + private readonly waitlistSignalProxy = this.createResourceProxy({ + resourceName: 'waitlist', + target: (): { id?: string; createdAt: Date | null; updatedAt: Date | null; join: (params: any) => Promise } => { + if (!inBrowser() || !this.isomorphicClerk.loaded) { + return { + id: undefined, + createdAt: null, + updatedAt: null, + join: () => Promise.resolve({ error: null }), + }; + } + const state = this.isomorphicClerk.__internal_state; + const waitlist = state.__internal_waitlist; + if (waitlist && '__internal_future' in waitlist) { + return (waitlist as { __internal_future: any }).__internal_future; + } + return { + id: undefined, + createdAt: null, + updatedAt: null, + join: () => Promise.resolve({ error: null }), + }; + }, + resourceProperties: { + id: { key: 'id', defaultValue: undefined }, + createdAt: { key: 'createdAt', defaultValue: null }, + updatedAt: { key: 'updatedAt', defaultValue: null }, + }, + resourceMethods: { + join: 'join', + }, + }); signInSignal() { return this.signInSignalProxy; @@ -45,236 +237,52 @@ export class StateProxy implements State { return this.isomorphicClerk.__internal_state.__internal_waitlist; } - private buildSignInProxy() { - const gateProperty = this.gateProperty.bind(this); - const target = () => this.client.signIn.__internal_future; - - return { - errors: defaultErrors(), - fetchStatus: 'idle' as const, - signIn: { - status: 'needs_identifier' as const, - availableStrategies: [], - isTransferable: false, - get id() { - return gateProperty(target, 'id', undefined); - }, - get supportedFirstFactors() { - return gateProperty(target, 'supportedFirstFactors', []); - }, - get supportedSecondFactors() { - return gateProperty(target, 'supportedSecondFactors', []); - }, - get secondFactorVerification() { - return gateProperty(target, 'secondFactorVerification', { - status: null, - error: null, - expireAt: null, - externalVerificationRedirectURL: null, - nonce: null, - attempts: null, - message: null, - strategy: null, - verifiedAtClient: null, - verifiedFromTheSameClient: () => false, - __internal_toSnapshot: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - pathRoot: '', - reload: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - }); - }, - get identifier() { - return gateProperty(target, 'identifier', null); - }, - get createdSessionId() { - return gateProperty(target, 'createdSessionId', null); - }, - get userData() { - return gateProperty(target, 'userData', {}); - }, - get firstFactorVerification() { - return gateProperty(target, 'firstFactorVerification', { - status: null, - error: null, - expireAt: null, - externalVerificationRedirectURL: null, - nonce: null, - attempts: null, - message: null, - strategy: null, - verifiedAtClient: null, - verifiedFromTheSameClient: () => false, - __internal_toSnapshot: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - pathRoot: '', - reload: () => { - throw new Error('__internal_toSnapshot called before Clerk is loaded'); - }, - }); - }, - - create: this.gateMethod(target, 'create'), - password: this.gateMethod(target, 'password'), - sso: this.gateMethod(target, 'sso'), - finalize: this.gateMethod(target, 'finalize'), - - emailCode: this.wrapMethods(() => target().emailCode, ['sendCode', 'verifyCode'] as const), - emailLink: this.wrapStruct( - () => target().emailLink, - ['sendLink', 'waitForVerification'] as const, - ['verification'] as const, - { verification: null }, - ), - resetPasswordEmailCode: this.wrapMethods(() => target().resetPasswordEmailCode, [ - 'sendCode', - 'verifyCode', - 'submitPassword', - ] as const), - phoneCode: this.wrapMethods(() => target().phoneCode, ['sendCode', 'verifyCode'] as const), - mfa: this.wrapMethods(() => target().mfa, [ - 'sendPhoneCode', - 'verifyPhoneCode', - 'verifyTOTP', - 'verifyBackupCode', - ] as const), - ticket: this.gateMethod(target, 'ticket'), - passkey: this.gateMethod(target, 'passkey'), - web3: this.gateMethod(target, 'web3'), - }, - }; - } - - private buildSignUpProxy() { + private createResourceProxy( + config: ResourceProxyConfig, + ): { + errors: Errors; + fetchStatus: 'idle'; + [K in TResourceName]: any; + } { const gateProperty = this.gateProperty.bind(this); const gateMethod = this.gateMethod.bind(this); const wrapMethods = this.wrapMethods.bind(this); - const target = () => this.client.signUp.__internal_future; + const wrapStruct = this.wrapStruct.bind(this); + const target = config.target; - return { - errors: defaultErrors(), - fetchStatus: 'idle' as const, - signUp: { - get id() { - return gateProperty(target, 'id', undefined); - }, - get requiredFields() { - return gateProperty(target, 'requiredFields', []); - }, - get optionalFields() { - return gateProperty(target, 'optionalFields', []); - }, - get missingFields() { - return gateProperty(target, 'missingFields', []); - }, - get username() { - return gateProperty(target, 'username', null); - }, - get firstName() { - return gateProperty(target, 'firstName', null); - }, - get lastName() { - return gateProperty(target, 'lastName', null); - }, - get emailAddress() { - return gateProperty(target, 'emailAddress', null); - }, - get phoneNumber() { - return gateProperty(target, 'phoneNumber', null); - }, - get web3Wallet() { - return gateProperty(target, 'web3Wallet', null); - }, - get hasPassword() { - return gateProperty(target, 'hasPassword', false); - }, - get unsafeMetadata() { - return gateProperty(target, 'unsafeMetadata', {}); - }, - get createdSessionId() { - return gateProperty(target, 'createdSessionId', null); - }, - get createdUserId() { - return gateProperty(target, 'createdUserId', null); - }, - get abandonAt() { - return gateProperty(target, 'abandonAt', null); - }, - get legalAcceptedAt() { - return gateProperty(target, 'legalAcceptedAt', null); - }, - get locale() { - return gateProperty(target, 'locale', null); - }, - get status() { - return gateProperty(target, 'status', 'missing_requirements'); - }, - get unverifiedFields() { - return gateProperty(target, 'unverifiedFields', []); - }, - get isTransferable() { - return gateProperty(target, 'isTransferable', false); - }, - - create: gateMethod(target, 'create'), - update: gateMethod(target, 'update'), - sso: gateMethod(target, 'sso'), - password: gateMethod(target, 'password'), - ticket: gateMethod(target, 'ticket'), - web3: gateMethod(target, 'web3'), - finalize: gateMethod(target, 'finalize'), + const resource: any = { ...config.resourceDefaults }; - verifications: wrapMethods(() => target().verifications, [ - 'sendEmailCode', - 'verifyEmailCode', - 'sendPhoneCode', - 'verifyPhoneCode', - ] as const), - }, - }; - } + if (config.resourceProperties) { + for (const [propName, { key, defaultValue }] of Object.entries(config.resourceProperties)) { + Object.defineProperty(resource, propName, { + get: () => gateProperty(target, key as keyof TTarget, defaultValue), + enumerable: true, + }); + } + } - private buildWaitlistProxy() { - const gateProperty = this.gateProperty.bind(this); - const gateMethod = this.gateMethod.bind(this); - const fallbackWaitlistFuture = { - id: undefined, - createdAt: null, - updatedAt: null, - join: () => Promise.resolve({ error: null }), - }; - const target = (): typeof fallbackWaitlistFuture => { - if (!inBrowser() || !this.isomorphicClerk.loaded) { - return fallbackWaitlistFuture; + if (config.resourceMethods) { + for (const [methodName, methodKey] of Object.entries(config.resourceMethods)) { + resource[methodName] = gateMethod(target, methodKey); } - const state = this.isomorphicClerk.__internal_state; - const waitlist = state.__internal_waitlist; - if (waitlist && '__internal_future' in waitlist) { - return (waitlist as { __internal_future: typeof fallbackWaitlistFuture }).__internal_future; + } + + if (config.resourceStructs) { + for (const [structName, structConfig] of Object.entries(config.resourceStructs)) { + resource[structName] = wrapStruct( + structConfig.getTarget, + structConfig.methods, + structConfig.getters || [], + structConfig.fallbacks || {}, + ); } - return fallbackWaitlistFuture; - }; + } return { errors: defaultErrors(), fetchStatus: 'idle' as const, - waitlist: { - get id() { - return gateProperty(target, 'id', undefined); - }, - get createdAt() { - return gateProperty(target, 'createdAt', null); - }, - get updatedAt() { - return gateProperty(target, 'updatedAt', null); - }, - - join: gateMethod(target, 'join'), - }, - }; + [config.resourceName]: resource, + } as any; } __internal_effect(_: () => void): () => void { diff --git a/packages/types/src/state.ts b/packages/types/src/state.ts index 457d2cef7aa..4163be004e6 100644 --- a/packages/types/src/state.ts +++ b/packages/types/src/state.ts @@ -84,10 +84,24 @@ export interface Errors { global: unknown[] | null; // does not include any errors that could be parsed as a field error } +type ResourceSignalValue = { + errors: Errors; + fetchStatus: 'idle' | 'fetching'; + [K in TSignalName]: TFutureResource; +}; + +type ResourceSignal = () => Omit< + ResourceSignalValue, + TSignalName +> & { + [K in TSignalName]: TFutureResource | null; +}; + /** * The value returned by the `useSignInSignal` hook. */ -export interface SignInSignalValue { +export interface SignInSignalValue + extends ResourceSignalValue { /** * Represents the errors that occurred during the last fetch of the parent resource. */ @@ -104,11 +118,12 @@ export interface SignInSignalValue { export type NullableSignInSignal = Omit & { signIn: SignInFutureResource | null; }; -export interface SignInSignal { - (): NullableSignInSignal; -} +export interface SignInSignal extends ResourceSignal {} -export interface SignUpSignalValue { +/** + * The value returned by the `useSignUpSignal` hook. + */ +export interface SignUpSignalValue extends ResourceSignalValue { /** * The errors that occurred during the last fetch of the underlying `SignUp` resource. */ @@ -125,11 +140,13 @@ export interface SignUpSignalValue { export type NullableSignUpSignal = Omit & { signUp: SignUpFutureResource | null; }; -export interface SignUpSignal { - (): NullableSignUpSignal; -} +export interface SignUpSignal extends ResourceSignal {} -export interface WaitlistSignalValue { +/** + * The value returned by the `useWaitlistSignal` hook. + */ +export interface WaitlistSignalValue + extends ResourceSignalValue { /** * The errors that occurred during the last fetch of the underlying `Waitlist` resource. */ @@ -146,9 +163,7 @@ export interface WaitlistSignalValue { export type NullableWaitlistSignal = Omit & { waitlist: WaitlistFutureResource | null; }; -export interface WaitlistSignal { - (): NullableWaitlistSignal; -} +export interface WaitlistSignal extends ResourceSignal {} export interface State { /**