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 329e38c8de8..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, @@ -508,6 +510,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() { @@ -627,6 +634,37 @@ class SignInFuture implements SignInFutureResource { }); } + 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 }); + } + + 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(params: SignInFuturePhoneCodeVerifyParams): Promise<{ error: unknown }> { + const { code } = params; + return runAsyncResourceTask(this.resource, async () => { + await this.resource.__internal_basePost({ + body: { code, strategy: 'phone_code' }, + action: 'attempt_first_factor', + }); + }); + } + async sso(params: SignInFutureSSOParams): Promise<{ error: unknown }> { const { flow = 'auto', strategy, redirectUrl, redirectCallbackUrl } = params; return runAsyncResourceTask(this.resource, async () => { diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 2317dfb902a..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, @@ -497,6 +499,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) {} @@ -594,7 +598,35 @@ class SignUpFuture implements SignUpFutureResource { }); } - async sso(params: SignUpFutureSSoParams): 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(); + 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(params: SignUpFuturePhoneCodeVerifyParams): Promise<{ error: unknown }> { + const { code } = params; + return runAsyncResourceTask(this.resource, async () => { + await this.resource.__internal_basePost({ + body: { strategy: 'phone_code', code }, + action: 'attempt_verification', + }); + }); + } + + 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/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/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..cc4310cc78e 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'; @@ -56,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 }>; 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 }>; }