Skip to content

Commit 3942bba

Browse files
dmoernerjacekradko
andauthored
fix(ui): UserProfile should show attributes enabled for sign in (#8042)
Co-authored-by: Jacek Radko <jacek@clerk.dev>
1 parent 82aec41 commit 3942bba

7 files changed

Lines changed: 128 additions & 7 deletions

File tree

.changeset/curvy-years-strive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/ui': patch
3+
---
4+
5+
UserProfile should show attributes enabled for sign in

packages/ui/src/components/UserProfile/AccountPage.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { EnterpriseAccountsSection } from './EnterpriseAccountsSection';
1313
import { PhoneSection } from './PhoneSection';
1414
import { UsernameSection } from './UsernameSection';
1515
import { UserProfileSection } from './UserProfileSection';
16+
import { isAttributeAvailable } from './utils';
1617
import { Web3Section } from './Web3Section';
1718

1819
export const AccountPage = withCardStateProvider(() => {
@@ -21,9 +22,9 @@ export const AccountPage = withCardStateProvider(() => {
2122
const { user } = useUser();
2223
const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext();
2324

24-
const showUsername = attributes.username?.enabled;
25-
const showEmail = attributes.email_address?.enabled;
26-
const showPhone = attributes.phone_number?.enabled;
25+
const showUsername = isAttributeAvailable(attributes.username);
26+
const showEmail = isAttributeAvailable(attributes.email_address);
27+
const showPhone = isAttributeAvailable(attributes.phone_number);
2728
const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0;
2829
const showEnterpriseAccounts = user && enterpriseSSO.enabled;
2930
const showWeb3 = attributes.web3_wallet?.enabled;

packages/ui/src/components/UserProfile/EmailForm.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { useWizard, Wizard } from '../../common';
1818
import { useEnvironment } from '../../contexts';
1919
import type { LocalizationKey } from '../../localization';
2020
import { localizationKeys } from '../../localization';
21+
import { isAttributeAvailable } from './utils';
2122
import { VerifyWithCode } from './VerifyWithCode';
2223
import { VerifyWithEnterpriseConnection } from './VerifyWithEnterpriseConnection';
2324
import { VerifyWithLink } from './VerifyWithLink';
@@ -138,7 +139,7 @@ const getTranslationKeyByStrategy = (strategy: PrepareEmailAddressVerificationPa
138139
function isEmailLinksEnabledForInstance(env: EnvironmentResource): boolean {
139140
const { userSettings } = env;
140141
const { email_address } = userSettings.attributes;
141-
return Boolean(email_address?.enabled && email_address?.verifications.includes('email_link'));
142+
return Boolean(isAttributeAvailable(email_address) && email_address?.verifications.includes('email_link'));
142143
}
143144

144145
/**

packages/ui/src/components/UserProfile/__tests__/AccountPage.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,40 @@ describe('AccountPage', () => {
6868
expect(queryByText(/Enterprise Accounts/i)).not.toBeInTheDocument();
6969
});
7070

71+
it('shows sections for attributes disabled for sign-up but used for first factor', async () => {
72+
const { wrapper } = await createFixtures(f => {
73+
f.withEmailAddress({ enabled: false, used_for_first_factor: true });
74+
f.withPhoneNumber({ enabled: false, used_for_first_factor: true });
75+
f.withUsername({ enabled: false, used_for_first_factor: true });
76+
f.withUser({
77+
first_name: 'George',
78+
last_name: 'Clerk',
79+
email_addresses: ['test@clerk.com'],
80+
phone_numbers: ['+11234567890'],
81+
username: 'georgeclerk',
82+
});
83+
});
84+
85+
render(<AccountPage />, { wrapper });
86+
screen.getByText(/Email addresses/i);
87+
screen.getByText(/Phone numbers/i);
88+
screen.getByText('georgeclerk');
89+
});
90+
91+
it('shows phone section when disabled for sign-up but used for second factor', async () => {
92+
const { wrapper } = await createFixtures(f => {
93+
f.withPhoneNumber({ enabled: false, used_for_first_factor: false, used_for_second_factor: true });
94+
f.withUser({
95+
first_name: 'George',
96+
last_name: 'Clerk',
97+
phone_numbers: ['+11234567890'],
98+
});
99+
});
100+
101+
render(<AccountPage />, { wrapper });
102+
screen.getByText(/Phone numbers/i);
103+
});
104+
71105
it('shows the connected accounts of the user', async () => {
72106
const { wrapper } = await createFixtures(f => {
73107
f.withSocialProvider({ provider: 'google' });

packages/ui/src/components/UserProfile/__tests__/EmailsSection.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,42 @@ describe('EmailSection', () => {
9595
});
9696
});
9797

98+
describe('Add email with attribute disabled for sign-up but used for sign-in', () => {
99+
const disabledForSignUpConfig = createFixtures.config(f => {
100+
f.withEmailAddress({ enabled: false, used_for_first_factor: true });
101+
f.withUser({ username: 'georgeclerk' });
102+
});
103+
104+
it('renders add email screen', async () => {
105+
const { wrapper } = await createFixtures(disabledForSignUpConfig);
106+
107+
const { getByRole, userEvent, getByLabelText, findByRole } = render(<EmailsSection />, { wrapper });
108+
await userEvent.click(getByRole('button', { name: 'Add email address' }));
109+
await findByRole('heading', { name: /Add email address/i });
110+
getByLabelText(/email address/i);
111+
});
112+
113+
it('can add an email and reach the verification screen', async () => {
114+
const { wrapper, fixtures } = await createFixtures(disabledForSignUpConfig);
115+
116+
const { getByRole, userEvent, getByLabelText, findByRole } = render(<EmailsSection />, { wrapper });
117+
await userEvent.click(getByRole('button', { name: 'Add email address' }));
118+
await findByRole('heading', { name: /Add email address/i });
119+
120+
fixtures.clerk.user?.createEmailAddress.mockReturnValueOnce(
121+
Promise.resolve({
122+
emailAddress: 'test+2@clerk.com',
123+
prepareVerification: vi.fn().mockReturnValueOnce(Promise.resolve({} as any)),
124+
} as any),
125+
);
126+
127+
await userEvent.type(getByLabelText(/email address/i), 'test+2@clerk.com');
128+
await userEvent.click(getByRole('button', { name: /add$/i }));
129+
expect(fixtures.clerk.user?.createEmailAddress).toHaveBeenCalledWith({ email: 'test+2@clerk.com' });
130+
await findByRole('heading', { name: /Verify email address/i });
131+
});
132+
});
133+
98134
describe('Remove email', () => {
99135
it('Renders remove screen', async () => {
100136
const { wrapper } = await createFixtures(withEmails);

packages/ui/src/components/UserProfile/__tests__/PhoneSection.test.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { act } from '@testing-library/react';
2-
import { describe, expect, it } from 'vitest';
2+
import { describe, expect, it, vi } from 'vitest';
33

44
import { bindCreateFixtures } from '@/test/create-fixtures';
55
import { render, screen } from '@/test/utils';
@@ -335,5 +335,39 @@ describe('PhoneSection', () => {
335335
});
336336
});
337337

338-
it.todo('Test for verification of added phone number');
338+
describe('Add phone with attribute disabled for sign-up but used for sign-in', () => {
339+
const disabledForSignUpConfig = createFixtures.config(f => {
340+
f.withPhoneNumber({ enabled: false, used_for_first_factor: true });
341+
f.withUser({ email_addresses: ['test@clerk.com'] });
342+
});
343+
344+
it('renders add phone screen', async () => {
345+
const { wrapper } = await createFixtures(disabledForSignUpConfig);
346+
347+
const { getByRole, userEvent, getByLabelText, findByRole } = render(<PhoneSection />, { wrapper });
348+
await userEvent.click(getByRole('button', { name: 'Add phone number' }));
349+
await findByRole('heading', { name: /Add phone number/i });
350+
getByLabelText(/phone number/i);
351+
});
352+
353+
it('can add a phone and reach the verification screen', async () => {
354+
const { wrapper, fixtures } = await createFixtures(disabledForSignUpConfig);
355+
356+
const { getByRole, userEvent, getByLabelText, findByRole } = render(<PhoneSection />, { wrapper });
357+
await userEvent.click(getByRole('button', { name: 'Add phone number' }));
358+
await findByRole('heading', { name: /Add phone number/i });
359+
360+
fixtures.clerk.user?.createPhoneNumber.mockReturnValueOnce(
361+
Promise.resolve({
362+
phoneNumber: '+16911111111',
363+
prepareVerification: vi.fn().mockReturnValueOnce(Promise.resolve({} as any)),
364+
} as any),
365+
);
366+
367+
await userEvent.type(getByLabelText(/phone number/i), '6911111111');
368+
await userEvent.click(getByRole('button', { name: /add$/i }));
369+
expect(fixtures.clerk.user?.createPhoneNumber).toHaveBeenCalledWith({ phoneNumber: '+16911111111' });
370+
await findByRole('heading', { name: /Verify phone number/i });
371+
});
372+
});
339373
});

packages/ui/src/components/UserProfile/utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
import type { EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types';
1+
import type { AttributeData, EmailAddressResource, PhoneNumberResource, Web3WalletResource } from '@clerk/shared/types';
2+
3+
/**
4+
* An attribute is "available" in the UserProfile if it's enabled for sign-up
5+
* OR used as a first/second factor for sign-in. This covers instances where
6+
* an attribute is disabled for sign-up but still used for authentication
7+
* (e.g. accounts provisioned exclusively by invitation).
8+
*/
9+
export function isAttributeAvailable(attr: AttributeData | undefined): boolean {
10+
return Boolean(attr?.enabled || attr?.used_for_first_factor || attr?.used_for_second_factor);
11+
}
212

313
type IDable = { id: string };
414

0 commit comments

Comments
 (0)