From 22c95db2351b70ada011c106a122688273cd14a4 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:02:44 -0500 Subject: [PATCH 1/3] feat(clerk-js,clerk-react,types): Signal phone code support --- .changeset/hungry-dogs-stick.md | 7 ++++ .../clerk-js/src/core/resources/SignIn.ts | 40 +++++++++++++++++++ .../clerk-js/src/core/resources/SignUp.ts | 34 ++++++++++++++++ packages/react/src/stateProxy.ts | 8 +++- packages/types/src/signIn.ts | 4 ++ packages/types/src/signUp.ts | 2 + 6 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .changeset/hungry-dogs-stick.md diff --git a/.changeset/hungry-dogs-stick.md b/.changeset/hungry-dogs-stick.md new file mode 100644 index 00000000000..0df08c2d08f --- /dev/null +++ b/.changeset/hungry-dogs-stick.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +[Experimental] Signal phone code support diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 5925e5c6399..ada791bc02a 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -495,6 +495,11 @@ class SignInFuture implements SignInFutureResource { submitPassword: this.submitResetPassword.bind(this), }; + phoneCode = { + sendCode: this.sendPhoneCode.bind(this), + verifyCode: this.verifyPhoneCode.bind(this), + }; + constructor(readonly resource: SignIn) {} get status() { @@ -621,6 +626,41 @@ class SignInFuture implements SignInFutureResource { }); } + async sendPhoneCode({ + phoneNumber, + channel = 'sms', + }: { + phoneNumber?: string; + channel?: 'sms' | 'whatsapp'; + } = {}): Promise<{ error: unknown }> { + return runAsyncResourceTask(this.resource, async () => { + if (!this.resource.id) { + await this.create({ identifier: phoneNumber }); + } + + const phoneCodeFactor = this.resource.supportedFirstFactors?.find(f => f.strategy === 'phone_code'); + + if (!phoneCodeFactor) { + throw new Error('Phone code factor not found'); + } + + const { phoneNumberId } = phoneCodeFactor; + await this.resource.__internal_basePost({ + body: { phoneNumberId, strategy: 'phone_code', channel }, + action: 'prepare_first_factor', + }); + }); + } + + async verifyPhoneCode({ code }: { code: string }): Promise<{ error: unknown }> { + return runAsyncResourceTask(this.resource, async () => { + await this.resource.__internal_basePost({ + body: { code, strategy: 'phone_code' }, + action: 'attempt_first_factor', + }); + }); + } + async sso({ flow = 'auto', strategy, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index d1db91e3ac4..13ca0aeab3a 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -476,6 +476,8 @@ 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), }; constructor(readonly resource: SignUp) {} @@ -570,6 +572,38 @@ class SignUpFuture implements SignUpFutureResource { }); } + async sendPhoneCode({ + phoneNumber, + channel = 'sms', + }: { + phoneNumber?: string; + channel?: 'sms' | 'whatsapp'; + } = {}): Promise<{ error: unknown }> { + return runAsyncResourceTask(this.resource, async () => { + if (!this.resource.id) { + const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(); + await this.resource.__internal_basePost({ + path: this.resource.pathRoot, + body: { phoneNumber, captchaToken, captchaWidgetType, captchaError }, + }); + } + + await this.resource.__internal_basePost({ + body: { strategy: 'phone_code', channel }, + action: 'prepare_verification', + }); + }); + } + + async verifyPhoneCode({ code }: { code: string }): Promise<{ error: unknown }> { + return runAsyncResourceTask(this.resource, async () => { + await this.resource.__internal_basePost({ + body: { strategy: 'phone_code', code }, + action: 'attempt_verification', + }); + }); + } + async sso({ strategy, redirectUrl, diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index 5ca649c6013..ca4a2ca8e1c 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -56,6 +56,7 @@ export class StateProxy implements State { 'verifyCode', 'submitPassword', ] as const), + phoneCode: this.wrapMethods(() => target().phoneCode, ['sendCode', 'verifyCode'] as const), }, }; } @@ -76,7 +77,12 @@ export class StateProxy implements State { password: this.gateMethod(target, 'password'), finalize: this.gateMethod(target, 'finalize'), - verifications: this.wrapMethods(() => target().verifications, ['sendEmailCode', 'verifyEmailCode'] as const), + verifications: this.wrapMethods(() => target().verifications, [ + 'sendEmailCode', + 'verifyEmailCode', + 'sendPhoneCode', + 'verifyPhoneCode', + ] as const), }, }; } diff --git a/packages/types/src/signIn.ts b/packages/types/src/signIn.ts index 241d015ffd9..de4907d0db0 100644 --- a/packages/types/src/signIn.ts +++ b/packages/types/src/signIn.ts @@ -153,6 +153,10 @@ export interface SignInFutureResource { verifyCode: (params: { code: string }) => Promise<{ error: unknown }>; submitPassword: (params: { password: string; signOutOfOtherSessions?: boolean }) => Promise<{ error: unknown }>; }; + phoneCode: { + sendCode: (params?: { phoneNumber?: string; channel?: 'sms' | 'whatsapp' }) => Promise<{ error: unknown }>; + verifyCode: (params: { code: string }) => Promise<{ error: unknown }>; + }; sso: (params: { flow?: 'auto' | 'modal'; strategy: OAuthStrategy | 'saml' | 'enterprise_sso'; diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index ae27c744b69..1e4e5c11d6d 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -133,6 +133,8 @@ export interface SignUpFutureResource { verifications: { sendEmailCode: () => Promise<{ error: unknown }>; verifyEmailCode: (params: { code: string }) => Promise<{ error: unknown }>; + sendPhoneCode: (params?: { phoneNumber?: string; channel?: 'sms' | 'whatsapp' }) => Promise<{ error: unknown }>; + verifyPhoneCode: (params: { code: string }) => Promise<{ error: unknown }>; }; password: (params: { emailAddress: string; password: string }) => Promise<{ error: unknown }>; sso: (params: { strategy: string; redirectUrl: string; redirectUrlComplete: string }) => Promise<{ error: unknown }>; From 6b9703829a9b45672409b37343ebb3776d81735f Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:26:12 -0500 Subject: [PATCH 2/3] refactor(clerk-js,types): Types for phone code methods --- packages/clerk-js/src/core/resources/SignIn.ts | 14 ++++++-------- packages/clerk-js/src/core/resources/SignUp.ts | 18 ++++++++---------- packages/types/src/factors.ts | 3 +-- packages/types/src/signInFuture.ts | 10 ++++++++++ packages/types/src/signUpFuture.ts | 16 ++++++++++++++-- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index cdff7746b61..c7a066494bc 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -32,6 +32,8 @@ import type { SignInFutureEmailCodeVerifyParams, SignInFutureFinalizeParams, SignInFuturePasswordParams, + SignInFuturePhoneCodeSendParams, + SignInFuturePhoneCodeVerifyParams, SignInFutureResetPasswordSubmitParams, SignInFutureResource, SignInFutureSSOParams, @@ -632,13 +634,8 @@ class SignInFuture implements SignInFutureResource { }); } - async sendPhoneCode({ - phoneNumber, - channel = 'sms', - }: { - phoneNumber?: string; - channel?: 'sms' | 'whatsapp'; - } = {}): Promise<{ error: unknown }> { + async sendPhoneCode(params: SignInFuturePhoneCodeSendParams): Promise<{ error: unknown }> { + const { phoneNumber, channel = 'sms' } = params; return runAsyncResourceTask(this.resource, async () => { if (!this.resource.id) { await this.create({ identifier: phoneNumber }); @@ -658,7 +655,8 @@ class SignInFuture implements SignInFutureResource { }); } - async verifyPhoneCode({ code }: { code: string }): Promise<{ error: unknown }> { + async verifyPhoneCode(params: SignInFuturePhoneCodeVerifyParams): Promise<{ error: unknown }> { + const { code } = params; return runAsyncResourceTask(this.resource, async () => { await this.resource.__internal_basePost({ body: { code, strategy: 'phone_code' }, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index fce85190a04..685f35e6c04 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -21,8 +21,10 @@ import type { SignUpFutureEmailCodeVerifyParams, SignUpFutureFinalizeParams, SignUpFuturePasswordParams, + SignUpFuturePhoneCodeSendParams, + SignUpFuturePhoneCodeVerifyParams, SignUpFutureResource, - SignUpFutureSSoParams, + SignUpFutureSSOParams, SignUpIdentificationField, SignUpJSON, SignUpJSONSnapshot, @@ -596,13 +598,8 @@ class SignUpFuture implements SignUpFutureResource { }); } - async sendPhoneCode({ - phoneNumber, - channel = 'sms', - }: { - phoneNumber?: string; - channel?: 'sms' | 'whatsapp'; - } = {}): Promise<{ error: unknown }> { + async sendPhoneCode(params: SignUpFuturePhoneCodeSendParams): Promise<{ error: unknown }> { + const { phoneNumber, channel = 'sms' } = params; return runAsyncResourceTask(this.resource, async () => { if (!this.resource.id) { const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(); @@ -619,7 +616,8 @@ class SignUpFuture implements SignUpFutureResource { }); } - async verifyPhoneCode({ code }: { code: string }): Promise<{ error: unknown }> { + async verifyPhoneCode(params: SignUpFuturePhoneCodeVerifyParams): Promise<{ error: unknown }> { + const { code } = params; return runAsyncResourceTask(this.resource, async () => { await this.resource.__internal_basePost({ body: { strategy: 'phone_code', code }, @@ -628,7 +626,7 @@ class SignUpFuture implements SignUpFutureResource { }); } - async sso(params: SignUpFutureSSoParams): Promise<{ error: unknown }> { + async sso(params: SignUpFutureSSOParams): Promise<{ error: unknown }> { const { strategy, redirectUrl, redirectCallbackUrl } = params; return runAsyncResourceTask(this.resource, async () => { const { captchaToken, captchaWidgetType, captchaError } = await this.getCaptchaToken(); diff --git a/packages/types/src/factors.ts b/packages/types/src/factors.ts index 7505beac359..38c1a4e0d12 100644 --- a/packages/types/src/factors.ts +++ b/packages/types/src/factors.ts @@ -1,6 +1,5 @@ -import type { PhoneCodeChannel } from 'phoneCodeChannel'; - import type { PublicKeyCredentialWithAuthenticatorAssertionResponse } from './passkey'; +import type { PhoneCodeChannel } from './phoneCodeChannel'; import type { BackupCodeStrategy, EmailCodeStrategy, diff --git a/packages/types/src/signInFuture.ts b/packages/types/src/signInFuture.ts index 345147f7e21..69a0f32fbb7 100644 --- a/packages/types/src/signInFuture.ts +++ b/packages/types/src/signInFuture.ts @@ -1,4 +1,5 @@ import type { SetActiveNavigate } from './clerk'; +import type { PhoneCodeChannel } from './phoneCodeChannel'; import type { SignInFirstFactor, SignInStatus } from './signInCommon'; import type { OAuthStrategy } from './strategies'; @@ -28,6 +29,15 @@ export interface SignInFutureResetPasswordSubmitParams { signOutOfOtherSessions?: boolean; } +export interface SignInFuturePhoneCodeSendParams { + phoneNumber?: string; + channel?: PhoneCodeChannel; +} + +export interface SignInFuturePhoneCodeVerifyParams { + code: string; +} + export interface SignInFutureSSOParams { flow?: 'auto' | 'modal'; strategy: OAuthStrategy | 'saml' | 'enterprise_sso'; diff --git a/packages/types/src/signUpFuture.ts b/packages/types/src/signUpFuture.ts index ccd5ab9e8ef..e06238ef172 100644 --- a/packages/types/src/signUpFuture.ts +++ b/packages/types/src/signUpFuture.ts @@ -1,4 +1,5 @@ import type { SetActiveNavigate } from './clerk'; +import type { PhoneCodeChannel } from './phoneCodeChannel'; import type { SignUpIdentificationField, SignUpStatus } from './signUpCommon'; export interface SignUpFutureCreateParams { @@ -14,7 +15,16 @@ export interface SignUpFuturePasswordParams { password: string; } -export interface SignUpFutureSSoParams { +export interface SignUpFuturePhoneCodeSendParams { + phoneNumber?: string; + channel?: PhoneCodeChannel; +} + +export interface SignUpFuturePhoneCodeVerifyParams { + code: string; +} + +export interface SignUpFutureSSOParams { strategy: string; /** * The URL to redirect to after the user has completed the SSO flow. @@ -39,8 +49,10 @@ export interface SignUpFutureResource { verifications: { sendEmailCode: () => Promise<{ error: unknown }>; verifyEmailCode: (params: SignUpFutureEmailCodeVerifyParams) => Promise<{ error: unknown }>; + sendPhoneCode: (params: SignUpFuturePhoneCodeSendParams) => Promise<{ error: unknown }>; + verifyPhoneCode: (params: SignUpFuturePhoneCodeVerifyParams) => Promise<{ error: unknown }>; }; password: (params: SignUpFuturePasswordParams) => Promise<{ error: unknown }>; - sso: (params: SignUpFutureSSoParams) => Promise<{ error: unknown }>; + sso: (params: SignUpFutureSSOParams) => Promise<{ error: unknown }>; finalize: (params?: SignUpFutureFinalizeParams) => Promise<{ error: unknown }>; } From b1ac39a7d64238472c5d4731c613c3e54f476aca Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:44:46 -0500 Subject: [PATCH 3/3] fix(types): Restore phoneCode to SignInFutureResource --- packages/types/src/signInFuture.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/types/src/signInFuture.ts b/packages/types/src/signInFuture.ts index 69a0f32fbb7..cc4310cc78e 100644 --- a/packages/types/src/signInFuture.ts +++ b/packages/types/src/signInFuture.ts @@ -66,6 +66,10 @@ export interface SignInFutureResource { sendCode: (params: SignInFutureEmailCodeSendParams) => Promise<{ error: unknown }>; verifyCode: (params: SignInFutureEmailCodeVerifyParams) => Promise<{ error: unknown }>; }; + phoneCode: { + sendCode: (params: SignInFuturePhoneCodeSendParams) => Promise<{ error: unknown }>; + verifyCode: (params: SignInFuturePhoneCodeVerifyParams) => Promise<{ error: unknown }>; + }; resetPasswordEmailCode: { sendCode: () => Promise<{ error: unknown }>; verifyCode: (params: SignInFutureEmailCodeVerifyParams) => Promise<{ error: unknown }>;