Skip to content
Open
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
6 changes: 6 additions & 0 deletions .changeset/khaki-colts-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/shared': minor
---

Add additional verification fields to SignUpFuture.
52 changes: 46 additions & 6 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
SignUpFutureSSOParams,
SignUpFutureTicketParams,
SignUpFutureUpdateParams,
SignUpFutureVerifications as SignUpFutureVerificationsType,
SignUpFutureWeb3Params,
SignUpIdentificationField,
SignUpJSON,
Expand Down Expand Up @@ -588,19 +589,58 @@ export class SignUp extends BaseResource implements SignUpResource {
};
}

type SignUpFutureVerificationsMethods = Pick<
SignUpFutureVerifications,
'sendEmailCode' | 'verifyEmailCode' | 'sendPhoneCode' | 'verifyPhoneCode'
>;

class SignUpFutureVerifications implements SignUpFutureVerificationsType {
#resource: SignUp;

sendEmailCode: SignUpFutureVerificationsType['sendEmailCode'];
verifyEmailCode: SignUpFutureVerificationsType['verifyEmailCode'];
sendPhoneCode: SignUpFutureVerificationsType['sendPhoneCode'];
verifyPhoneCode: SignUpFutureVerificationsType['verifyPhoneCode'];

constructor(resource: SignUp, methods: SignUpFutureVerificationsMethods) {
this.#resource = resource;
this.sendEmailCode = methods.sendEmailCode;
this.verifyEmailCode = methods.verifyEmailCode;
this.sendPhoneCode = methods.sendPhoneCode;
this.verifyPhoneCode = methods.verifyPhoneCode;
}

get emailAddress() {
return this.#resource.verifications.emailAddress;
}

get phoneNumber() {
return this.#resource.verifications.phoneNumber;
}

get web3Wallet() {
return this.#resource.verifications.web3Wallet;
}

get externalAccount() {
return this.#resource.verifications.externalAccount;
}
}

class SignUpFuture implements SignUpFutureResource {
verifications = {
sendEmailCode: this.sendEmailCode.bind(this),
verifyEmailCode: this.verifyEmailCode.bind(this),
sendPhoneCode: this.sendPhoneCode.bind(this),
verifyPhoneCode: this.verifyPhoneCode.bind(this),
};
verifications: SignUpFutureVerifications;

#canBeDiscarded = false;
readonly #resource: SignUp;

constructor(resource: SignUp) {
this.#resource = resource;
this.verifications = new SignUpFutureVerifications(this.#resource, {
sendEmailCode: this.sendEmailCode.bind(this),
verifyEmailCode: this.verifyEmailCode.bind(this),
sendPhoneCode: this.sendPhoneCode.bind(this),
verifyPhoneCode: this.verifyPhoneCode.bind(this),
});
}

get id() {
Expand Down
71 changes: 64 additions & 7 deletions packages/react/src/stateProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import type {
ForPayerType,
SignInErrors,
SignUpErrors,
SignUpVerificationResource,
State,
VerificationResource,
WaitlistErrors,
WaitlistResource,
} from '@clerk/shared/types';
Expand Down Expand Up @@ -48,6 +50,57 @@ const defaultWaitlistErrors = (): WaitlistErrors => ({
global: null,
});

const defaultVerificationResource = (): VerificationResource => ({
pathRoot: '',

attempts: null,
error: null,
expireAt: null,
externalVerificationRedirectURL: null,
nonce: null,
message: null,
status: null,
strategy: null,
verifiedAtClient: null,
verifiedFromTheSameClient() {
return false;
},
reload() {
throw new Error('reload() called before Clerk is loaded');
},
__internal_toSnapshot() {
return {
object: 'verification',
id: '',
attempts: null,
error: { code: '', message: '' },
expire_at: null,
externalVerificationRedirectURL: null,
nonce: null,
message: null,
status: null,
strategy: null,
verified_at_client: null,
};
},
});

const defaultSignUpVerificationResource = (): SignUpVerificationResource => ({
...defaultVerificationResource(),
supportedStrategies: [],
nextAction: '',
reload() {
throw new Error('reload() called before Clerk is loaded');
},
__internal_toSnapshot() {
return {
...defaultVerificationResource().__internal_toSnapshot(),
next_action: this.nextAction,
supported_strategies: this.supportedStrategies,
};
},
});

type CheckoutSignalProps = {
for?: ForPayerType;
planPeriod: BillingSubscriptionPlanPeriod;
Expand Down Expand Up @@ -191,7 +244,6 @@ export class StateProxy implements State {
private buildSignUpProxy() {
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;

return {
Expand Down Expand Up @@ -271,12 +323,17 @@ export class StateProxy implements State {
finalize: gateMethod(target, 'finalize'),
reset: gateMethod(target, 'reset'),

verifications: wrapMethods(() => target().verifications, [
'sendEmailCode',
'verifyEmailCode',
'sendPhoneCode',
'verifyPhoneCode',
] as const),
verifications: this.wrapStruct(
() => target().verifications,
['sendEmailCode', 'verifyEmailCode', 'sendPhoneCode', 'verifyPhoneCode'] as const,
['emailAddress', 'phoneNumber', 'web3Wallet', 'externalAccount'] as const,
{
emailAddress: defaultSignUpVerificationResource(),
phoneNumber: defaultSignUpVerificationResource(),
web3Wallet: defaultSignUpVerificationResource(),
externalAccount: defaultSignUpVerificationResource(),
},
),
Comment on lines +326 to +336
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add test coverage for new sign-up verification fields.

If this PR doesn’t include tests, please add coverage for the new signUp.verifications fields (emailAddress, phoneNumber, web3Wallet, externalAccount) and their fallback behavior to prevent regressions.

🤖 Prompt for AI Agents
In `@packages/react/src/stateProxy.ts` around lines 326 - 336, Add unit tests that
exercise the new sign-up verification fields defined by the verifications
wrapStruct (the properties: emailAddress, phoneNumber, web3Wallet,
externalAccount) and assert their fallback behavior to
defaultSignUpVerificationResource when values are missing; specifically, write
tests that instantiate the state proxy (or call wrapStruct/wrapped accessors) to
read and mutate each verification field, assert correct calls to
sendEmailCode/verifyEmailCode/sendPhoneCode/verifyPhoneCode through the wrapped
actions, and assert that when a specific verification entry is undefined the
code returns the defaultSignUpVerificationResource fallback.

},
};
}
Expand Down
72 changes: 49 additions & 23 deletions packages/shared/src/types/signUpFuture.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ClerkError } from '../errors/clerkError';
import type { SetActiveNavigate } from './clerk';
import type { PhoneCodeChannel } from './phoneCodeChannel';
import type { SignUpField, SignUpIdentificationField, SignUpStatus } from './signUpCommon';
import type { SignUpField, SignUpIdentificationField, SignUpStatus, SignUpVerificationResource } from './signUpCommon';
import type { Web3Strategy } from './strategies';
import type { VerificationResource } from './verification';

export interface SignUpFutureAdditionalParams {
/**
Expand Down Expand Up @@ -252,6 +253,51 @@ export interface SignUpFutureFinalizeParams {
navigate?: SetActiveNavigate;
}

/**
* An object that contains information about all available verification strategies.
*/
export interface SignUpFutureVerifications {
/**
* An object holding information about the email address verification.
*/
readonly emailAddress: SignUpVerificationResource;

/**
* An object holding information about the phone number verification.
*/
readonly phoneNumber: SignUpVerificationResource;

/**
* An object holding information about the Web3 wallet verification.
*/
readonly web3Wallet: VerificationResource;

/**
* An object holding information about the external account verification.
*/
readonly externalAccount: VerificationResource;

/**
* Used to send an email code to verify an email address.
*/
sendEmailCode: () => Promise<{ error: ClerkError | null }>;

/**
* Used to verify a code sent via email.
*/
verifyEmailCode: (params: SignUpFutureEmailCodeVerifyParams) => Promise<{ error: ClerkError | null }>;

/**
* Used to send a phone code to verify a phone number.
*/
sendPhoneCode: (params: SignUpFuturePhoneCodeSendParams) => Promise<{ error: ClerkError | null }>;

/**
* Used to verify a code sent via phone.
*/
verifyPhoneCode: (params: SignUpFuturePhoneCodeVerifyParams) => Promise<{ error: ClerkError | null }>;
}

/**
* The `SignUpFuture` class holds the state of the current sign-up attempt and provides methods to drive custom sign-up
* flows, including email/phone verification, password, SSO, ticket-based, and Web3-based account creation.
Expand Down Expand Up @@ -414,29 +460,9 @@ export interface SignUpFutureResource {
update: (params: SignUpFutureUpdateParams) => Promise<{ error: ClerkError | null }>;

/**
*
* An object that contains information about all available verification strategies.
*/
verifications: {
/**
* Used to send an email code to verify an email address.
*/
sendEmailCode: () => Promise<{ error: ClerkError | null }>;

/**
* Used to verify a code sent via email.
*/
verifyEmailCode: (params: SignUpFutureEmailCodeVerifyParams) => Promise<{ error: ClerkError | null }>;

/**
* Used to send a phone code to verify a phone number.
*/
sendPhoneCode: (params: SignUpFuturePhoneCodeSendParams) => Promise<{ error: ClerkError | null }>;

/**
* Used to verify a code sent via phone.
*/
verifyPhoneCode: (params: SignUpFuturePhoneCodeVerifyParams) => Promise<{ error: ClerkError | null }>;
};
verifications: SignUpFutureVerifications;

/**
* Used to sign up using an email address and password.
Expand Down
Loading