Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/user-profile-edit-password.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
8 changes: 8 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,12 @@ import {
meta as userProfilePasskeysSectionMeta,
} from '../stories/user-profile-passkeys-section.stories';
import {
ActiveEnterpriseAccount as UserProfilePasswordSectionActiveEnterpriseAccount,
Default as UserProfilePasswordSectionDefault,
EditPasswordFails as UserProfilePasswordSectionEditPasswordFails,
meta as userProfilePasswordSectionMeta,
SetPassword as UserProfilePasswordSectionSetPassword,
WithoutCurrentPassword as UserProfilePasswordSectionWithoutCurrentPassword,
} from '../stories/user-profile-password-section.stories';
import {
Default as UserProfilePaymentMethodsSectionDefault,
Expand Down Expand Up @@ -488,6 +492,10 @@ const userProfileBillingHistorySectionModule: StoryModule = {
const userProfilePasswordSectionModule: StoryModule = {
meta: userProfilePasswordSectionMeta,
Default: UserProfilePasswordSectionDefault,
SetPassword: UserProfilePasswordSectionSetPassword,
WithoutCurrentPassword: UserProfilePasswordSectionWithoutCurrentPassword,
ActiveEnterpriseAccount: UserProfilePasswordSectionActiveEnterpriseAccount,
EditPasswordFails: UserProfilePasswordSectionEditPasswordFails,
};
const userProfilePasskeysSectionModule: StoryModule = {
meta: userProfilePasskeysSectionMeta,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { UserProfileFormError } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-account-section.types';
import { UserProfileSaveError } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-account-section.types';
import type { UserProfileEditPasswordValue } from '@clerk/ui/mosaic/user-profile/user-profile-password-section/user-profile-edit-password.view';
import { useState } from 'react';

export interface UserProfileEditPasswordFixtureOptions {
hasPassword?: boolean;
requiresCurrentPassword?: boolean;
latency?: number;
/** Rejects every save instead of committing it. */
failWith?: UserProfileFormError;
}

/** Stands in for the model. A successful save leaves the account with a password, whichever it started with. */
export function useUserProfileEditPasswordFixture({
hasPassword: initialHasPassword = true,
requiresCurrentPassword = true,
latency = 800,
failWith,
}: UserProfileEditPasswordFixtureOptions = {}) {
const [hasPassword, setHasPassword] = useState(initialHasPassword);

return {
hasPassword,
requiresCurrentPassword,
onSubmitPassword: async (_value: UserProfileEditPasswordValue) => {
await new Promise(resolve => setTimeout(resolve, latency));
if (failWith) {
throw new UserProfileSaveError(failWith.message ?? 'Something went wrong.', failWith.fields);
}
setHasPassword(true);
},
};
}
5 changes: 3 additions & 2 deletions packages/swingset/src/stories/fixtures/user-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useMemo, useState } from 'react';

import { usePreviewImage } from './use-preview-image';
import { useUserProfileEditNameFixture } from './user-profile-edit-name';
import { useUserProfileEditPasswordFixture } from './user-profile-edit-password';
import { useUserProfileEditUsernameFixture } from './user-profile-edit-username';

export interface UserProfileFixtureOptions {
Expand Down Expand Up @@ -46,6 +47,7 @@ const initialAPIKeys: UserProfileAPIKey[] = [
export function useUserProfileFixture({ onAddEmail }: UserProfileFixtureOptions = {}) {
const editName = useUserProfileEditNameFixture();
const editUsername = useUserProfileEditUsernameFixture();
const editPassword = useUserProfileEditPasswordFixture();
const [activePage, setActivePage] = useState<UserProfileViewProps['activePage']>('account');
const [emails, setEmails] = useState<UserProfileEmail[]>([
{ id: 'email_1', value: 'preston@clerk.dev', isDefault: true, isVerified: true },
Expand Down Expand Up @@ -145,7 +147,7 @@ export function useUserProfileFixture({ onAddEmail }: UserProfileFixtureOptions
setPhones(current => current.map(phone => (phone.id === id ? { ...phone, isVerified: true } : phone))),
},
security: {
hasPassword: true,
...editPassword,
passkeys,
mfaMethods,
devices,
Expand All @@ -159,7 +161,6 @@ export function useUserProfileFixture({ onAddEmail }: UserProfileFixtureOptions
...current,
{ id: `passkey-${Date.now()}`, name: `Passkey ${current.length + 1}`, createdAtLabel: 'Created just now' },
]),
onChangePassword: () => undefined,
onDeleteAccount: () => Promise.resolve(),
onManageDevice: () => undefined,
onManagePasskey: () => undefined,
Expand Down
39 changes: 37 additions & 2 deletions packages/swingset/src/stories/user-profile-password-section.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,45 @@ import * as Stories from './user-profile-password-section.stories';

# UserProfilePasswordSection

Password management composed with `Section`.
## Change password

<Story
name='Default'
storyModule={Stories}
composition={[{ name: 'Section', href: '/components/section', layer: 'Components' }]}
composition={[
{ name: 'Section', href: '/components/section', layer: 'Components' },
{ name: 'Dialog', href: '/components/dialog', layer: 'Components' },
{ name: 'Field', href: '/components/field', layer: 'Components' },
{ name: 'InputGroup', href: '/components/input-group', layer: 'Components' },
{ name: 'Button', href: '/components/button', layer: 'Components' },
{ name: 'Icon', href: '/components/icon', layer: 'Components' },
]}
/>

## Set password

<Story
name='SetPassword'
storyModule={Stories}
/>

## Without current password

<Story
name='WithoutCurrentPassword'
storyModule={Stories}
/>

## Active enterprise account

<Story
name='ActiveEnterpriseAccount'
storyModule={Stories}
/>

## Save fails

<Story
name='EditPasswordFails'
storyModule={Stories}
/>
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { UserProfilePasswordSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-password-section.view';
import type { UserProfileFormError } from '@clerk/ui/mosaic/user-profile/user-profile-account-section/user-profile-account-section.types';
import { UserProfilePasswordSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-password-section/user-profile-password-section.view';

import type { StoryMeta } from '@/lib/types';

import { useUserProfileEditPasswordFixture } from './fixtures/user-profile-edit-password';

export { default as __source } from './user-profile-password-section.stories?raw';

export const meta: StoryMeta = {
Expand All @@ -10,9 +13,57 @@ export const meta: StoryMeta = {
title: 'UserProfilePasswordSection',
label: 'Password',
navigation: { category: 'Sections' },
source: 'packages/ui/src/mosaic/user-profile/user-profile-password-section.view.tsx',
source: 'packages/ui/src/mosaic/user-profile/user-profile-password-section/user-profile-password-section.view.tsx',
};

function PasswordSection({
hasPassword,
requiresCurrentPassword,
hasActiveEnterpriseAccount,
failWith,
}: {
hasPassword?: boolean;
requiresCurrentPassword?: boolean;
hasActiveEnterpriseAccount?: boolean;
failWith?: UserProfileFormError;
}) {
const editPassword = useUserProfileEditPasswordFixture({ hasPassword, requiresCurrentPassword, failWith });

return (
<UserProfilePasswordSectionView
{...editPassword}
hasActiveEnterpriseAccount={hasActiveEnterpriseAccount}
/>
);
}

export function Default() {
return <UserProfilePasswordSectionView onChangePassword={() => undefined} />;
return <PasswordSection />;
}

/** The account has no password yet, so the row sets one instead of changing one. */
export function SetPassword() {
return <PasswordSection hasPassword={false} />;
}

/** Reverification already proved the user, so the dialog skips asking for the current password. */
export function WithoutCurrentPassword() {
return <PasswordSection requiresCurrentPassword={false} />;
}

/** The account signs in only through an enterprise connection: the dialog opens to say so, and nothing else. */
export function ActiveEnterpriseAccount() {
return <PasswordSection hasActiveEnterpriseAccount />;
}

/** Every save is rejected, so the dialog shows both halves of a failure at once. */
export function EditPasswordFails() {
return (
<PasswordSection
failWith={{
message: 'Your password could not be updated.',
fields: { currentPassword: 'Incorrect password.' },
}}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useState } from 'react';

import type { StoryMeta } from '@/lib/types';

import { useUserProfileEditPasswordFixture } from './fixtures/user-profile-edit-password';

export { default as __source } from './user-profile-security-panel.stories?raw';

export const meta: StoryMeta = {
Expand All @@ -20,6 +22,7 @@ export const meta: StoryMeta = {
};

export function Default() {
const editPassword = useUserProfileEditPasswordFixture();
const [passkeys, setPasskeys] = useState<UserProfilePasskey[]>([
{
id: 'passkey',
Expand Down Expand Up @@ -56,8 +59,8 @@ export function Default() {

return (
<UserProfileSecurityPanelView
{...editPassword}
devices={devices}
hasPassword
mfaMethods={mfaMethods}
passkeys={passkeys}
onAddMfaMethod={type =>
Expand All @@ -82,7 +85,6 @@ export function Default() {
{ id: `passkey-${Date.now()}`, name: `Passkey ${current.length + 1}`, createdAtLabel: 'Created just now' },
])
}
onChangePassword={() => undefined}
onDeleteAccount={() => Promise.resolve()}
onManageDevice={() => undefined}
onManagePasskey={() => undefined}
Expand Down
Loading
Loading