From d789ddafe3fbd461a84f43aaa0118271d83a843c Mon Sep 17 00:00:00 2001 From: Alex Bratsos Date: Fri, 4 Jul 2025 18:12:38 +0200 Subject: [PATCH 1/3] fix(clerk-js): require email or phone when password field is visible --- packages/clerk-js/bundlewatch.config.json | 2 +- .../SignUp/__tests__/SignUpStart.spec.tsx | 75 ++- .../__tests__/signUpFormHelpers.test.ts | 427 +++++++++++++++++- .../ui/components/SignUp/signUpFormHelpers.ts | 129 +++++- 4 files changed, 626 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 3edd92caf01..3a0c7d1afee 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -15,7 +15,7 @@ { "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" }, { "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" }, { "path": "./dist/signin*.js", "maxSize": "14KB" }, - { "path": "./dist/signup*.js", "maxSize": "8.5KB" }, + { "path": "./dist/signup*.js", "maxSize": "8.86KB" }, { "path": "./dist/userbutton*.js", "maxSize": "5KB" }, { "path": "./dist/userprofile*.js", "maxSize": "16KB" }, { "path": "./dist/userverification*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.spec.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.spec.tsx index 2e4b61ad97b..04d92304e34 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.spec.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.spec.tsx @@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'; import { CardStateProvider } from '@/ui/elements/contexts'; -import { render, screen, waitFor } from '../../../../vitestUtils'; +import { fireEvent, render, screen, waitFor } from '../../../../vitestUtils'; import { OptionsProvider } from '../../../contexts'; import { AppearanceProvider } from '../../../customizables'; import { bindCreateFixtures } from '../../../utils/vitest/createFixtures'; @@ -88,14 +88,83 @@ describe('SignUpStart', () => { screen.getByText('Password'); }); - it('enables optional email', async () => { + it('should keep email optional when phone is primary with password', async () => { const { wrapper } = await createFixtures(f => { f.withEmailAddress({ required: false }); f.withPhoneNumber({ required: true }); f.withPassword({ required: true }); }); render(, { wrapper }); - expect(screen.getByText('Email address').nextElementSibling?.textContent).toBe('Optional'); + + const emailAddress = screen.getByLabelText('Email address', { selector: 'input' }); + expect(emailAddress.ariaRequired).toBe('false'); + expect(screen.getByText('Optional')).toBeInTheDocument(); + + const phoneInput = screen.getByLabelText('Phone number', { selector: 'input' }); + expect(phoneInput.ariaRequired).toBe('true'); + }); + + it('should require phone when password is required and phone is primary communication method', async () => { + const { wrapper } = await createFixtures(f => { + f.withPhoneNumber({ required: false, used_for_first_factor: true }); + f.withPassword({ required: true }); + }); + render(, { wrapper }); + + const phoneInput = screen.getByLabelText('Phone number', { selector: 'input' }); + expect(phoneInput.ariaRequired).toBe('true'); + + expect(screen.queryByLabelText('Email address', { selector: 'input' })).not.toBeInTheDocument(); + }); + + it('should require email when only email is enabled with password', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress({ required: true, used_for_first_factor: true }); + f.withPassword({ required: true }); + }); + + render(, { wrapper }); + + const emailAddress = screen.getByLabelText('Email address', { selector: 'input' }); + expect(emailAddress.ariaRequired).toBe('true'); + expect(screen.queryByText('Optional')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Phone number', { selector: 'input' })).not.toBeInTheDocument(); + }); + + it('should require email when password is required and email is primary communication method', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress({ required: false, used_for_first_factor: true }); + f.withPhoneNumber({ required: false, used_for_first_factor: false }); + f.withPassword({ required: true }); + }); + render(, { wrapper }); + + const emailAddress = screen.getByLabelText('Email address', { selector: 'input' }); + expect(emailAddress.ariaRequired).toBe('true'); + expect(screen.queryByText('Optional')).not.toBeInTheDocument(); + + expect(screen.queryByLabelText('Phone number', { selector: 'input' })).not.toBeInTheDocument(); + }); + + it('should require active field when toggling between email and phone with password', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress({ required: false, used_for_first_factor: true }); + f.withPhoneNumber({ required: false, used_for_first_factor: true }); + f.withPassword({ required: true }); + }); + render(, { wrapper }); + + const emailInput = screen.getByLabelText('Email address', { selector: 'input' }); + expect(emailInput.ariaRequired).toBe('true'); + expect(screen.queryByText('Optional')).not.toBeInTheDocument(); + + const usePhoneButton = screen.getByText(/use phone/i); + fireEvent.click(usePhoneButton); + + const phoneInput = screen.getByLabelText('Phone number', { selector: 'input' }); + expect(phoneInput.ariaRequired).toBe('true'); + + expect(screen.queryByLabelText('Email address', { selector: 'input' })).not.toBeInTheDocument(); }); it('enables optional phone number', async () => { diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts b/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts index 6063909f14b..24178735437 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts @@ -1,6 +1,10 @@ import type { Attribute } from '@clerk/types'; -import { determineActiveFields, getInitialActiveIdentifier } from '../signUpFormHelpers'; +import { + determineActiveFields, + determineRequiredCommunicationMethod, + getInitialActiveIdentifier, +} from '../signUpFormHelpers'; const createAttributeData = (name: Attribute, enabled: boolean, required: boolean, usedForFirstFactor: boolean) => ({ name, @@ -536,6 +540,427 @@ describe('determineActiveFields()', () => { expect(res).toEqual(result); }); + + describe('email or phone requirements with password', () => { + type Scenario = [string, any, any]; + const scenaria: Scenario[] = [ + [ + 'email optional, phone primary required with password', + { + ...mockDefaultAttributesProgressive, + email_address: { + enabled: true, + required: false, + used_for_first_factor: true, + }, + phone_number: { + enabled: true, + required: true, + used_for_first_factor: true, + }, + password: { + enabled: true, + required: true, + }, + }, + { + emailAddress: { + required: false, + disabled: false, + }, + phoneNumber: { + required: true, + }, + password: { + required: true, + }, + }, + ], + [ + 'phone optional, email required with password', + { + ...mockDefaultAttributesProgressive, + email_address: { + enabled: true, + required: true, + used_for_first_factor: true, + }, + phone_number: { + enabled: true, + required: false, + used_for_first_factor: true, + }, + password: { + enabled: true, + required: true, + }, + }, + { + emailAddress: { + required: true, + disabled: false, + }, + phoneNumber: { + required: false, + }, + password: { + required: true, + }, + }, + ], + [ + 'email and phone required with password', + { + ...mockDefaultAttributesProgressive, + email_address: { + enabled: true, + required: true, + used_for_first_factor: true, + }, + phone_number: { + enabled: true, + required: true, + used_for_first_factor: true, + }, + password: { + enabled: true, + required: true, + }, + }, + { + emailAddress: { + required: true, + disabled: false, + }, + phoneNumber: { + required: true, + }, + password: { + required: true, + }, + }, + ], + ]; + + it.each(scenaria)('%s', (___, attributes, result) => { + const actualResult = determineActiveFields({ + attributes: attributes, + activeCommIdentifierType: getInitialActiveIdentifier(attributes, isProgressiveSignUp), + isProgressiveSignUp, + }); + + expect(actualResult).toEqual(result); + }); + }); + + describe('password security: requires communication method', () => { + // When password is required but no communication method is explicitly required, + // we should automatically require one for password recovery + type Scenario = [string, any, any]; + const scenaria: Scenario[] = [ + [ + 'password required, both email and phone optional and neither primary - email takes precedence', + { + ...mockDefaultAttributesProgressive, + email_address: { + enabled: true, + required: false, + used_for_first_factor: false, + }, + phone_number: { + enabled: true, + required: false, + used_for_first_factor: false, + }, + password: { + enabled: true, + required: true, + }, + }, + { + emailAddress: { + required: true, + disabled: false, + }, + password: { + required: true, + }, + }, + ], + [ + 'password required, only email available - email becomes required', + { + ...mockDefaultAttributesProgressive, + email_address: { + enabled: true, + required: false, + used_for_first_factor: false, + }, + phone_number: { + enabled: false, + required: false, + used_for_first_factor: false, + }, + password: { + enabled: true, + required: true, + }, + }, + { + emailAddress: { + required: true, + disabled: false, + }, + password: { + required: true, + }, + }, + ], + [ + 'password required, only phone available - phone becomes required', + { + ...mockDefaultAttributesProgressive, + email_address: { + enabled: false, + required: false, + used_for_first_factor: false, + }, + phone_number: { + enabled: true, + required: false, + used_for_first_factor: false, + }, + password: { + enabled: true, + required: true, + }, + }, + { + phoneNumber: { + required: true, + }, + password: { + required: true, + }, + }, + ], + ]; + + it.each(scenaria)('%s', (___, attributes, result) => { + const actualResult = determineActiveFields({ + attributes: attributes, + activeCommIdentifierType: getInitialActiveIdentifier(attributes, isProgressiveSignUp), + isProgressiveSignUp, + }); + + expect(actualResult).toEqual(result); + }); + }); + + describe('password security: requirement follows active field in email OR phone scenarios', () => { + // When both email and phone are optional but password is required, + // the currently active field should become required + it('email required when active in email OR phone scenario with password', () => { + const attributes = { + email_address: createAttributeData('email_address', true, false, true), + phone_number: createAttributeData('phone_number', true, false, true), + password: createAttributeData('password', true, true, false), + first_name: createAttributeData('first_name', false, false, false), + last_name: createAttributeData('last_name', false, false, false), + username: createAttributeData('username', false, false, false), + }; + + const result = determineActiveFields({ + attributes: attributes, + activeCommIdentifierType: 'emailAddress', // Email is currently active + isProgressiveSignUp, + }); + + expect(result).toEqual({ + emailAddress: { + required: true, + disabled: false, + }, + password: { + required: true, + }, + }); + }); + + it('phone required when active in email OR phone scenario with password', () => { + const attributes = { + email_address: createAttributeData('email_address', true, false, true), + phone_number: createAttributeData('phone_number', true, false, true), + password: createAttributeData('password', true, true, false), + first_name: createAttributeData('first_name', false, false, false), + last_name: createAttributeData('last_name', false, false, false), + username: createAttributeData('username', false, false, false), + }; + + const result = determineActiveFields({ + attributes: attributes, + activeCommIdentifierType: 'phoneNumber', // Phone is currently active + isProgressiveSignUp, + }); + + expect(result).toEqual({ + phoneNumber: { + required: true, + }, + password: { + required: true, + }, + }); + }); + }); + }); +}); + +describe('determineRequiredCommunicationMethod', () => { + const createMinimalAttributes = (overrides: any = {}) => ({ + email_address: createAttributeData('email_address', false, false, false), + phone_number: createAttributeData('phone_number', false, false, false), + username: createAttributeData('username', false, false, false), + password: createAttributeData('password', false, false, false), + ...overrides, + }); + + describe('when password is NOT required', () => { + it('mirrors server settings for all fields', () => { + const attributes = createMinimalAttributes({ + password: createAttributeData('password', true, false, false), // password not required + email_address: createAttributeData('email_address', true, true, true), + phone_number: createAttributeData('phone_number', true, false, false), + username: createAttributeData('username', true, true, false), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: true, + phoneShouldBeRequired: false, + usernameShouldBeRequired: true, + }); + }); + }); + + describe('when password IS required', () => { + const requiredPassword = { password: createAttributeData('password', true, true, false) }; + + it('mirrors server settings if a communication method is already required', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + email_address: createAttributeData('email_address', true, true, true), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: true, + phoneShouldBeRequired: false, + usernameShouldBeRequired: false, + }); + }); + + it('requires nothing if no communication methods are enabled', () => { + const attributes = createMinimalAttributes({ ...requiredPassword }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: false, + phoneShouldBeRequired: false, + usernameShouldBeRequired: false, + }); + }); + + it('requires email if it is the only enabled communication method', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + email_address: createAttributeData('email_address', true, false, false), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: true, + phoneShouldBeRequired: false, + usernameShouldBeRequired: false, + }); + }); + + it('requires phone if it is the only enabled communication method', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + phone_number: createAttributeData('phone_number', true, false, false), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: false, + phoneShouldBeRequired: true, + usernameShouldBeRequired: false, + }); + }); + + it('requires username if it is the only enabled communication method and a first factor', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + username: createAttributeData('username', true, false, true), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: false, + phoneShouldBeRequired: false, + usernameShouldBeRequired: true, + }); + }); + + it('defaults to requiring both email and phone if both email and phone are enabled but not required', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + email_address: createAttributeData('email_address', true, false, false), + phone_number: createAttributeData('phone_number', true, false, false), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: true, + phoneShouldBeRequired: true, + usernameShouldBeRequired: false, + }); + }); + + it('requires email by default if no other communication methods are required by instance settings', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + username: createAttributeData('username', true, false, false), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: true, + phoneShouldBeRequired: false, + usernameShouldBeRequired: false, + }); + }); + + it('requires email when username is a non-required first factor', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + email_address: createAttributeData('email_address', true, false, false), + username: createAttributeData('username', true, false, true), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: true, + phoneShouldBeRequired: false, + usernameShouldBeRequired: false, + }); + }); + + it('requires phone when username is a non-required first factor and email is disabled', () => { + const attributes = createMinimalAttributes({ + ...requiredPassword, + phone_number: createAttributeData('phone_number', true, false, false), + username: createAttributeData('username', true, false, true), + }); + const result = determineRequiredCommunicationMethod(attributes); + expect(result).toEqual({ + emailShouldBeRequired: false, + phoneShouldBeRequired: true, + usernameShouldBeRequired: false, + }); + }); }); }); diff --git a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts index 5922509a3f2..70c6b4f6db8 100644 --- a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts +++ b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts @@ -192,8 +192,10 @@ function getEmailAddressField({ return; } + const { emailShouldBeRequired } = determineRequiredCommunicationMethod(attributes); + return { - required: Boolean(attributes.email_address?.required), + required: emailShouldBeRequired, disabled: !!hasTicket && !!hasEmail, }; } @@ -234,8 +236,10 @@ function getPhoneNumberField({ return; } + const { phoneShouldBeRequired } = determineRequiredCommunicationMethod(attributes); + return { - required: Boolean(attributes.phone_number?.required), + required: phoneShouldBeRequired, }; } @@ -300,3 +304,124 @@ function getGenericField(fieldKey: FieldKey, attributes: Partial): F required: attributes[attrKey]?.required, }; } + +type Outcome = 'email' | 'phone' | 'username' | 'mirrorServer' | 'none'; + +type SignUpAttributeField = { + enabled: boolean; + required: boolean; + firstFactor: boolean; +}; + +type Context = { + passwordRequired: boolean; + email: SignUpAttributeField; + phone: SignUpAttributeField; + username: SignUpAttributeField; +}; + +const outcomePredicates: Record boolean)[]> = { + mirrorServer: [ + // If password is not required, then field requirements are determined by the server. + ctx => !ctx.passwordRequired, + // If any of the communication methods are already required by the server, then we don't need to do anything. + ctx => ctx.email.required || ctx.phone.required || (ctx.username.required && ctx.username.firstFactor), + ], + none: [ + // If none of the communication methods are enabled, then none can be required. + ctx => !ctx.email.enabled && !ctx.phone.enabled && !ctx.username.enabled, + ], + email: [ + // If email is the only enabled communication method, it should be required. + ctx => ctx.email.enabled && !ctx.phone.enabled && !ctx.username.enabled, + // If email is enabled but not required, and phone is enabled and not required, then email should be required. + ctx => ctx.email.enabled && !ctx.email.required && ctx.phone.enabled && !ctx.phone.required, + // If username is a first factor but not required, email can be used as an alternative. + ctx => ctx.username.firstFactor && !ctx.username.required && ctx.email.enabled && !ctx.email.required, + // If username is required but not a first factor, and both email and phone are enabled, then email is a valid identifier. + ctx => ctx.username.required && !ctx.username.firstFactor && ctx.email.enabled && ctx.phone.enabled, + ], + phone: [ + ctx => ctx.phone.enabled && !ctx.email.required && !ctx.phone.required, + // If username is a first factor but not required, phone can be used as an alternative. + ctx => ctx.username.firstFactor && !ctx.username.required && ctx.phone.enabled && !ctx.phone.required, + // If phone is the only first factor, it should be required. + ctx => ctx.phone.firstFactor && !ctx.email.firstFactor && !ctx.username.firstFactor, + // If username is required but not a first factor, and both email and phone are enabled, then phone is a valid identifier. + ctx => ctx.username.required && !ctx.username.firstFactor && ctx.phone.enabled && ctx.email.enabled, + // If email is not enabled, but phone and username are, phone should be available. + ctx => !ctx.email.enabled && ctx.phone.enabled && ctx.username.enabled, + ], + username: [ + // If username is the only first factor, it should be required. + ctx => ctx.username.enabled && ctx.username.firstFactor && !ctx.email.enabled && !ctx.phone.enabled, + // If username is required but not a first factor, and both email and phone are enabled, it should be required. + ctx => ctx.username.required && !ctx.username.firstFactor && ctx.email.enabled && ctx.phone.enabled, + ], +}; + +/** + * When password is required, we need to ensure at least one identifier + * (email, phone, or username) is also required + */ +export function determineRequiredCommunicationMethod(attributes: Partial): { + emailShouldBeRequired: boolean; + phoneShouldBeRequired: boolean; + usernameShouldBeRequired: boolean; +} { + const ctx = { + passwordRequired: Boolean(attributes.password?.enabled && attributes.password.required), + email: { + enabled: Boolean(attributes.email_address?.enabled), + required: Boolean(attributes.email_address?.required), + firstFactor: Boolean(attributes.email_address?.used_for_first_factor), + }, + phone: { + enabled: Boolean(attributes.phone_number?.enabled), + required: Boolean(attributes.phone_number?.required), + firstFactor: Boolean(attributes.phone_number?.used_for_first_factor), + }, + username: { + enabled: Boolean(attributes.username?.enabled), + required: Boolean(attributes.username?.required), + firstFactor: Boolean(attributes.username?.used_for_first_factor), + }, + }; + + const outcomeMet = (outcome: Outcome) => outcomePredicates[outcome].some(predicate => predicate(ctx)); + + if (outcomeMet('mirrorServer')) { + return { + emailShouldBeRequired: ctx.email.required, + phoneShouldBeRequired: ctx.phone.required, + usernameShouldBeRequired: ctx.username.required, + }; + } + + if (outcomeMet('none')) { + return { + emailShouldBeRequired: false, + phoneShouldBeRequired: false, + usernameShouldBeRequired: false, + }; + } + + const emailShouldBeRequired = outcomeMet('email'); + const phoneShouldBeRequired = outcomeMet('phone'); + const usernameShouldBeRequired = outcomeMet('username'); + + // If password is required and no communication method is enabled, then email is the default. + if (ctx.passwordRequired && !emailShouldBeRequired && !phoneShouldBeRequired && !usernameShouldBeRequired) { + return { + emailShouldBeRequired: true, + phoneShouldBeRequired: false, + usernameShouldBeRequired: false, + }; + } + + return { + emailShouldBeRequired, + phoneShouldBeRequired, + usernameShouldBeRequired, + }; +} From a378587032192889d31188620a0790996aa5cdea Mon Sep 17 00:00:00 2001 From: Alex Bratsos Date: Fri, 4 Jul 2025 18:31:28 +0200 Subject: [PATCH 2/3] chore(clerk-js): add changeset --- .changeset/fair-olives-wish.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fair-olives-wish.md diff --git a/.changeset/fair-olives-wish.md b/.changeset/fair-olives-wish.md new file mode 100644 index 00000000000..197e89efe5e --- /dev/null +++ b/.changeset/fair-olives-wish.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Bugfix: Fixed incorrect field validation when using password authentication with email or phone number during sign-up. Optional email and phone fields now correctly display their requirement status. From 5ee7b6ce81bf3e5d9fd10bbd16500f6342a9be08 Mon Sep 17 00:00:00 2001 From: Alex Bratsos Date: Thu, 10 Jul 2025 10:56:19 +0200 Subject: [PATCH 3/3] fixup! fix(clerk-js): require email or phone when password field is visible --- .../__tests__/signUpFormHelpers.test.ts | 28 ++++++++----------- .../ui/components/SignUp/signUpFormHelpers.ts | 14 +++++----- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts b/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts index 24178735437..bb55e2d8f15 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/signUpFormHelpers.test.ts @@ -1,10 +1,6 @@ import type { Attribute } from '@clerk/types'; -import { - determineActiveFields, - determineRequiredCommunicationMethod, - getInitialActiveIdentifier, -} from '../signUpFormHelpers'; +import { determineActiveFields, determineRequiredIdentifier, getInitialActiveIdentifier } from '../signUpFormHelpers'; const createAttributeData = (name: Attribute, enabled: boolean, required: boolean, usedForFirstFactor: boolean) => ({ name, @@ -816,7 +812,7 @@ describe('determineActiveFields()', () => { }); }); -describe('determineRequiredCommunicationMethod', () => { +describe('determineRequiredIdentifier', () => { const createMinimalAttributes = (overrides: any = {}) => ({ email_address: createAttributeData('email_address', false, false, false), phone_number: createAttributeData('phone_number', false, false, false), @@ -833,7 +829,7 @@ describe('determineRequiredCommunicationMethod', () => { phone_number: createAttributeData('phone_number', true, false, false), username: createAttributeData('username', true, true, false), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: true, phoneShouldBeRequired: false, @@ -850,7 +846,7 @@ describe('determineRequiredCommunicationMethod', () => { ...requiredPassword, email_address: createAttributeData('email_address', true, true, true), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: true, phoneShouldBeRequired: false, @@ -860,7 +856,7 @@ describe('determineRequiredCommunicationMethod', () => { it('requires nothing if no communication methods are enabled', () => { const attributes = createMinimalAttributes({ ...requiredPassword }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: false, phoneShouldBeRequired: false, @@ -873,7 +869,7 @@ describe('determineRequiredCommunicationMethod', () => { ...requiredPassword, email_address: createAttributeData('email_address', true, false, false), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: true, phoneShouldBeRequired: false, @@ -886,7 +882,7 @@ describe('determineRequiredCommunicationMethod', () => { ...requiredPassword, phone_number: createAttributeData('phone_number', true, false, false), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: false, phoneShouldBeRequired: true, @@ -899,7 +895,7 @@ describe('determineRequiredCommunicationMethod', () => { ...requiredPassword, username: createAttributeData('username', true, false, true), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: false, phoneShouldBeRequired: false, @@ -913,7 +909,7 @@ describe('determineRequiredCommunicationMethod', () => { email_address: createAttributeData('email_address', true, false, false), phone_number: createAttributeData('phone_number', true, false, false), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: true, phoneShouldBeRequired: true, @@ -926,7 +922,7 @@ describe('determineRequiredCommunicationMethod', () => { ...requiredPassword, username: createAttributeData('username', true, false, false), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: true, phoneShouldBeRequired: false, @@ -940,7 +936,7 @@ describe('determineRequiredCommunicationMethod', () => { email_address: createAttributeData('email_address', true, false, false), username: createAttributeData('username', true, false, true), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: true, phoneShouldBeRequired: false, @@ -954,7 +950,7 @@ describe('determineRequiredCommunicationMethod', () => { phone_number: createAttributeData('phone_number', true, false, false), username: createAttributeData('username', true, false, true), }); - const result = determineRequiredCommunicationMethod(attributes); + const result = determineRequiredIdentifier(attributes); expect(result).toEqual({ emailShouldBeRequired: false, phoneShouldBeRequired: true, diff --git a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts index 70c6b4f6db8..dba5807f168 100644 --- a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts +++ b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts @@ -192,7 +192,7 @@ function getEmailAddressField({ return; } - const { emailShouldBeRequired } = determineRequiredCommunicationMethod(attributes); + const { emailShouldBeRequired } = determineRequiredIdentifier(attributes); return { required: emailShouldBeRequired, @@ -236,7 +236,7 @@ function getPhoneNumberField({ return; } - const { phoneShouldBeRequired } = determineRequiredCommunicationMethod(attributes); + const { phoneShouldBeRequired } = determineRequiredIdentifier(attributes); return { required: phoneShouldBeRequired, @@ -324,15 +324,15 @@ const outcomePredicates: Record boolean)[]> = { mirrorServer: [ // If password is not required, then field requirements are determined by the server. ctx => !ctx.passwordRequired, - // If any of the communication methods are already required by the server, then we don't need to do anything. + // If any of the identifiers are already required by the server, then we don't need to do anything. ctx => ctx.email.required || ctx.phone.required || (ctx.username.required && ctx.username.firstFactor), ], none: [ - // If none of the communication methods are enabled, then none can be required. + // If none of the identifiers are enabled, then none can be required. ctx => !ctx.email.enabled && !ctx.phone.enabled && !ctx.username.enabled, ], email: [ - // If email is the only enabled communication method, it should be required. + // If email is the only enabled identifier, it should be required. ctx => ctx.email.enabled && !ctx.phone.enabled && !ctx.username.enabled, // If email is enabled but not required, and phone is enabled and not required, then email should be required. ctx => ctx.email.enabled && !ctx.email.required && ctx.phone.enabled && !ctx.phone.required, @@ -364,7 +364,7 @@ const outcomePredicates: Record boolean)[]> = { * When password is required, we need to ensure at least one identifier * (email, phone, or username) is also required */ -export function determineRequiredCommunicationMethod(attributes: Partial): { +export function determineRequiredIdentifier(attributes: Partial): { emailShouldBeRequired: boolean; phoneShouldBeRequired: boolean; usernameShouldBeRequired: boolean; @@ -410,7 +410,7 @@ export function determineRequiredCommunicationMethod(attributes: Partial