Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
58d9fab
feat(ui): add user profile password dialog
austincalvelage Aug 20, 2026
5ed2d8a
feat(ui): add nested password verification example
austincalvelage Aug 20, 2026
f346fe0
feat(swingset): compose password dialog in profile stories
austincalvelage Aug 20, 2026
40d39da
feat(ui): add delete account alert dialog
austincalvelage Aug 20, 2026
2777d6c
feat(ui): add sign out all devices alert dialog
austincalvelage Aug 20, 2026
fbb8827
feat(ui): model user profile security flows
austincalvelage Aug 20, 2026
95ccaf4
feat(ui): add active device dialog
austincalvelage Aug 20, 2026
4174e15
feat(ui): add two-factor security flows
austincalvelage Aug 20, 2026
89994db
feat(ui): add passkey security dialogs
austincalvelage Aug 20, 2026
99318fc
feat(ui): add backup code security flow
austincalvelage Aug 20, 2026
5917116
fix(ui): align backup code regeneration flow
austincalvelage Aug 20, 2026
122aa16
feat(ui): model legacy security flow states
austincalvelage Aug 20, 2026
71875d9
refactor(ui): reuse shared profile flow chrome
austincalvelage Aug 21, 2026
85c009a
refactor(swingset): split security flow harnesses by section
austincalvelage Aug 21, 2026
565f0c9
refactor(ui): move password dialog lifecycle to flow
austincalvelage Aug 21, 2026
8593556
refactor(ui): move passkey dialog lifecycle to flow
austincalvelage Aug 21, 2026
6e83f20
refactor(ui): move mfa dialog lifecycle to flow
austincalvelage Aug 21, 2026
df45b34
refactor(ui): move delete dialog lifecycle to flow
austincalvelage Aug 21, 2026
7bda0e6
refactor(ui): move device dialog lifecycle to flow
austincalvelage Aug 21, 2026
f7b6da2
refactor(ui): name security flow action contracts
austincalvelage Aug 21, 2026
a212682
refactor(ui): use shared chrome for security prompts
austincalvelage Aug 21, 2026
68b1592
fix(ui): clarify bulk device sign out
austincalvelage Aug 21, 2026
c52cd70
refactor(swingset): split security stories by section
austincalvelage Aug 21, 2026
3344952
feat(ui): model legacy password requirements
austincalvelage Aug 21, 2026
ba813ff
feat(ui): model legacy reverification routes
austincalvelage Aug 21, 2026
4fbf99c
feat(ui): model legacy mfa setup states
austincalvelage Aug 21, 2026
699c653
feat(ui): model legacy active device states
austincalvelage Aug 21, 2026
2cc2acb
feat(ui): model post-delete session handling
austincalvelage Aug 21, 2026
57d95e8
fix(ui): harden pending security operations
austincalvelage Aug 21, 2026
02aeb36
refactor(ui): centralize security flow messages
austincalvelage Aug 21, 2026
58655a7
fix(swingset): satisfy security flow lint
austincalvelage Aug 21, 2026
5e0d2a4
refactor(swingset): consolidate security panel flow
austincalvelage Aug 21, 2026
1ec1a92
fix(ui): start passkey creation directly
austincalvelage Aug 21, 2026
be71c22
refactor(swingset): remove security panel state story
austincalvelage Aug 21, 2026
d8f6f44
fix(ui): simplify backup code actions
austincalvelage Aug 21, 2026
c9cedb6
fix(ui): use white QR background
austincalvelage Aug 21, 2026
9d18a1c
fix(ui): constrain authenticator setup values
austincalvelage Aug 21, 2026
3141537
fix(ui): render authenticator values as inputs
austincalvelage Aug 21, 2026
80296d0
fix(ui): close completed MFA enrollment
austincalvelage Aug 21, 2026
7488a6a
fix(ui): sign out devices directly
austincalvelage Aug 21, 2026
973eb1f
fix(swingset): allow dismissing security dialogs
austincalvelage Aug 21, 2026
a69367d
fix(ui): restore backup code printing
austincalvelage Aug 21, 2026
b4c0bdf
refactor(swingset): remove duplicate security stories
austincalvelage Aug 21, 2026
3cc47ce
refactor(swingset): centralize security flow controller
austincalvelage Aug 21, 2026
fc68b18
refactor(swingset): compact security flow states
austincalvelage Aug 21, 2026
5a44aae
docs(swingset): simplify security flow guidance
austincalvelage Aug 21, 2026
cbcd22f
fix(ui): match legacy security menu labels
austincalvelage Aug 21, 2026
a2e6111
refactor(ui): render security dialogs from panel view
austincalvelage Aug 21, 2026
6c1fd21
refactor(swingset): compose security stories through shared controller
austincalvelage Aug 21, 2026
81ab845
fix(swingset): share composed security flow config
austincalvelage Aug 21, 2026
f0825ec
fix(swingset): close security reverification dialog
austincalvelage Aug 21, 2026
7123ef0
chore(ui): satisfy security flow lint
austincalvelage Aug 24, 2026
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/mosaic-password-dialog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
1 change: 1 addition & 0 deletions packages/swingset/src/components/DocsViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const docModules: Record<string, Record<string, React.ComponentType>> = {
'user-profile': {
'user-page': dynamic(() => import('../stories/user-page.mdx')),
'user-profile-account-section-flow': dynamic(() => import('../stories/user-profile-account-section-flow.mdx')),
'user-profile-security-panel-flow': dynamic(() => import('../stories/user-profile-security-panel-flow.mdx')),
'user-profile-profile-panel': dynamic(() => import('../stories/user-profile-profile-panel.mdx')),
'user-profile-security-panel': dynamic(() => import('../stories/user-profile-security-panel.mdx')),
'user-profile-billing-panel': dynamic(() => import('../stories/user-profile-billing-panel.mdx')),
Expand Down
11 changes: 11 additions & 0 deletions packages/swingset/src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ import {
Default as UserProfileSecurityPanelDefault,
meta as userProfileSecurityPanelMeta,
} from '../stories/user-profile-security-panel.stories';
import {
Default as UserProfileSecurityPanelFlowDefault,
meta as userProfileSecurityPanelFlowMeta,
States as UserProfileSecurityPanelFlowStates,
} from '../stories/user-profile-security-panel-flow.stories';
import {
Default as UserProfileSubscriptionSectionDefault,
meta as userProfileSubscriptionSectionMeta,
Expand Down Expand Up @@ -365,13 +370,19 @@ const userProfileDeleteSectionModule: StoryModule = {
meta: userProfileDeleteSectionMeta,
Default: UserProfileDeleteSectionDefault,
};
const userProfileSecurityPanelFlowModule: StoryModule = {
meta: userProfileSecurityPanelFlowMeta,
Default: UserProfileSecurityPanelFlowDefault,
States: UserProfileSecurityPanelFlowStates,
};

export const registry: StoryModule[] = [
// User Button
userButtonModule,
// User Profile
userPageModule,
userProfileAccountSectionFlowModule,
userProfileSecurityPanelFlowModule,
// User Profile 路 Panels
userProfileProfilePanelModule,
userProfileSecurityPanelModule,
Expand Down
77 changes: 7 additions & 70 deletions packages/swingset/src/stories/user-page.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ import type {
UserProfileSubscription,
} from '@clerk/ui/mosaic/user-profile/user-profile-billing-panel.view';
import type { UserProfileEmail, UserProfilePhone } from '@clerk/ui/mosaic/user-profile/user-profile-profile-panel.view';
import type {
UserProfileDevice,
UserProfileMfaMethod,
UserProfilePasskey,
} from '@clerk/ui/mosaic/user-profile/user-profile-security-panel.view';
import type { UserProfilePanelId } from '@clerk/ui/mosaic/user-profile/user-profile-sidebar';
import { useMemo, useState } from 'react';

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

import { COMPOSED_SECURITY_FLOW_CONFIG } from './user-profile-security-panel-flow.config';
import { useUserProfileSecurityPanelMockController } from './user-profile-security-panel-flow.controller';

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

export const meta: StoryMeta = {
Expand Down Expand Up @@ -46,40 +44,16 @@ const initialAPIKeys: UserProfileAPIKey[] = [

export function Default() {
const [activePanel, setActivePanel] = useState<UserProfilePanelId>('account');
const securityController = useUserProfileSecurityPanelMockController({
config: COMPOSED_SECURITY_FLOW_CONFIG,
});
const [emails, setEmails] = useState<UserProfileEmail[]>([
{ id: 'email_1', value: 'item1@clerk.dev', isDefault: true, isVerified: true },
{ id: 'email_2', value: 'item2@clerk.dev', isVerified: true },
]);
const [phones, setPhones] = useState<UserProfilePhone[]>([
{ id: 'phone_1', value: '+1 801-888-8181', isDefault: true, isVerified: true },
]);
const [passkeys, setPasskeys] = useState<UserProfilePasskey[]>([
{
id: 'passkey',
name: 'Passkey',
createdAtLabel: 'Created today at 10:12 PM',
lastUsedAtLabel: 'Last used 1h ago',
},
]);
const [mfaMethods, setMfaMethods] = useState<UserProfileMfaMethod[]>([
{ id: 'sms', type: 'sms', description: '+1 801-888-8181' },
{ id: 'backup', type: 'backup-codes' },
]);
const [devices, setDevices] = useState<UserProfileDevice[]>([
{
id: 'current',
name: 'Safari on macOS',
description: 'Salt Lake City, UT, United States',
type: 'desktop',
isCurrent: true,
},
{
id: 'mobile',
name: 'Safari on iOS',
description: 'Last seen 2 weeks ago 路 Orem, UT, United States',
type: 'mobile',
},
]);
const [subscription, setSubscription] = useState<UserProfileSubscription>({
planName: 'Basic Plan',
priceLabel: '$12 / Month',
Expand Down Expand Up @@ -136,44 +110,7 @@ export function Default() {
onVerifyPhone: id =>
setPhones(current => current.map(phone => (phone.id === id ? { ...phone, isVerified: true } : phone))),
},
security: {
hasPassword: true,
passkeys,
mfaMethods,
devices,
onAddMfaMethod: type =>
setMfaMethods(current => {
const timestamp = Date.now();
return [
...current,
{
id: `${type}-${timestamp}`,
type,
description: type === 'sms' ? '+1 801-555-0100' : undefined,
},
...(current.some(method => method.type === 'backup-codes')
? []
: [{ id: `backup-${timestamp}`, type: 'backup-codes' as const }]),
];
}),
onAddPasskey: () =>
setPasskeys(current => [
...current,
{ id: `passkey-${Date.now()}`, name: `Passkey ${current.length + 1}`, createdAtLabel: 'Created just now' },
]),
onChangePassword: () => undefined,
onDeleteAccount: () => undefined,
onManageDevice: () => undefined,
onManagePasskey: () => undefined,
onRegenerateBackupCodes: () =>
setMfaMethods(current =>
current.map(method => (method.type === 'backup-codes' ? { ...method, description: 'Just now' } : method)),
),
onRemoveMfaMethod: id => setMfaMethods(current => current.filter(method => method.id !== id)),
onRemovePasskey: id => setPasskeys(current => current.filter(passkey => passkey.id !== id)),
onSignOutAllOtherDevices: () => setDevices(current => current.filter(device => device.isCurrent)),
onSignOutDevice: id => setDevices(current => current.filter(device => device.id !== id)),
},
security: securityController,
billing: {
subscription,
paymentMethods,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { UserProfileDevice } from '@clerk/ui/mosaic/user-profile/user-profile-active-devices-section.view';
import { SecurityPanelDialogsView } from '@clerk/ui/mosaic/user-profile/dialogs/security-panel-dialogs.view';
import { UserProfileActiveDevicesSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-active-devices-section.view';
import { useState } from 'react';

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

import { useUserProfileSecurityPanelMockController } from './user-profile-security-panel-flow.controller';

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

export const meta: StoryMeta = {
Expand All @@ -15,34 +16,20 @@ export const meta: StoryMeta = {
};

export function Default() {
const [devices, setDevices] = useState<UserProfileDevice[]>([
{
id: 'current',
name: 'Safari on macOS',
description: 'Salt Lake City, UT, United States',
type: 'desktop',
isCurrent: true,
},
{
id: 'mobile',
name: 'Safari on iOS',
description: 'Last seen 2 weeks ago 路 Orem, UT, United States',
type: 'mobile',
},
{
id: 'desktop',
name: 'Clerk App on macOS',
description: 'Last seen May 14th, 2026 路 San Francisco, CA, United States',
type: 'desktop',
},
]);
const controller = useUserProfileSecurityPanelMockController();

return (
<UserProfileActiveDevicesSectionView
devices={devices}
onManageDevice={() => undefined}
onSignOutAllOtherDevices={() => setDevices(current => current.filter(device => device.isCurrent))}
onSignOutDevice={id => setDevices(current => current.filter(device => device.id !== id))}
/>
<>
<UserProfileActiveDevicesSectionView
devices={controller.devices ?? []}
error={controller.devicesError}
signOutState={controller.deviceSignOutState}
status={controller.devicesStatus}
onManageDevice={controller.onManageDevice}
onSignOutAllOtherDevices={controller.onSignOutAllOtherDevices}
onSignOutDevice={controller.onSignOutDevice}
/>
<SecurityPanelDialogsView {...controller} />
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { SecurityPanelDialogsView } from '@clerk/ui/mosaic/user-profile/dialogs/security-panel-dialogs.view';
import { UserProfileDeleteSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-delete-section.view';

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

import { useUserProfileSecurityPanelMockController } from './user-profile-security-panel-flow.controller';

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

export const meta: StoryMeta = {
Expand All @@ -13,5 +16,12 @@ export const meta: StoryMeta = {
};

export function Default() {
return <UserProfileDeleteSectionView onDelete={() => undefined} />;
const controller = useUserProfileSecurityPanelMockController();

return (
<>
<UserProfileDeleteSectionView onDelete={() => controller.onDeleteAccount?.()} />
<SecurityPanelDialogsView {...controller} />
</>
);
}
93 changes: 34 additions & 59 deletions packages/swingset/src/stories/user-profile-mfa-section.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { UserProfileMfaMethod } from '@clerk/ui/mosaic/user-profile/user-profile-mfa-section.view';
import { SecurityPanelDialogsView } from '@clerk/ui/mosaic/user-profile/dialogs/security-panel-dialogs.view';
import { UserProfileMfaSectionView } from '@clerk/ui/mosaic/user-profile/user-profile-mfa-section.view';
import { useState } from 'react';

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

import { COMPOSED_SECURITY_FLOW_CONFIG } from './user-profile-security-panel-flow.config';
import { useUserProfileSecurityPanelMockController } from './user-profile-security-panel-flow.controller';

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

export const meta: StoryMeta = {
Expand All @@ -15,70 +17,43 @@ export const meta: StoryMeta = {
};

export function Default() {
const [methods, setMethods] = useState<UserProfileMfaMethod[]>([
{ id: 'sms', type: 'sms', description: '+1 801-888-8181' },
{ id: 'backup', type: 'backup-codes' },
]);
const controller = useUserProfileSecurityPanelMockController({
config: COMPOSED_SECURITY_FLOW_CONFIG,
});

return (
<UserProfileMfaSectionView
methods={methods}
sectionTitle='Authentication'
onAdd={type =>
setMethods(current => {
const timestamp = Date.now();
return [
...current,
{
id: `${type}-${timestamp}`,
type,
description: type === 'sms' ? '+1 801-555-0100' : undefined,
},
...(current.some(method => method.type === 'backup-codes')
? []
: [{ id: `backup-${timestamp}`, type: 'backup-codes' as const }]),
];
})
}
onRegenerateBackupCodes={() =>
setMethods(current =>
current.map(method => (method.type === 'backup-codes' ? { ...method, description: 'Just now' } : method)),
)
}
onRemove={id => setMethods(current => current.filter(method => method.id !== id))}
/>
<>
<UserProfileMfaSectionView
addableMethods={controller.mfaAddableMethods}
methods={controller.mfaMethods ?? []}
sectionTitle='Authentication'
onAdd={controller.onAddMfaMethod}
onEnableBackupCodes={controller.onEnableBackupCodes}
onRegenerateBackupCodes={controller.onRegenerateBackupCodes}
onRemove={controller.onRemoveMfaMethod}
onSetDefault={controller.onSetDefaultMfaMethod}
/>
<SecurityPanelDialogsView {...controller} />
</>
);
}

export function Empty() {
const [methods, setMethods] = useState<UserProfileMfaMethod[]>([]);
const controller = useUserProfileSecurityPanelMockController();

return (
<UserProfileMfaSectionView
methods={methods}
sectionTitle='Authentication'
onAdd={type =>
setMethods(current => {
const timestamp = Date.now();
return [
...current,
{
id: `${type}-${timestamp}`,
type,
description: type === 'sms' ? '+1 801-555-0100' : undefined,
},
...(current.some(method => method.type === 'backup-codes')
? []
: [{ id: `backup-${timestamp}`, type: 'backup-codes' as const }]),
];
})
}
onRegenerateBackupCodes={() =>
setMethods(current =>
current.map(method => (method.type === 'backup-codes' ? { ...method, description: 'Just now' } : method)),
)
}
onRemove={id => setMethods(current => current.filter(method => method.id !== id))}
/>
<>
<UserProfileMfaSectionView
addableMethods={controller.mfaAddableMethods}
methods={controller.mfaMethods ?? []}
sectionTitle='Authentication'
onAdd={controller.onAddMfaMethod}
onEnableBackupCodes={controller.onEnableBackupCodes}
onRegenerateBackupCodes={controller.onRegenerateBackupCodes}
onRemove={controller.onRemoveMfaMethod}
onSetDefault={controller.onSetDefaultMfaMethod}
/>
<SecurityPanelDialogsView {...controller} />
</>
);
}
Loading
Loading