diff --git a/apps/docs/personal-settings.mdx b/apps/docs/personal-settings.mdx index e153312ae9..fbafe7c836 100644 --- a/apps/docs/personal-settings.mdx +++ b/apps/docs/personal-settings.mdx @@ -16,6 +16,11 @@ identities, adjust the app, and manage profile details. Use **Profile** to update account details such as your name, email, avatar, and security settings. +Email editing requires account email verification to be enabled for the +deployment. Otherwise, the email field is read-only with an explanation; you +can still change your name. When email editing is available, the existing +verification flow must complete before the new address takes effect. + These settings affect your own Roomote account. They do not change deployment membership, environment access, or integration setup for the rest of the team. diff --git a/apps/web/src/components/settings/UserProfileSection.test.tsx b/apps/web/src/components/settings/UserProfileSection.test.tsx index 69e1d3c446..d086cfe322 100644 --- a/apps/web/src/components/settings/UserProfileSection.test.tsx +++ b/apps/web/src/components/settings/UserProfileSection.test.tsx @@ -139,6 +139,7 @@ describe('UserProfileSection', () => { it('updates name and email from the expanded profile form', async () => { render( { render( { }); expect(toast.error).toHaveBeenCalledWith('Email update failed'); }); + + it('keeps email read-only while allowing name changes without verification delivery', async () => { + render( + , + ); + fireEvent.click(screen.getByRole('button', { name: 'Edit' })); + expect(screen.getByLabelText('Email')).toHaveAttribute('readonly'); + expect(screen.getByLabelText('Email')).toHaveAccessibleDescription( + 'Email changes require email verification to be enabled for this deployment.', + ); + fireEvent.change(screen.getByLabelText('Name'), { + target: { value: 'New Name' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })); + await waitFor(() => + expect(updateUserMock).toHaveBeenCalledWith({ name: 'New Name' }), + ); + expect(changeEmailMock).not.toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith('Profile updated.'); + }); + + it('does not submit an email draft after email capability becomes unavailable', async () => { + const profile = { + email: 'ada@example.com', + imageUrl: '', + name: 'Ada User', + }; + const view = render( + , + ); + fireEvent.click(screen.getByRole('button', { name: 'Edit' })); + fireEvent.change(screen.getByLabelText('Email'), { + target: { value: 'new@example.com' }, + }); + view.rerender( + , + ); + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })); + expect(changeEmailMock).not.toHaveBeenCalled(); + expect(updateUserMock).not.toHaveBeenCalled(); + }); }); diff --git a/apps/web/src/components/settings/UserProfileSection.tsx b/apps/web/src/components/settings/UserProfileSection.tsx index b2b331a084..823c8ec2c9 100644 --- a/apps/web/src/components/settings/UserProfileSection.tsx +++ b/apps/web/src/components/settings/UserProfileSection.tsx @@ -35,9 +35,11 @@ function getAuthErrorMessage( } export function UserProfileSection({ + canChangeEmail = false, canChangePassword = false, profile, }: { + canChangeEmail?: boolean; canChangePassword?: boolean; profile: UserProfileSectionProfile; }) { @@ -53,7 +55,7 @@ export function UserProfileSection({ const trimmedName = name.trim(); const trimmedEmail = email.trim(); const hasNameChange = trimmedName !== profile.name; - const hasEmailChange = trimmedEmail !== profile.email; + const hasEmailChange = canChangeEmail && trimmedEmail !== profile.email; // Only show the Remove control for avatars uploaded through this app's // /api/avatars/ endpoint. OAuth-provider avatar URLs are not removable here // — the user would lose their provider-supplied picture with no way back. @@ -285,8 +287,8 @@ export function UserProfileSection({ ) : (

- Update your name or email. Your profile picture can be changed - from the profile view. + Update your name{canChangeEmail ? ' or email' : ''}. Your profile + picture can be changed from the profile view.

@@ -303,13 +305,26 @@ export function UserProfileSection({ setEmail(event.target.value)} disabled={isSubmitting} /> + {!canChangeEmail && ( +

+ Email changes require email verification to be enabled for + this deployment. +

+ )}