diff --git a/.changeset/dark-moons-sort.md b/.changeset/dark-moons-sort.md new file mode 100644 index 00000000000..13fdcff2638 --- /dev/null +++ b/.changeset/dark-moons-sort.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Trigger a new request to submit the captcha token on sign up when executing the `signUp.create` method. \ No newline at end of file diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index bd7805ffc44..48f648f3423 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,9 +1,9 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "610kB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "70.16KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "70.25KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" }, - { "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" }, + { "path": "./dist/clerk.headless*.js", "maxSize": "53.2KB" }, { "path": "./dist/ui-common*.js", "maxSize": "108.4KB" }, { "path": "./dist/vendors*.js", "maxSize": "40.2KB" }, { "path": "./dist/coinbase*.js", "maxSize": "38KB" }, diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index 453a2bf90de..8a1a27fbe77 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -26,6 +26,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource branded: boolean = false; captchaHeartbeat: boolean = false; captchaHeartbeatIntervalMs?: number; + twoStepSignUpCreateEnabled: boolean = false; captchaOauthBypass: OAuthStrategy[] = ['oauth_google', 'oauth_microsoft', 'oauth_apple']; captchaProvider: CaptchaProvider = 'turnstile'; captchaPublicKey: string | null = null; @@ -80,6 +81,10 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource this.applicationName = this.withDefault(data.application_name, this.applicationName); this.branded = this.withDefault(data.branded, this.branded); this.captchaHeartbeat = this.withDefault(data.captcha_heartbeat, this.captchaHeartbeat); + this.twoStepSignUpCreateEnabled = this.withDefault( + data.two_step_sign_up_create_enabled, + this.twoStepSignUpCreateEnabled, + ); this.captchaHeartbeatIntervalMs = this.withDefault( data.captcha_heartbeat_interval_ms, this.captchaHeartbeatIntervalMs, @@ -130,6 +135,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource branded: this.branded, captcha_heartbeat_interval_ms: this.captchaHeartbeatIntervalMs, captcha_heartbeat: this.captchaHeartbeat, + two_step_sign_up_create_enabled: this.twoStepSignUpCreateEnabled, captcha_oauth_bypass: this.captchaOauthBypass, captcha_provider: this.captchaProvider, captcha_public_key_invisible: this.captchaPublicKeyInvisible, diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 78db8f0b78e..735d160905c 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -14,6 +14,7 @@ import type { PrepareVerificationParams, PrepareWeb3WalletVerificationParams, SignUpAuthenticateWithWeb3Params, + SignUpCreateOptions, SignUpCreateParams, SignUpField, SignUpIdentificationField, @@ -46,7 +47,7 @@ import { clerkVerifyEmailAddressCalledBeforeCreate, clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; -import { BaseResource, ClerkRuntimeError, SignUpVerifications } from './internal'; +import { BaseResource, SignUpVerifications } from './internal'; declare global { interface Window { @@ -83,16 +84,23 @@ export class SignUp extends BaseResource implements SignUpResource { this.fromJSON(data); } - create = async (_params: SignUpCreateParams): Promise => { + create = async (_params: SignUpCreateParams, options?: SignUpCreateOptions): Promise => { + if (SignUp.clerk.__unstable__environment?.displayConfig?.twoStepSignUpCreateEnabled) { + return this.twoStepCreate(_params, options); + } + + // This is the old flow and will be completely replaced by the two step flow when it's rolled out to everyone + return this.legacyCreate(_params); + }; + + private legacyCreate = async (_params: SignUpCreateParams): Promise => { let params: Record = _params; - if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) { - const captchaChallenge = new CaptchaChallenge(SignUp.clerk); - const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signup' }); - if (!captchaParams) { - throw new ClerkRuntimeError('', { code: 'captcha_unavailable' }); + if (!this.shouldBypassCaptchaForAttempt(params)) { + const captchaParams = await this.getCaptchaParams(); + if (captchaParams) { + params = { ...params, ...captchaParams }; } - params = { ...params, ...captchaParams }; } if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) { @@ -105,6 +113,30 @@ export class SignUp extends BaseResource implements SignUpResource { }); }; + private twoStepCreate = async ( + _params: SignUpCreateParams, + options?: SignUpCreateOptions, + ): Promise => { + const params: Record = _params; + + // This is a legacy flow, where we allowed specific OAuth providers to bypass the captcha + // This is no longer supported, but we need to keep it for backwards compatibility + if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) { + params.strategy = SignUp.clerk.client?.signIn.firstFactorVerification.strategy; + } + + await this._basePost({ + path: this.pathRoot, + body: normalizeUnsafeMetadata(params), + }); + + if (!this.shouldBypassCaptchaForAttempt(params) && !options?.skipChallenge) { + return this.solveChallenge(); + } + + return this; + }; + prepareVerification = (params: PrepareVerificationParams): Promise => { return this._basePost({ body: params, @@ -438,12 +470,33 @@ export class SignUp extends BaseResource implements SignUpResource { }; } + private solveChallenge = async (): Promise => { + const params = await this.getCaptchaParams(); + if (params) { + return this.update(params); + } + + return this; + }; + + private getCaptchaParams = async (): Promise | undefined> => { + let params: Record | undefined; + + if (!__BUILD_DISABLE_RHC__ && !this.clientBypass()) { + const captchaChallenge = new CaptchaChallenge(SignUp.clerk); + params = await captchaChallenge.managedOrInvisible({ action: 'signup' }); + } + + return params; + }; + private clientBypass() { return SignUp.clerk.client?.captchaBypass; } /** * We delegate bot detection to the following providers, instead of relying on turnstile exclusively + * This is a legacy flow, where we allowed specific OAuth providers to bypass the captcha */ protected shouldBypassCaptchaForAttempt(params: SignUpCreateParams) { if (!params.strategy) { diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 6ad232059b9..ca9fb4860df 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -215,8 +215,8 @@ function SignUpStartInternal(): JSX.Element { // TODO: This is a hack to reset the sign in attempt so that the oauth error // does not persist on full page reloads. - // We will revise this strategy as part of the Clerk DX epic. - void (await signUp.create({})); + // This will be handled by the backend (FAPI) in the future. + void (await signUp.create({}, { skipChallenge: true })); } } diff --git a/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts b/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts index 57459dc9aa8..aeb5c2d9d48 100644 --- a/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts +++ b/packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts @@ -56,15 +56,14 @@ export class CaptchaChallenge { if (e.captchaError) { return { captchaError: e.captchaError }; } - // if captcha action is signup, we return undefined, because we don't want to make the call to FAPI - return opts?.action === 'verify' ? { captchaError: e?.message || e || 'unexpected_captcha_error' } : undefined; + return { captchaError: e?.message || e || 'unexpected_captcha_error' }; }); - return opts?.action === 'verify' ? { ...captchaResult, captchaAction: 'verify' } : captchaResult; + return { ...captchaResult, captchaAction: opts?.action }; } - // if captcha action is signup, we return an empty object, because it means that the bot protection is disabled + // if captcha action is signup, we return undefined, because it means that the bot protection is disabled // and the user should be able to sign up without solving a captcha - return opts?.action === 'verify' ? { captchaError: 'captcha_unavailable', captchaAction: opts?.action } : {}; + return opts?.action === 'verify' ? { captchaError: 'captcha_unavailable', captchaAction: opts?.action } : undefined; } /** diff --git a/packages/types/src/displayConfig.ts b/packages/types/src/displayConfig.ts index 9f7c5b07ea5..c80d2b19482 100644 --- a/packages/types/src/displayConfig.ts +++ b/packages/types/src/displayConfig.ts @@ -24,6 +24,7 @@ export interface DisplayConfigJSON { captcha_oauth_bypass: OAuthStrategy[] | null; captcha_heartbeat?: boolean; captcha_heartbeat_interval_ms?: number; + two_step_sign_up_create_enabled?: boolean; home_url: string; instance_environment_type: string; logo_image_url: string; @@ -69,6 +70,7 @@ export interface DisplayConfigResource extends ClerkResource { captchaOauthBypass: OAuthStrategy[]; captchaHeartbeat: boolean; captchaHeartbeatIntervalMs?: number; + twoStepSignUpCreateEnabled?: boolean; homeUrl: string; instanceEnvironmentType: string; logoImageUrl: string; diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index d9485906aa3..fbbc43d4960 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -71,7 +71,7 @@ export interface SignUpResource extends ClerkResource { abandonAt: number | null; legalAcceptedAt: number | null; - create: (params: SignUpCreateParams) => Promise; + create: (params: SignUpCreateParams, options?: SignUpCreateOptions) => Promise; update: (params: SignUpUpdateParams) => Promise; @@ -199,6 +199,10 @@ export type SignUpCreateParams = Partial< } & Omit>, 'legalAccepted'> >; +export type SignUpCreateOptions = Partial<{ + skipChallenge: boolean; +}>; + export type SignUpUpdateParams = SignUpCreateParams; /**