From b0cb1c93cf571bc0047807c53d8e6a1f05841bf3 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:55:11 -0500 Subject: [PATCH 1/2] feat(clerk-js,clerk-react,types): Add support for additional properties to Signal SignIn/SignUp --- .changeset/weak-deer-tie.md | 7 ++ .../clerk-js/src/core/resources/SignIn.ts | 26 +++++- .../clerk-js/src/core/resources/SignUp.ts | 64 ++++++++++++++ packages/react/src/stateProxy.ts | 87 +++++++++++++++++++ packages/types/src/signInFuture.ts | 34 +++++++- packages/types/src/signUpFuture.ts | 46 +++++++++- 6 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 .changeset/weak-deer-tie.md diff --git a/.changeset/weak-deer-tie.md b/.changeset/weak-deer-tie.md new file mode 100644 index 00000000000..bb45eaecdcf --- /dev/null +++ b/.changeset/weak-deer-tie.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +[Experimental] Add support for additional properties to Signal SignIn/SignUp diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 64e36feed89..4c5279a00ce 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -608,15 +608,35 @@ class SignInFuture implements SignInFutureResource { constructor(readonly resource: SignIn) {} + get id() { + return this.resource.id; + } + + get identifier() { + return this.resource.identifier; + } + + get createdSessionId() { + return this.resource.createdSessionId; + } + + get userData() { + return this.resource.userData; + } + get status() { // @TODO hooks-revamp: Consolidate this fallback val with stateProxy return this.resource.status || 'needs_identifier'; } - get availableStrategies() { + get supportedFirstFactors() { return this.resource.supportedFirstFactors ?? []; } + get supportedSecondFactors() { + return this.resource.supportedSecondFactors ?? []; + } + get isTransferable() { return this.resource.firstFactorVerification.status === 'transferable'; } @@ -637,6 +657,10 @@ class SignInFuture implements SignInFutureResource { return this.resource.firstFactorVerification; } + get secondFactorVerification() { + return this.resource.secondFactorVerification; + } + async sendResetPasswordEmailCode(): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { if (!this.resource.id) { diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 6305e81d84e..35023047767 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -551,11 +551,75 @@ class SignUpFuture implements SignUpFutureResource { constructor(readonly resource: SignUp) {} + get id() { + return this.resource.id; + } + + get requiredFields() { + return this.resource.requiredFields; + } + + get optionalFields() { + return this.resource.optionalFields; + } + + get missingFields() { + return this.resource.missingFields; + } + get status() { // @TODO hooks-revamp: Consolidate this fallback val with stateProxy return this.resource.status || 'missing_requirements'; } + get username() { + return this.resource.username; + } + + get firstName() { + return this.resource.firstName; + } + + get lastName() { + return this.resource.lastName; + } + + get emailAddress() { + return this.resource.emailAddress; + } + + get phoneNumber() { + return this.resource.phoneNumber; + } + + get web3Wallet() { + return this.resource.web3wallet; + } + + get hasPassword() { + return this.resource.hasPassword; + } + + get unsafeMetadata() { + return this.resource.unsafeMetadata; + } + + get createdSessionId() { + return this.resource.createdSessionId; + } + + get createdUserId() { + return this.resource.createdUserId; + } + + get abandonAt() { + return this.resource.abandonAt; + } + + get legalAcceptedAt() { + return this.resource.legalAcceptedAt; + } + get unverifiedFields() { return this.resource.unverifiedFields; } diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index eda6427c8d8..995c78dc647 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -45,6 +45,45 @@ export class StateProxy implements State { 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, @@ -107,6 +146,54 @@ export class StateProxy implements State { 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 status() { return gateProperty(target, 'status', 'missing_requirements'); }, diff --git a/packages/types/src/signInFuture.ts b/packages/types/src/signInFuture.ts index 0bb30cd2036..343e1320f8d 100644 --- a/packages/types/src/signInFuture.ts +++ b/packages/types/src/signInFuture.ts @@ -1,6 +1,6 @@ import type { SetActiveNavigate } from './clerk'; import type { PhoneCodeChannel } from './phoneCodeChannel'; -import type { SignInFirstFactor, SignInStatus } from './signInCommon'; +import type { SignInFirstFactor, SignInSecondFactor, SignInStatus, UserData } from './signInCommon'; import type { OAuthStrategy, Web3Strategy } from './strategies'; import type { VerificationResource } from './verification'; @@ -127,10 +127,20 @@ export interface SignInFutureFinalizeParams { * The current active `SignIn` instance, for use in custom flows. */ export interface SignInFutureResource { + /** + * The unique identifier for the current sign-in attempt. + */ + readonly id?: string; + /** * The list of first-factor strategies that are available for the current sign-in attempt. */ - readonly availableStrategies: SignInFirstFactor[]; + readonly supportedFirstFactors: SignInFirstFactor[]; + + /** + * The list of second-factor strategies that are available for the current sign-in attempt. + */ + readonly supportedSecondFactors: SignInSecondFactor[]; /** * The status of the current sign-in attempt as a string (for example, `'needs_identifier'`, `'needs_first_factor'`, @@ -148,6 +158,26 @@ export interface SignInFutureResource { readonly firstFactorVerification: VerificationResource; + /** + * The second-factor verification for the current sign-in attempt. + */ + readonly secondFactorVerification: VerificationResource; + + /** + * The identifier for the current sign-in attempt. + */ + readonly identifier: string | null; + + /** + * The created session ID for the current sign-in attempt. + */ + readonly createdSessionId: string | null; + + /** + * The user data for the current sign-in attempt. + */ + readonly userData: UserData; + /** * Used to supply an identifier for the sign-in attempt. Calling this method will populate data on the sign-in * attempt, such as `signIn.resource.supportedFirstFactors`. diff --git a/packages/types/src/signUpFuture.ts b/packages/types/src/signUpFuture.ts index 987519b153b..f33a0764ecb 100644 --- a/packages/types/src/signUpFuture.ts +++ b/packages/types/src/signUpFuture.ts @@ -1,6 +1,6 @@ import type { SetActiveNavigate } from './clerk'; import type { PhoneCodeChannel } from './phoneCodeChannel'; -import type { SignUpIdentificationField, SignUpStatus } from './signUpCommon'; +import type { SignUpField, SignUpIdentificationField, SignUpStatus } from './signUpCommon'; import type { Web3Strategy } from './strategies'; interface SignUpFutureAdditionalParams { @@ -71,11 +71,31 @@ export interface SignUpFutureFinalizeParams { * The current active `SignUp` instance, for use in custom flows. */ export interface SignUpFutureResource { + /** + * The unique identifier for the current sign-up attempt. + */ + readonly id?: string; + /** * The status of the current sign-up attempt as a string (for example, `'missing_requirements'`, `'complete'`, `'abandoned'`, etc.) */ readonly status: SignUpStatus; + /** + * The list of required fields for the current sign-up attempt. + */ + readonly requiredFields: SignUpField[]; + + /** + * The list of optional fields for the current sign-up attempt. + */ + readonly optionalFields: SignUpField[]; + + /** + * The list of missing fields for the current sign-up attempt. + */ + readonly missingFields: SignUpField[]; + /** * An array of strings representing unverified fields such as `’email_address’`. Can be used to detect when verification is necessary. */ @@ -89,6 +109,30 @@ export interface SignUpFutureResource { readonly existingSession?: { sessionId: string }; + readonly username: string | null; + + readonly firstName: string | null; + + readonly lastName: string | null; + + readonly emailAddress: string | null; + + readonly phoneNumber: string | null; + + readonly web3Wallet: string | null; + + readonly hasPassword: boolean; + + readonly unsafeMetadata: SignUpUnsafeMetadata; + + readonly createdSessionId: string | null; + + readonly createdUserId: string | null; + + readonly abandonAt: number | null; + + readonly legalAcceptedAt: number | null; + create: (params: SignUpFutureCreateParams) => Promise<{ error: unknown }>; update: (params: SignUpFutureUpdateParams) => Promise<{ error: unknown }>; From 70b0710e8324f8e801d52935e45e54f09f489710 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:14:26 -0500 Subject: [PATCH 2/2] fix(e2e): Update property name --- .../templates/custom-flows-react-vite/src/routes/SignIn.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx b/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx index 60f08d1734c..eb4ab0041f9 100644 --- a/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx +++ b/integration/templates/custom-flows-react-vite/src/routes/SignIn.tsx @@ -104,7 +104,7 @@ export function SignIn({ className, ...props }: React.ComponentProps<'div'>) {
- {signIn.availableStrategies + {signIn.supportedFirstFactors .filter(({ strategy }) => strategy !== 'reset_password_email_code') .map(({ strategy }) => (