From 39ac1eecce144bc41e8f5be7795768280ec22ad2 Mon Sep 17 00:00:00 2001 From: Daniel Moerner Date: Mon, 9 Mar 2026 21:25:40 -0400 Subject: [PATCH 1/6] fix(ui): Display attributes enabled for sign in --- .../components/UserProfile/AccountPage.tsx | 7 ++-- .../src/components/UserProfile/EmailForm.tsx | 3 +- .../__tests__/AccountPage.test.tsx | 34 +++++++++++++++++++ .../ui/src/components/UserProfile/utils.ts | 12 ++++++- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/UserProfile/AccountPage.tsx b/packages/ui/src/components/UserProfile/AccountPage.tsx index 01efd16cf33..d328612de49 100644 --- a/packages/ui/src/components/UserProfile/AccountPage.tsx +++ b/packages/ui/src/components/UserProfile/AccountPage.tsx @@ -12,6 +12,7 @@ import { EnterpriseAccountsSection } from './EnterpriseAccountsSection'; import { PhoneSection } from './PhoneSection'; import { UsernameSection } from './UsernameSection'; import { UserProfileSection } from './UserProfileSection'; +import { isAttributeAvailable } from './utils'; import { Web3Section } from './Web3Section'; export const AccountPage = withCardStateProvider(() => { @@ -20,9 +21,9 @@ export const AccountPage = withCardStateProvider(() => { const { user } = useUser(); const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext(); - const showUsername = attributes.username?.enabled; - const showEmail = attributes.email_address?.enabled; - const showPhone = attributes.phone_number?.enabled; + const showUsername = isAttributeAvailable(attributes.username); + const showEmail = isAttributeAvailable(attributes.email_address); + const showPhone = isAttributeAvailable(attributes.phone_number); const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0; const showEnterpriseAccounts = user && enterpriseSSO.enabled; const showWeb3 = attributes.web3_wallet?.enabled; diff --git a/packages/ui/src/components/UserProfile/EmailForm.tsx b/packages/ui/src/components/UserProfile/EmailForm.tsx index 6f7a0ee6d8c..671f35ca82e 100644 --- a/packages/ui/src/components/UserProfile/EmailForm.tsx +++ b/packages/ui/src/components/UserProfile/EmailForm.tsx @@ -18,6 +18,7 @@ import { useWizard, Wizard } from '../../common'; import { useEnvironment } from '../../contexts'; import type { LocalizationKey } from '../../localization'; import { localizationKeys } from '../../localization'; +import { isAttributeAvailable } from './utils'; import { VerifyWithCode } from './VerifyWithCode'; import { VerifyWithEnterpriseConnection } from './VerifyWithEnterpriseConnection'; import { VerifyWithLink } from './VerifyWithLink'; @@ -138,7 +139,7 @@ const getTranslationKeyByStrategy = (strategy: PrepareEmailAddressVerificationPa function isEmailLinksEnabledForInstance(env: EnvironmentResource): boolean { const { userSettings } = env; const { email_address } = userSettings.attributes; - return Boolean(email_address?.enabled && email_address?.verifications.includes('email_link')); + return Boolean(isAttributeAvailable(email_address) && email_address?.verifications.includes('email_link')); } /** diff --git a/packages/ui/src/components/UserProfile/__tests__/AccountPage.test.tsx b/packages/ui/src/components/UserProfile/__tests__/AccountPage.test.tsx index bdef0fbc4c9..b04bf15a390 100644 --- a/packages/ui/src/components/UserProfile/__tests__/AccountPage.test.tsx +++ b/packages/ui/src/components/UserProfile/__tests__/AccountPage.test.tsx @@ -68,6 +68,40 @@ describe('AccountPage', () => { expect(queryByText(/Enterprise Accounts/i)).not.toBeInTheDocument(); }); + it('shows sections for attributes disabled for sign-up but used for first factor', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress({ enabled: false, used_for_first_factor: true }); + f.withPhoneNumber({ enabled: false, used_for_first_factor: true }); + f.withUsername({ enabled: false, used_for_first_factor: true }); + f.withUser({ + first_name: 'George', + last_name: 'Clerk', + email_addresses: ['test@clerk.com'], + phone_numbers: ['+11234567890'], + username: 'georgeclerk', + }); + }); + + render(, { wrapper }); + screen.getByText(/Email addresses/i); + screen.getByText(/Phone numbers/i); + screen.getByText('georgeclerk'); + }); + + it('shows phone section when disabled for sign-up but used for second factor', async () => { + const { wrapper } = await createFixtures(f => { + f.withPhoneNumber({ enabled: false, used_for_first_factor: false, used_for_second_factor: true }); + f.withUser({ + first_name: 'George', + last_name: 'Clerk', + phone_numbers: ['+11234567890'], + }); + }); + + render(, { wrapper }); + screen.getByText(/Phone numbers/i); + }); + it('shows the connected accounts of the user', async () => { const { wrapper } = await createFixtures(f => { f.withSocialProvider({ provider: 'google' }); diff --git a/packages/ui/src/components/UserProfile/utils.ts b/packages/ui/src/components/UserProfile/utils.ts index b1565b08ed9..13ca67cce73 100644 --- a/packages/ui/src/components/UserProfile/utils.ts +++ b/packages/ui/src/components/UserProfile/utils.ts @@ -1,4 +1,14 @@ -import type { EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types'; +import type { AttributeData, EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types'; + +/** + * An attribute is "available" in the UserProfile if it's enabled for sign-up + * OR used as a first/second factor for sign-in. This covers instances where + * an attribute is disabled for sign-up but still used for authentication + * (e.g. accounts provisioned exclusively by invitation). + */ +export function isAttributeAvailable(attr: AttributeData | undefined): boolean { + return Boolean(attr?.enabled || attr?.used_for_first_factor || attr?.used_for_second_factor); +} type IDable = { id: string }; From a389214ae6e9729e65f5f1a1ed04e262827b0058 Mon Sep 17 00:00:00 2001 From: Daniel Moerner Date: Wed, 11 Mar 2026 13:08:07 -0400 Subject: [PATCH 2/6] chore: changeset --- .changeset/curvy-years-strive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curvy-years-strive.md diff --git a/.changeset/curvy-years-strive.md b/.changeset/curvy-years-strive.md new file mode 100644 index 00000000000..ff0089cb02b --- /dev/null +++ b/.changeset/curvy-years-strive.md @@ -0,0 +1,5 @@ +--- +'@clerk/ui': patch +--- + +UserProfile should show attributes enabled for sign in From abe963c5893025d368b7693cf1a05ee1ece157e9 Mon Sep 17 00:00:00 2001 From: Daniel Moerner Date: Wed, 11 Mar 2026 13:34:39 -0400 Subject: [PATCH 3/6] chore: Add unit tests for email and phone verification --- .../__tests__/EmailsSection.test.tsx | 36 ++++++++++++++++++ .../__tests__/PhoneSection.test.tsx | 38 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/UserProfile/__tests__/EmailsSection.test.tsx b/packages/ui/src/components/UserProfile/__tests__/EmailsSection.test.tsx index c98e79194f2..fd5d9ee4c49 100644 --- a/packages/ui/src/components/UserProfile/__tests__/EmailsSection.test.tsx +++ b/packages/ui/src/components/UserProfile/__tests__/EmailsSection.test.tsx @@ -95,6 +95,42 @@ describe('EmailSection', () => { }); }); + describe('Add email with attribute disabled for sign-up but used for sign-in', () => { + const disabledForSignUpConfig = createFixtures.config(f => { + f.withEmailAddress({ enabled: false, used_for_first_factor: true }); + f.withUser({ username: 'georgeclerk' }); + }); + + it('renders add email screen', async () => { + const { wrapper } = await createFixtures(disabledForSignUpConfig); + + const { getByRole, userEvent, getByLabelText, findByRole } = render(, { wrapper }); + await userEvent.click(getByRole('button', { name: 'Add email address' })); + await findByRole('heading', { name: /Add email address/i }); + getByLabelText(/email address/i); + }); + + it('can add an email and reach the verification screen', async () => { + const { wrapper, fixtures } = await createFixtures(disabledForSignUpConfig); + + const { getByRole, userEvent, getByLabelText, findByRole } = render(, { wrapper }); + await userEvent.click(getByRole('button', { name: 'Add email address' })); + await findByRole('heading', { name: /Add email address/i }); + + fixtures.clerk.user?.createEmailAddress.mockReturnValueOnce( + Promise.resolve({ + emailAddress: 'test+2@clerk.com', + prepareVerification: vi.fn().mockReturnValueOnce(Promise.resolve({} as any)), + } as any), + ); + + await userEvent.type(getByLabelText(/email address/i), 'test+2@clerk.com'); + await userEvent.click(getByRole('button', { name: /add$/i })); + expect(fixtures.clerk.user?.createEmailAddress).toHaveBeenCalledWith({ email: 'test+2@clerk.com' }); + await findByRole('heading', { name: /Verify email address/i }); + }); + }); + describe('Remove email', () => { it('Renders remove screen', async () => { const { wrapper } = await createFixtures(withEmails); diff --git a/packages/ui/src/components/UserProfile/__tests__/PhoneSection.test.tsx b/packages/ui/src/components/UserProfile/__tests__/PhoneSection.test.tsx index a2734dd0871..3b2776dc5a3 100644 --- a/packages/ui/src/components/UserProfile/__tests__/PhoneSection.test.tsx +++ b/packages/ui/src/components/UserProfile/__tests__/PhoneSection.test.tsx @@ -1,5 +1,5 @@ import { act } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; import { render, screen } from '@/test/utils'; @@ -335,5 +335,39 @@ describe('PhoneSection', () => { }); }); - it.todo('Test for verification of added phone number'); + describe('Add phone with attribute disabled for sign-up but used for sign-in', () => { + const disabledForSignUpConfig = createFixtures.config(f => { + f.withPhoneNumber({ enabled: false, used_for_first_factor: true }); + f.withUser({ email_addresses: ['test@clerk.com'] }); + }); + + it('renders add phone screen', async () => { + const { wrapper } = await createFixtures(disabledForSignUpConfig); + + const { getByRole, userEvent, getByLabelText, findByRole } = render(, { wrapper }); + await userEvent.click(getByRole('button', { name: 'Add phone number' })); + await findByRole('heading', { name: /Add phone number/i }); + getByLabelText(/phone number/i); + }); + + it('can add a phone and reach the verification screen', async () => { + const { wrapper, fixtures } = await createFixtures(disabledForSignUpConfig); + + const { getByRole, userEvent, getByLabelText, findByRole } = render(, { wrapper }); + await userEvent.click(getByRole('button', { name: 'Add phone number' })); + await findByRole('heading', { name: /Add phone number/i }); + + fixtures.clerk.user?.createPhoneNumber.mockReturnValueOnce( + Promise.resolve({ + phoneNumber: '+16911111111', + prepareVerification: vi.fn().mockReturnValueOnce(Promise.resolve({} as any)), + } as any), + ); + + await userEvent.type(getByLabelText(/phone number/i), '6911111111'); + await userEvent.click(getByRole('button', { name: /add$/i })); + expect(fixtures.clerk.user?.createPhoneNumber).toHaveBeenCalledWith({ phoneNumber: '+16911111111' }); + await findByRole('heading', { name: /Verify phone number/i }); + }); + }); }); From 8f1e53b8c771e2b26336a2b90aff44f5897c6ab5 Mon Sep 17 00:00:00 2001 From: Daniel Moerner Date: Mon, 20 Apr 2026 15:27:34 -0400 Subject: [PATCH 4/6] fix: Precalculate available attributes on usersettings object --- .../src/core/resources/UserSettings.ts | 14 +++++- .../resources/__tests__/UserSettings.test.ts | 50 +++++++++++++++++++ packages/shared/src/types/userSettings.ts | 1 + .../components/UserProfile/AccountPage.tsx | 10 ++-- .../src/components/UserProfile/EmailForm.tsx | 5 +- .../ui/src/components/UserProfile/utils.ts | 12 +---- 6 files changed, 71 insertions(+), 21 deletions(-) diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 48a8c85a426..ccc08e2d55e 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -162,6 +162,16 @@ export class UserSettings extends BaseResource implements UserSettingsResource { .sort(); } + get availableAttributes(): Array { + if (!this.attributes) { + return []; + } + + return Object.entries(this.attributes) + .filter(([, attr]) => attr.enabled || attr.used_for_first_factor || attr.used_for_second_factor) + .map(([name]) => name) as Array; + } + get web3FirstFactors(): Web3Strategy[] { if (!this.attributes) { return []; @@ -196,8 +206,8 @@ export class UserSettings extends BaseResource implements UserSettingsResource { get hasValidAuthFactor() { return Boolean( this.attributes?.email_address?.enabled || - this.attributes?.phone_number?.enabled || - (this.attributes.password?.required && this.attributes.username?.required), + this.attributes?.phone_number?.enabled || + (this.attributes.password?.required && this.attributes.username?.required), ); } diff --git a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts b/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts index e87df8e5028..4921a54cf61 100644 --- a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts @@ -275,6 +275,56 @@ describe('UserSettings', () => { expect(sut.authenticatableSocialStrategies).toEqual(['oauth_facebook']); }); + it('returns available attributes (enabled or used for first/second factor)', function () { + const sut = new UserSettings({ + attributes: { + email_address: { + enabled: true, + required: false, + used_for_first_factor: true, + first_factors: ['email_code'], + used_for_second_factor: false, + second_factors: [], + verifications: ['email_code'], + verify_at_sign_up: true, + }, + phone_number: { + enabled: false, + required: false, + used_for_first_factor: true, + first_factors: ['phone_code'], + used_for_second_factor: false, + second_factors: [], + verifications: ['phone_code'], + verify_at_sign_up: false, + }, + username: { + enabled: false, + required: false, + used_for_first_factor: false, + first_factors: [], + used_for_second_factor: false, + second_factors: [], + verifications: [], + verify_at_sign_up: false, + }, + password: { + enabled: false, + required: false, + used_for_first_factor: false, + first_factors: [], + used_for_second_factor: true, + second_factors: [], + verifications: [], + verify_at_sign_up: false, + }, + }, + } as any as UserSettingsJSON); + + const res = sut.availableAttributes; + expect(res).toEqual(['email_address', 'phone_number', 'password']); + }); + it('returns enabled standard form attributes', function () { const sut = new UserSettings({ attributes: { diff --git a/packages/shared/src/types/userSettings.ts b/packages/shared/src/types/userSettings.ts index f8424d1eba6..28fe740481b 100644 --- a/packages/shared/src/types/userSettings.ts +++ b/packages/shared/src/types/userSettings.ts @@ -141,6 +141,7 @@ export interface UserSettingsResource extends ClerkResource { web3FirstFactors: Web3Strategy[]; alternativePhoneCodeChannels: PhoneCodeChannel[]; enabledFirstFactorIdentifiers: Attribute[]; + availableAttributes: Attribute[]; instanceIsPasswordBased: boolean; hasValidAuthFactor: boolean; __internal_toSnapshot: () => UserSettingsJSONSnapshot; diff --git a/packages/ui/src/components/UserProfile/AccountPage.tsx b/packages/ui/src/components/UserProfile/AccountPage.tsx index d328612de49..3a97a01fb60 100644 --- a/packages/ui/src/components/UserProfile/AccountPage.tsx +++ b/packages/ui/src/components/UserProfile/AccountPage.tsx @@ -12,18 +12,16 @@ import { EnterpriseAccountsSection } from './EnterpriseAccountsSection'; import { PhoneSection } from './PhoneSection'; import { UsernameSection } from './UsernameSection'; import { UserProfileSection } from './UserProfileSection'; -import { isAttributeAvailable } from './utils'; import { Web3Section } from './Web3Section'; export const AccountPage = withCardStateProvider(() => { - const { attributes, social, enterpriseSSO } = useEnvironment().userSettings; + const { attributes, availableAttributes, social, enterpriseSSO } = useEnvironment().userSettings; const card = useCardState(); const { user } = useUser(); const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext(); - - const showUsername = isAttributeAvailable(attributes.username); - const showEmail = isAttributeAvailable(attributes.email_address); - const showPhone = isAttributeAvailable(attributes.phone_number); + const showUsername = availableAttributes.includes('username'); + const showEmail = availableAttributes.includes('email_address'); + const showPhone = availableAttributes.includes('phone_number'); const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0; const showEnterpriseAccounts = user && enterpriseSSO.enabled; const showWeb3 = attributes.web3_wallet?.enabled; diff --git a/packages/ui/src/components/UserProfile/EmailForm.tsx b/packages/ui/src/components/UserProfile/EmailForm.tsx index 671f35ca82e..ae1a984cbe4 100644 --- a/packages/ui/src/components/UserProfile/EmailForm.tsx +++ b/packages/ui/src/components/UserProfile/EmailForm.tsx @@ -18,7 +18,6 @@ import { useWizard, Wizard } from '../../common'; import { useEnvironment } from '../../contexts'; import type { LocalizationKey } from '../../localization'; import { localizationKeys } from '../../localization'; -import { isAttributeAvailable } from './utils'; import { VerifyWithCode } from './VerifyWithCode'; import { VerifyWithEnterpriseConnection } from './VerifyWithEnterpriseConnection'; import { VerifyWithLink } from './VerifyWithLink'; @@ -139,7 +138,9 @@ const getTranslationKeyByStrategy = (strategy: PrepareEmailAddressVerificationPa function isEmailLinksEnabledForInstance(env: EnvironmentResource): boolean { const { userSettings } = env; const { email_address } = userSettings.attributes; - return Boolean(isAttributeAvailable(email_address) && email_address?.verifications.includes('email_link')); + return Boolean( + userSettings.availableAttributes.includes('email_address') && email_address?.verifications.includes('email_link'), + ); } /** diff --git a/packages/ui/src/components/UserProfile/utils.ts b/packages/ui/src/components/UserProfile/utils.ts index 13ca67cce73..b1565b08ed9 100644 --- a/packages/ui/src/components/UserProfile/utils.ts +++ b/packages/ui/src/components/UserProfile/utils.ts @@ -1,14 +1,4 @@ -import type { AttributeData, EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types'; - -/** - * An attribute is "available" in the UserProfile if it's enabled for sign-up - * OR used as a first/second factor for sign-in. This covers instances where - * an attribute is disabled for sign-up but still used for authentication - * (e.g. accounts provisioned exclusively by invitation). - */ -export function isAttributeAvailable(attr: AttributeData | undefined): boolean { - return Boolean(attr?.enabled || attr?.used_for_first_factor || attr?.used_for_second_factor); -} +import type { EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types'; type IDable = { id: string }; From e848ffcb0966903d2baa3b9466b5658bf09bdf2b Mon Sep 17 00:00:00 2001 From: Daniel Moerner Date: Mon, 20 Apr 2026 17:04:23 -0400 Subject: [PATCH 5/6] prettier --- packages/clerk-js/src/core/resources/UserSettings.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index ccc08e2d55e..26c8a23a54d 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -206,8 +206,8 @@ export class UserSettings extends BaseResource implements UserSettingsResource { get hasValidAuthFactor() { return Boolean( this.attributes?.email_address?.enabled || - this.attributes?.phone_number?.enabled || - (this.attributes.password?.required && this.attributes.username?.required), + this.attributes?.phone_number?.enabled || + (this.attributes.password?.required && this.attributes.username?.required), ); } From af31b7f7867b1ca6b3d231f10361fcdca3fd994b Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 10 Jun 2026 21:38:05 -0500 Subject: [PATCH 6/6] fix(ui): compute attribute availability locally instead of a UserSettings getter Reverts 8f1e53b8c7 and e848ffcb09, restoring the original isAttributeAvailable helper in the UserProfile utils. ui.browser.js and clerk.browser.js are loaded independently (floating major tags, pinnable clerkJSVersion), so @clerk/ui can run against a clerk-js that does not have the availableAttributes getter; the unguarded .includes() calls would then crash every mounted Clerk component. The attribute data itself is FAPI-provided and safe to read across bundle versions. See SDK-115. --- .../src/core/resources/UserSettings.ts | 10 ---- .../resources/__tests__/UserSettings.test.ts | 50 ------------------- packages/shared/src/types/userSettings.ts | 1 - .../components/UserProfile/AccountPage.tsx | 10 ++-- .../src/components/UserProfile/EmailForm.tsx | 5 +- .../ui/src/components/UserProfile/utils.ts | 12 ++++- 6 files changed, 19 insertions(+), 69 deletions(-) diff --git a/packages/clerk-js/src/core/resources/UserSettings.ts b/packages/clerk-js/src/core/resources/UserSettings.ts index 0c8db442a3b..aaabb6738b6 100644 --- a/packages/clerk-js/src/core/resources/UserSettings.ts +++ b/packages/clerk-js/src/core/resources/UserSettings.ts @@ -163,16 +163,6 @@ export class UserSettings extends BaseResource implements UserSettingsResource { .sort(); } - get availableAttributes(): Array { - if (!this.attributes) { - return []; - } - - return Object.entries(this.attributes) - .filter(([, attr]) => attr.enabled || attr.used_for_first_factor || attr.used_for_second_factor) - .map(([name]) => name) as Array; - } - get web3FirstFactors(): Web3Strategy[] { if (!this.attributes) { return []; diff --git a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts b/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts index 4921a54cf61..e87df8e5028 100644 --- a/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/UserSettings.test.ts @@ -275,56 +275,6 @@ describe('UserSettings', () => { expect(sut.authenticatableSocialStrategies).toEqual(['oauth_facebook']); }); - it('returns available attributes (enabled or used for first/second factor)', function () { - const sut = new UserSettings({ - attributes: { - email_address: { - enabled: true, - required: false, - used_for_first_factor: true, - first_factors: ['email_code'], - used_for_second_factor: false, - second_factors: [], - verifications: ['email_code'], - verify_at_sign_up: true, - }, - phone_number: { - enabled: false, - required: false, - used_for_first_factor: true, - first_factors: ['phone_code'], - used_for_second_factor: false, - second_factors: [], - verifications: ['phone_code'], - verify_at_sign_up: false, - }, - username: { - enabled: false, - required: false, - used_for_first_factor: false, - first_factors: [], - used_for_second_factor: false, - second_factors: [], - verifications: [], - verify_at_sign_up: false, - }, - password: { - enabled: false, - required: false, - used_for_first_factor: false, - first_factors: [], - used_for_second_factor: true, - second_factors: [], - verifications: [], - verify_at_sign_up: false, - }, - }, - } as any as UserSettingsJSON); - - const res = sut.availableAttributes; - expect(res).toEqual(['email_address', 'phone_number', 'password']); - }); - it('returns enabled standard form attributes', function () { const sut = new UserSettings({ attributes: { diff --git a/packages/shared/src/types/userSettings.ts b/packages/shared/src/types/userSettings.ts index cbb1c73858d..dafa0190251 100644 --- a/packages/shared/src/types/userSettings.ts +++ b/packages/shared/src/types/userSettings.ts @@ -142,7 +142,6 @@ export interface UserSettingsResource extends ClerkResource { web3FirstFactors: Web3Strategy[]; alternativePhoneCodeChannels: PhoneCodeChannel[]; enabledFirstFactorIdentifiers: Attribute[]; - availableAttributes: Attribute[]; instanceIsPasswordBased: boolean; hasValidAuthFactor: boolean; __internal_toSnapshot: () => UserSettingsJSONSnapshot; diff --git a/packages/ui/src/components/UserProfile/AccountPage.tsx b/packages/ui/src/components/UserProfile/AccountPage.tsx index c073b12d668..5ad6ec0c94e 100644 --- a/packages/ui/src/components/UserProfile/AccountPage.tsx +++ b/packages/ui/src/components/UserProfile/AccountPage.tsx @@ -13,16 +13,18 @@ import { EnterpriseAccountsSection } from './EnterpriseAccountsSection'; import { PhoneSection } from './PhoneSection'; import { UsernameSection } from './UsernameSection'; import { UserProfileSection } from './UserProfileSection'; +import { isAttributeAvailable } from './utils'; import { Web3Section } from './Web3Section'; export const AccountPage = withCardStateProvider(() => { - const { attributes, availableAttributes, social, enterpriseSSO } = useEnvironment().userSettings; + const { attributes, social, enterpriseSSO } = useEnvironment().userSettings; const card = useCardState(); const { user } = useUser(); const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext(); - const showUsername = availableAttributes.includes('username'); - const showEmail = availableAttributes.includes('email_address'); - const showPhone = availableAttributes.includes('phone_number'); + + const showUsername = isAttributeAvailable(attributes.username); + const showEmail = isAttributeAvailable(attributes.email_address); + const showPhone = isAttributeAvailable(attributes.phone_number); const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0; const showEnterpriseAccounts = user && enterpriseSSO.enabled; const showWeb3 = attributes.web3_wallet?.enabled; diff --git a/packages/ui/src/components/UserProfile/EmailForm.tsx b/packages/ui/src/components/UserProfile/EmailForm.tsx index ae1a984cbe4..671f35ca82e 100644 --- a/packages/ui/src/components/UserProfile/EmailForm.tsx +++ b/packages/ui/src/components/UserProfile/EmailForm.tsx @@ -18,6 +18,7 @@ import { useWizard, Wizard } from '../../common'; import { useEnvironment } from '../../contexts'; import type { LocalizationKey } from '../../localization'; import { localizationKeys } from '../../localization'; +import { isAttributeAvailable } from './utils'; import { VerifyWithCode } from './VerifyWithCode'; import { VerifyWithEnterpriseConnection } from './VerifyWithEnterpriseConnection'; import { VerifyWithLink } from './VerifyWithLink'; @@ -138,9 +139,7 @@ const getTranslationKeyByStrategy = (strategy: PrepareEmailAddressVerificationPa function isEmailLinksEnabledForInstance(env: EnvironmentResource): boolean { const { userSettings } = env; const { email_address } = userSettings.attributes; - return Boolean( - userSettings.availableAttributes.includes('email_address') && email_address?.verifications.includes('email_link'), - ); + return Boolean(isAttributeAvailable(email_address) && email_address?.verifications.includes('email_link')); } /** diff --git a/packages/ui/src/components/UserProfile/utils.ts b/packages/ui/src/components/UserProfile/utils.ts index b1565b08ed9..13ca67cce73 100644 --- a/packages/ui/src/components/UserProfile/utils.ts +++ b/packages/ui/src/components/UserProfile/utils.ts @@ -1,4 +1,14 @@ -import type { EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types'; +import type { AttributeData, EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types'; + +/** + * An attribute is "available" in the UserProfile if it's enabled for sign-up + * OR used as a first/second factor for sign-in. This covers instances where + * an attribute is disabled for sign-up but still used for authentication + * (e.g. accounts provisioned exclusively by invitation). + */ +export function isAttributeAvailable(attr: AttributeData | undefined): boolean { + return Boolean(attr?.enabled || attr?.used_for_first_factor || attr?.used_for_second_factor); +} type IDable = { id: string };