From e59d54ea6e1a76cc18193c36816669d03012b4a8 Mon Sep 17 00:00:00 2001
From: austincalvelage
Date: Tue, 29 Sep 2026 00:09:50 -0600
Subject: [PATCH 1/7] feat(mosaic): wire enterprise account linking and
reverification
---
.changeset/bright-enterprise-accounts.md | 2 +
...rise-accounts-section.integration.test.tsx | 319 ++++++++++++++++++
.../user-profile-profile-panel.view.test.tsx | 12 +
...prise-accounts-section.controller.test.tsx | 78 +++++
...-enterprise-accounts-section.controller.ts | 89 +++++
...le-enterprise-accounts-section.messages.ts | 4 +
...-enterprise-accounts-section.model.test.ts | 53 +++
...file-enterprise-accounts-section.model.tsx | 133 ++++++++
...er-profile-enterprise-accounts-section.tsx | 41 +++
.../user-profile-profile-panel.view.tsx | 3 +
packages/mosaic/src/styles/index.ts | 1 +
11 files changed, 735 insertions(+)
create mode 100644 .changeset/bright-enterprise-accounts.md
create mode 100644 packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
create mode 100644 packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx
create mode 100644 packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.ts
create mode 100644 packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts
create mode 100644 packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
create mode 100644 packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx
diff --git a/.changeset/bright-enterprise-accounts.md b/.changeset/bright-enterprise-accounts.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/bright-enterprise-accounts.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
new file mode 100644
index 00000000000..1b188b8e36f
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
@@ -0,0 +1,319 @@
+import { ClerkAPIResponseError } from '@clerk/shared/error';
+import type * as SharedReact from '@clerk/shared/react';
+import { createDeferredPromise } from '@clerk/shared/utils';
+import { render, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { useReverification as useReverificationSource } from '../../../../../shared/src/react/hooks/useReverification';
+import { MosaicProvider } from '../../../MosaicProvider';
+import { UserProfileEnterpriseAccountsSection } from '../user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section';
+
+let isLoaded: boolean;
+let signedIn: boolean;
+let enabled: boolean;
+let enterpriseAccounts: Record[];
+let connections: Record[] | undefined;
+let createExternalAccount: ReturnType;
+let navigate: ReturnType | undefined;
+let setActive: ReturnType;
+let startVerification: ReturnType;
+let attemptFirstFactorVerification: ReturnType;
+let fallbackNavigate: ReturnType;
+let queryOptions: { enabled?: boolean; withOrganizationAccountLinking?: boolean } | undefined;
+
+const reverificationHint = {
+ clerk_error: {
+ type: 'forbidden',
+ reason: 'reverification-error',
+ metadata: { reverification: { level: 'first_factor', afterMinutes: 1 } },
+ },
+};
+
+vi.mock('../../../../../shared/src/react/hooks/useClerk', () => ({
+ useClerk: () => ({ __internal_openReverification: vi.fn(), telemetry: { record: vi.fn() } }),
+}));
+
+vi.mock('@clerk/shared/internal/clerk-js/windowNavigate', () => ({
+ windowNavigate: (url: URL | string) => fallbackNavigate(url),
+}));
+
+vi.mock('@clerk/shared/react', async importOriginal => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useUser: () => ({
+ isLoaded,
+ user: signedIn ? { id: 'user_1', enterpriseAccounts, createExternalAccount } : null,
+ }),
+ __internal_useUserEnterpriseConnections: (options: typeof queryOptions) => {
+ queryOptions = options;
+ return { data: connections };
+ },
+ useClerk: () => ({
+ __internal_environment: {
+ userSettings: { enterpriseSSO: { enabled } },
+ displayConfig: { supportEmail: 'help@example.com' },
+ },
+ __internal_windowNavigate: navigate,
+ __internal_getOption: () => undefined,
+ setActive,
+ }),
+ useSession: () => ({
+ session: {
+ id: 'session_1',
+ startVerification,
+ attemptFirstFactorVerification,
+ },
+ }),
+ useReverification: useReverificationSource,
+ };
+});
+
+function connection(id: string, overrides: Record = {}) {
+ return {
+ id,
+ name: id === 'okta' ? 'Acme Okta' : 'Custom SAML',
+ logoPublicUrl: null,
+ allowOrganizationAccountLinking: true,
+ ...overrides,
+ };
+}
+
+function account(id: string, connectionId: string, overrides: Record = {}) {
+ return {
+ id,
+ enterpriseConnectionId: connectionId,
+ emailAddress: `${id}@example.com`,
+ enterpriseConnection: { active: true, name: connectionId, logoPublicUrl: null },
+ verification: { error: null },
+ ...overrides,
+ };
+}
+
+function response(url = 'https://accounts.example/authorize') {
+ return { verification: { externalVerificationRedirectURL: new URL(url) } };
+}
+
+function renderSection(props: Parameters[0] = {}) {
+ return render(
+
+
+ ,
+ );
+}
+
+describe('UserProfileEnterpriseAccountsSection', () => {
+ beforeEach(() => {
+ isLoaded = true;
+ signedIn = true;
+ enabled = true;
+ enterpriseAccounts = [];
+ connections = [connection('okta'), connection('saml')];
+ createExternalAccount = vi.fn().mockResolvedValue(response());
+ navigate = vi.fn();
+ setActive = vi.fn().mockResolvedValue(undefined);
+ startVerification = vi.fn().mockResolvedValue({
+ status: 'needs_first_factor',
+ supportedFirstFactors: [{ strategy: 'password' }],
+ });
+ attemptFirstFactorVerification = vi.fn().mockResolvedValue({ status: 'complete' });
+ fallbackNavigate = vi.fn();
+ queryOptions = undefined;
+ });
+
+ it('hides data when SSO is disabled and handles loading, signed-out, and empty states', () => {
+ enterpriseAccounts = [account('linked', 'okta')];
+ enabled = false;
+ const { container, rerender } = renderSection({ fallback: Loading accounts
});
+ expect(container).toBeEmptyDOMElement();
+ expect(queryOptions).toEqual({ enabled: false, withOrganizationAccountLinking: true });
+
+ enabled = true;
+ isLoaded = false;
+ rerender(
+
+ Loading accounts
} />
+ ,
+ );
+ expect(screen.getByText('Loading accounts')).toBeInTheDocument();
+ expect(queryOptions?.enabled).toBe(false);
+
+ isLoaded = true;
+ signedIn = false;
+ rerender(
+
+
+ ,
+ );
+ expect(container).toBeEmptyDOMElement();
+ expect(queryOptions?.enabled).toBe(false);
+
+ signedIn = true;
+ enterpriseAccounts = [];
+ connections = [];
+ rerender(
+
+
+ ,
+ );
+ expect(container).toBeEmptyDOMElement();
+ expect(queryOptions?.enabled).toBe(true);
+ });
+
+ it('shows rows by connection activity and only offers organization-linkable, unlinked connections', () => {
+ enterpriseAccounts = [
+ account('linked', 'okta', { active: false, verification: { error: { longMessage: 'Fix this account' } } }),
+ account('inactive', 'saml', { active: true, enterpriseConnection: { active: false, name: 'Custom SAML' } }),
+ ];
+ connections = [
+ connection('okta'),
+ connection('saml'),
+ connection('other', { allowOrganizationAccountLinking: false }),
+ ];
+ renderSection();
+ expect(screen.getByText('linked@example.com')).toBeInTheDocument();
+ expect(screen.getByText('Requires action')).toBeInTheDocument();
+ expect(screen.queryByText('inactive@example.com')).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /^Connect/ })).not.toBeInTheDocument();
+ });
+
+ it('keeps linked rows while the connection query is unresolved', () => {
+ enterpriseAccounts = [account('linked', 'okta')];
+ connections = undefined;
+ renderSection();
+ expect(screen.getByText('linked@example.com')).toBeInTheDocument();
+ });
+
+ it('connects with the current URL and navigates using Clerk', async () => {
+ const user = userEvent.setup();
+ renderSection();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() =>
+ expect(createExternalAccount).toHaveBeenCalledWith({
+ enterpriseConnectionId: 'okta',
+ redirectUrl: window.location.href,
+ }),
+ );
+ expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize'));
+ });
+
+ it('uses the shared navigation fallback when the Clerk instance lacks the internal method', async () => {
+ navigate = undefined;
+ const user = userEvent.setup();
+ renderSection();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(fallbackNavigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize')));
+ });
+
+ it('holds the selected row pending and disables competing actions while connecting', async () => {
+ const operation = createDeferredPromise();
+ createExternalAccount = vi.fn(() => operation.promise);
+ const user = userEvent.setup();
+ renderSection();
+ const okta = screen.getByRole('button', { name: 'Connect Acme Okta' });
+ const saml = screen.getByRole('button', { name: 'Connect Custom SAML' });
+ await user.click(okta);
+ expect(okta).toHaveAttribute('aria-busy', 'true');
+ expect(saml).toBeDisabled();
+ await user.click(saml);
+ expect(createExternalAccount).toHaveBeenCalledOnce();
+ operation.resolve(response());
+ await waitFor(() => expect(navigate).toHaveBeenCalledOnce());
+ expect(okta).toHaveAttribute('aria-busy', 'true');
+ });
+
+ it('preserves the UserProfile modal return state', async () => {
+ const user = userEvent.setup();
+ renderSection({ mode: 'modal' });
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(createExternalAccount).toHaveBeenCalledOnce());
+ const redirectUrl = createExternalAccount.mock.calls[0]?.[0]?.redirectUrl;
+ const encoded = new URL(redirectUrl).searchParams.get('__clerk_modal_state');
+ expect(encoded).toBeTruthy();
+ expect(JSON.parse(window.atob(encoded))).toMatchObject({ componentName: 'UserProfile' });
+ });
+
+ it('shows a recoverable error when verification has no redirect', async () => {
+ createExternalAccount = vi.fn().mockResolvedValue({ verification: null });
+ const user = userEvent.setup();
+ renderSection();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent('The connection could not start. Please try again.');
+ expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
+ });
+
+ it('shows the API long message and allows retry', async () => {
+ createExternalAccount = vi
+ .fn()
+ .mockRejectedValueOnce(
+ new ClerkAPIResponseError('failed', {
+ data: [{ code: 'enterprise_error', message: 'failed', long_message: 'Acme is unavailable.' }],
+ status: 422,
+ }),
+ )
+ .mockResolvedValueOnce(response());
+ const user = userEvent.setup();
+ renderSection();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent('Acme is unavailable.');
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(createExternalAccount).toHaveBeenCalledTimes(2));
+ });
+
+ it('runs a real Mosaic reverification challenge, then retries the same connection', async () => {
+ createExternalAccount = vi
+ .fn()
+ .mockRejectedValueOnce(
+ new ClerkAPIResponseError('reverification required', {
+ data: [{ code: 'session_reverification_required', message: 'Verify your session.' }],
+ status: 403,
+ }),
+ )
+ .mockResolvedValueOnce(response());
+ const user = userEvent.setup();
+ renderSection();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ const dialog = await screen.findByRole('dialog', { name: 'Verification required' });
+ await user.type(within(dialog).getByLabelText('Password'), 'secret');
+ await user.click(within(dialog).getByRole('button', { name: 'Continue' }));
+ await waitFor(() => expect(createExternalAccount).toHaveBeenCalledTimes(2));
+ expect(createExternalAccount.mock.calls[0]?.[0]).toEqual(createExternalAccount.mock.calls[1]?.[0]);
+ expect(attemptFirstFactorVerification).toHaveBeenCalledWith({ strategy: 'password', password: 'secret' });
+ expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize'));
+ });
+
+ it('shows a failed reverification retry on the selected row', async () => {
+ createExternalAccount = vi
+ .fn()
+ .mockResolvedValueOnce(reverificationHint)
+ .mockRejectedValueOnce(
+ new ClerkAPIResponseError('failed', {
+ data: [{ code: 'enterprise_error', message: 'failed', long_message: 'Account linking failed.' }],
+ status: 422,
+ }),
+ );
+ const user = userEvent.setup();
+ renderSection();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ const dialog = await screen.findByRole('dialog', { name: 'Verification required' });
+ await user.type(within(dialog).getByLabelText('Password'), 'secret');
+ await user.click(within(dialog).getByRole('button', { name: 'Continue' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent('Account linking failed.');
+ expect(navigate).not.toHaveBeenCalled();
+ expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
+ });
+
+ it('cancels a real Mosaic reverification challenge without showing an error', async () => {
+ createExternalAccount = vi.fn().mockResolvedValue(reverificationHint);
+ const user = userEvent.setup();
+ renderSection();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ const dialog = await screen.findByRole('dialog', { name: 'Verification required' });
+ await user.click(within(dialog).getByRole('button', { name: 'Close' }));
+ await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled());
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ expect(createExternalAccount).toHaveBeenCalledOnce();
+ });
+});
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx
index a12b39214b2..fcef008fee8 100644
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx
@@ -308,6 +308,18 @@ describe('UserProfileProfilePanelView', () => {
expect(onDeleteAccount).toHaveBeenCalledOnce();
});
+ it('places enterprise accounts after Web3 wallets and before the danger zone', () => {
+ const { container } = renderView({
+ web3WalletsSlot: Wallets
,
+ enterpriseAccountsSlot: Enterprise accounts
,
+ deleteAccountSlot: Danger zone
,
+ });
+
+ expect(
+ Array.from(container.querySelectorAll('[data-testid]')).map(element => element.getAttribute('data-testid')),
+ ).toEqual(['wallets', 'enterprise', 'danger']);
+ });
+
it('renders connected provider and Web3 images inside icon frames', () => {
const { container } = renderView({
connectedAccounts: [{ id: 'google', provider: 'Google', iconUrl: '/google.svg' }],
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx
new file mode 100644
index 00000000000..2ec0d1151c5
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx
@@ -0,0 +1,78 @@
+import { act, renderHook, waitFor } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { useUserProfileEnterpriseAccountsController } from './user-profile-enterprise-accounts-section.controller';
+
+const connections = [
+ { id: 'okta', name: 'Acme Okta' },
+ { id: 'saml', name: 'Custom SAML' },
+];
+
+function deferred() {
+ let resolve: (value: T) => void = () => undefined;
+ let reject: (error: unknown) => void = () => undefined;
+ const promise = new Promise((resolvePromise, rejectPromise) => {
+ resolve = resolvePromise;
+ reject = rejectPromise;
+ });
+ return { promise, resolve, reject };
+}
+
+afterEach(() => vi.useRealTimers());
+
+describe('useUserProfileEnterpriseAccountsController', () => {
+ it('synchronously suppresses duplicate and competing connects while keeping the selected row pending', async () => {
+ const operation = deferred<'redirecting'>();
+ const onConnect = vi.fn(() => operation.promise);
+ const { result } = renderHook(() =>
+ useUserProfileEnterpriseAccountsController({ status: 'ready', accounts: [], connections, onConnect }),
+ );
+
+ act(() => {
+ result.current.onConnect?.('okta');
+ result.current.onConnect?.('okta');
+ result.current.onConnect?.('saml');
+ });
+ expect(onConnect).toHaveBeenCalledExactlyOnceWith('okta');
+ expect(result.current.pendingConnectionId).toBe('okta');
+
+ await act(async () => {
+ operation.resolve('redirecting');
+ await operation.promise;
+ });
+ expect(result.current.pendingConnectionId).toBe('okta');
+ });
+
+ it('returns to idle with a row error on failure and permits a retry', async () => {
+ const onConnect = vi
+ .fn()
+ .mockRejectedValueOnce(new Error('Network unavailable'))
+ .mockResolvedValueOnce('redirecting');
+ const { result } = renderHook(() =>
+ useUserProfileEnterpriseAccountsController({ status: 'ready', accounts: [], connections, onConnect }),
+ );
+ act(() => result.current.onConnect?.('okta'));
+ await waitFor(() => expect(result.current.connections[0]?.connectError).toBe('Network unavailable'));
+ expect(result.current.pendingConnectionId).toBeUndefined();
+ act(() => result.current.onConnect?.('okta'));
+ await waitFor(() => expect(onConnect).toHaveBeenCalledTimes(2));
+ });
+
+ it('releases redirect pending after two seconds', async () => {
+ vi.useFakeTimers();
+ const onConnect = vi.fn().mockResolvedValue('redirecting');
+ const { result } = renderHook(() =>
+ useUserProfileEnterpriseAccountsController({ status: 'ready', accounts: [], connections, onConnect }),
+ );
+ await act(async () => {
+ result.current.onConnect?.('okta');
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+ expect(result.current.pendingConnectionId).toBe('okta');
+ act(() => {
+ vi.advanceTimersByTime(2000);
+ });
+ expect(result.current.pendingConnectionId).toBeUndefined();
+ });
+});
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.ts b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.ts
new file mode 100644
index 00000000000..3ca2e52d925
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.ts
@@ -0,0 +1,89 @@
+import { useMessages } from '../../../localization';
+import { setup } from '../../../machine/setup';
+import { useMachine } from '../../../machine/useMachine';
+import type { UserProfileEnterpriseAccountsSectionViewProps } from './user-profile-enterprise-accounts-section.view';
+
+type Ready = Required> & {
+ status: 'ready';
+ onConnect: (connectionId: string) => Promise<'redirecting' | void>;
+};
+
+interface Context {
+ selectedId: string | undefined;
+ errorId: string | undefined;
+ errorMessage: string | undefined;
+ run: () => Promise<'redirecting' | void>;
+}
+
+type Event = { type: 'CONNECT'; id: string; run: () => Promise<'redirecting' | void> };
+
+const { createMachine, assign, fromPromise } = setup();
+
+const machine = createMachine({
+ id: 'enterpriseAccountConnect',
+ initial: 'idle',
+ context: {
+ selectedId: undefined,
+ errorId: undefined,
+ errorMessage: undefined,
+ run: () => Promise.resolve(),
+ },
+ states: {
+ idle: {
+ on: {
+ CONNECT: {
+ target: 'connecting',
+ actions: assign((_, event) => ({
+ selectedId: event.id,
+ errorId: undefined,
+ errorMessage: undefined,
+ run: event.run,
+ })),
+ },
+ },
+ },
+ connecting: {
+ invoke: fromPromise(context => context.run(), {
+ onDone: [
+ { target: 'redirecting', guard: (_, event) => event.output === 'redirecting' },
+ { target: 'idle', actions: assign(() => ({ selectedId: undefined })) },
+ ],
+ onError: {
+ target: 'idle',
+ actions: assign((context, event) => ({
+ selectedId: undefined,
+ errorId: context.selectedId,
+ errorMessage: event.error instanceof Error && event.error.message ? event.error.message : undefined,
+ })),
+ },
+ }),
+ },
+ redirecting: {
+ after: {
+ 2000: { target: 'idle', actions: assign(() => ({ selectedId: undefined })) },
+ },
+ },
+ },
+});
+
+export function useUserProfileEnterpriseAccountsController(
+ model: Ready,
+): UserProfileEnterpriseAccountsSectionViewProps {
+ const messages = useMessages('userProfileEnterpriseAccountsSection');
+ const [snapshot, send] = useMachine(machine);
+ const { selectedId, errorId, errorMessage } = snapshot.context;
+
+ return {
+ accounts: model.accounts,
+ connections: model.connections.map(connection =>
+ connection.id === errorId ? { ...connection, connectError: errorMessage ?? messages.errors.generic } : connection,
+ ),
+ pendingConnectionId: selectedId,
+ onConnect: id => {
+ if (!model.connections.some(connection => connection.id === id)) {
+ return;
+ }
+ send({ type: 'CONNECT', id, run: () => model.onConnect(id) });
+ },
+ };
+}
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.messages.ts b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.messages.ts
index a601b7bada9..48c0d27c24b 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.messages.ts
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.messages.ts
@@ -3,4 +3,8 @@ export const userProfileEnterpriseAccountsMessages = {
connect: 'Connect',
connectProvider: 'Connect {provider}',
requiresAction: 'Requires action',
+ errors: {
+ generic: 'Something went wrong. Please try again.',
+ missingRedirect: 'The connection could not start. Please try again.',
+ },
} as const;
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts
new file mode 100644
index 00000000000..afb364bc03f
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'vitest';
+
+import { projectEnterpriseAccounts } from './user-profile-enterprise-accounts-section.model';
+
+const accounts = [
+ {
+ id: 'active',
+ enterpriseConnectionId: 'okta',
+ emailAddress: 'alice@example.com',
+ verification: { error: { longMessage: 'Needs attention' } },
+ enterpriseConnection: { active: true, name: 'Acme Okta', logoPublicUrl: 'https://example.com/logo.svg' },
+ },
+ {
+ id: 'inactive',
+ enterpriseConnectionId: 'saml',
+ emailAddress: 'bob@example.com',
+ verification: { error: { longMessage: '' } },
+ enterpriseConnection: { active: false, name: 'Custom SAML' },
+ },
+];
+
+const connections = [
+ { id: 'okta', name: 'Acme Okta', allowOrganizationAccountLinking: true },
+ { id: 'saml', name: 'Custom SAML', allowOrganizationAccountLinking: true },
+ { id: 'other', name: 'Other', allowOrganizationAccountLinking: true },
+ { id: 'forbidden', name: 'Forbidden', allowOrganizationAccountLinking: false },
+];
+
+describe('projectEnterpriseAccounts', () => {
+ it('hides existing accounts and links when enterprise SSO is disabled', () => {
+ expect(projectEnterpriseAccounts({ enabled: false, accounts, connections })).toEqual({ status: 'hidden' });
+ });
+
+ it('uses connection activity for rows and excludes every linked connection', () => {
+ expect(projectEnterpriseAccounts({ enabled: true, accounts, connections })).toEqual({
+ status: 'ready',
+ accounts: [
+ {
+ id: 'active',
+ name: 'Acme Okta',
+ emailAddress: 'alice@example.com',
+ iconUrl: 'https://example.com/logo.svg',
+ requiresAction: true,
+ },
+ ],
+ connections: [{ id: 'other', name: 'Other', iconUrl: undefined }],
+ });
+ });
+
+ it('hides an empty section', () => {
+ expect(projectEnterpriseAccounts({ enabled: true, accounts: [], connections: [] })).toEqual({ status: 'hidden' });
+ });
+});
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
new file mode 100644
index 00000000000..c56257e84f4
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
@@ -0,0 +1,133 @@
+import { isClerkAPIResponseError, isReverificationCancelledError } from '@clerk/shared/error';
+import { appendModalState } from '@clerk/shared/internal/clerk-js/queryStateParams';
+import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate';
+import { __internal_useUserEnterpriseConnections, useClerk, useUser } from '@clerk/shared/react';
+import type { EnterpriseAccountResource, EnterpriseConnectionResource } from '@clerk/shared/types';
+import { useRef } from 'react';
+
+import { useMosaicEnvironment } from '../../../hooks/useMosaicEnvironment';
+import { useMessages } from '../../../localization';
+import { currentInteractionOrigin } from '../../../primitives/utils/interaction-origin';
+import { useReverificationFlow } from '../../reverification';
+import type {
+ UserProfileEnterpriseAccount,
+ UserProfileEnterpriseConnection,
+} from './user-profile-enterprise-accounts-section.types';
+
+type Account = Pick & {
+ verification?: { error?: { longMessage?: string | null } | null } | null;
+ enterpriseConnection: {
+ active: boolean;
+ name: string;
+ logoPublicUrl?: string | null;
+ } | null;
+};
+
+type Connection = Pick & {
+ logoPublicUrl?: string | null;
+};
+
+export type EnterpriseAccountsProjection =
+ | { status: 'hidden' }
+ | { status: 'ready'; accounts: UserProfileEnterpriseAccount[]; connections: UserProfileEnterpriseConnection[] };
+
+export function projectEnterpriseAccounts({
+ enabled,
+ accounts,
+ connections,
+}: {
+ enabled: boolean;
+ accounts: readonly Account[];
+ connections: readonly Connection[];
+}): EnterpriseAccountsProjection {
+ if (!enabled) {
+ return { status: 'hidden' };
+ }
+
+ const linkedConnectionIds = new Set(accounts.map(account => account.enterpriseConnectionId));
+ const visibleAccounts = accounts
+ .filter(account => account.enterpriseConnection?.active)
+ .map(account => ({
+ id: account.id ?? '',
+ name: account.enterpriseConnection?.name ?? '',
+ iconUrl: account.enterpriseConnection?.logoPublicUrl ?? undefined,
+ emailAddress: account.emailAddress,
+ requiresAction: Boolean(account.verification?.error?.longMessage),
+ }));
+ const linkableConnections = connections
+ .filter(connection => connection.allowOrganizationAccountLinking && !linkedConnectionIds.has(connection.id))
+ .map(connection => ({ id: connection.id, name: connection.name, iconUrl: connection.logoPublicUrl ?? undefined }));
+
+ return visibleAccounts.length || linkableConnections.length
+ ? { status: 'ready', accounts: visibleAccounts, connections: linkableConnections }
+ : { status: 'hidden' };
+}
+
+export function useUserProfileEnterpriseAccountsModel({ mode }: { mode?: 'modal' | 'mounted' } = {}) {
+ const clerk = useClerk();
+ const { isLoaded, user } = useUser();
+ const environment = useMosaicEnvironment();
+ const { data: connections = [] } = __internal_useUserEnterpriseConnections({
+ withOrganizationAccountLinking: true,
+ enabled: Boolean(isLoaded && user && environment?.userSettings.enterpriseSSO.enabled),
+ });
+ const messages = useMessages('userProfileEnterpriseAccountsSection');
+ const reverificationFocus = useRef(null);
+ const [createExternalAccount, reverification] = useReverificationFlow((connectionId: string) => {
+ const url = window.location.href;
+ const redirectUrl = mode === 'modal' ? appendModalState({ url, componentName: 'UserProfile' }) : url;
+ return user?.createExternalAccount({ enterpriseConnectionId: connectionId, redirectUrl });
+ });
+
+ if (!isLoaded || !environment) {
+ return { status: 'loading' } as const;
+ }
+ if (!user) {
+ return { status: 'hidden' } as const;
+ }
+
+ const projection = projectEnterpriseAccounts({
+ enabled: environment.userSettings.enterpriseSSO.enabled,
+ accounts: user.enterpriseAccounts,
+ connections,
+ });
+ if (projection.status === 'hidden') {
+ return projection;
+ }
+
+ const connect = async (connectionId: string): Promise<'redirecting' | void> => {
+ if (!projection.connections.some(connection => connection.id === connectionId)) {
+ throw new Error(messages.errors.generic);
+ }
+ const active = document.activeElement;
+ reverificationFocus.current = currentInteractionOrigin() ?? (active instanceof HTMLElement ? active : null);
+
+ let account: Awaited>;
+ try {
+ account = await createExternalAccount(connectionId);
+ } catch (error) {
+ if (isReverificationCancelledError(error)) {
+ return;
+ }
+ if (isClerkAPIResponseError(error)) {
+ const first = error.errors[0];
+ throw new Error(first?.longMessage || first?.message || messages.errors.generic);
+ }
+ throw new Error(messages.errors.generic);
+ }
+ const redirect = account?.verification?.externalVerificationRedirectURL;
+ if (!redirect) {
+ throw new Error(messages.errors.missingRedirect);
+ }
+ if (typeof clerk.__internal_windowNavigate === 'function') {
+ clerk.__internal_windowNavigate(redirect);
+ } else {
+ windowNavigate(redirect);
+ }
+ return 'redirecting';
+ };
+
+ return { ...projection, onConnect: connect, reverification, reverificationFocus };
+}
+
+export type UserProfileEnterpriseAccountsModel = ReturnType;
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx
new file mode 100644
index 00000000000..6091ee560af
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx
@@ -0,0 +1,41 @@
+import type { ReactNode } from 'react';
+
+import { ReverificationDialog } from '../../reverification';
+import { useUserProfileEnterpriseAccountsController } from './user-profile-enterprise-accounts-section.controller';
+import {
+ type UserProfileEnterpriseAccountsModel,
+ useUserProfileEnterpriseAccountsModel,
+} from './user-profile-enterprise-accounts-section.model';
+import { UserProfileEnterpriseAccountsSectionView } from './user-profile-enterprise-accounts-section.view';
+
+type Ready = Extract;
+
+function ReadyEnterpriseAccountsSection({ model }: { model: Ready }) {
+ const controller = useUserProfileEnterpriseAccountsController(model);
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export function UserProfileEnterpriseAccountsSection({
+ fallback,
+ mode,
+}: {
+ fallback?: ReactNode;
+ mode?: 'modal' | 'mounted';
+}) {
+ const model = useUserProfileEnterpriseAccountsModel({ mode });
+ if (model.status === 'loading') {
+ return fallback ?? null;
+ }
+ if (model.status === 'hidden') {
+ return null;
+ }
+ return ;
+}
diff --git a/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx
index ee27358f829..84df2c6ef78 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx
@@ -36,6 +36,7 @@ export interface UserProfileProfilePanelViewProps extends UserProfileAccountSect
/** Connected accounts section. Replaces the one built from `connectedAccounts` and its callbacks. */
connectedAccountsSlot?: ReactNode;
web3WalletsSlot?: ReactNode;
+ enterpriseAccountsSlot?: ReactNode;
onConnectWeb3Wallet?: (id: string) => void;
onSetPrimaryWeb3Wallet?: (id: string) => void;
onRemoveWeb3Wallet?: (id: string) => void | Promise;
@@ -82,6 +83,7 @@ export function UserProfileProfilePanelView({
onRemoveConnectedAccount,
connectedAccountsSlot,
web3WalletsSlot,
+ enterpriseAccountsSlot,
onConnectWeb3Wallet,
onSetPrimaryWeb3Wallet,
onRemoveWeb3Wallet,
@@ -148,6 +150,7 @@ export function UserProfileProfilePanelView({
onSetPrimary={onSetPrimaryWeb3Wallet}
/>
)}
+ {enterpriseAccountsSlot}
{onDeleteAccount ? : null}
diff --git a/packages/mosaic/src/styles/index.ts b/packages/mosaic/src/styles/index.ts
index b069dfb7618..9af3cbd403c 100644
--- a/packages/mosaic/src/styles/index.ts
+++ b/packages/mosaic/src/styles/index.ts
@@ -228,6 +228,7 @@ export { UserProfileMfaSectionView } from '../features/user-profile/user-profile
export { UserProfileMfaSetupView } from '../features/user-profile/user-profile-mfa-setup.view';
export { UserProfileApiKeysPanelView } from '../features/user-profile/user-profile-api-keys-panel.view';
+export { UserProfileEnterpriseAccountsSectionView } from '../features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.view';
export { OrganizationProfileApiKeysPanelView } from '../features/organization-profile/organization-profile-api-keys-panel.view';
export { OrganizationProfileCreateAPIKeyDialog } from '../features/organization-profile/organization-profile-create-api-key.dialog';
From 6c0cb5282cbdaff28f139e87741fa90730ee63e8 Mon Sep 17 00:00:00 2001
From: austincalvelage
Date: Tue, 29 Sep 2026 00:10:31 -0600
Subject: [PATCH 2/7] feat(swingset): add enterprise accounts live sandbox
---
.../swingset/src/app/(clerk)/live-sidebar.tsx | 1 +
.../(clerk)/live/enterprise-accounts/page.tsx | 36 +++++++++++++++++++
2 files changed, 37 insertions(+)
create mode 100644 packages/swingset/src/app/(clerk)/live/enterprise-accounts/page.tsx
diff --git a/packages/swingset/src/app/(clerk)/live-sidebar.tsx b/packages/swingset/src/app/(clerk)/live-sidebar.tsx
index 69aa840738c..bd73ab524e4 100644
--- a/packages/swingset/src/app/(clerk)/live-sidebar.tsx
+++ b/packages/swingset/src/app/(clerk)/live-sidebar.tsx
@@ -21,6 +21,7 @@ import {
const flows = [
{ title: 'Connected accounts', href: '/live/connected-accounts' },
{ title: 'Web3 wallets', href: '/live/web3-wallets' },
+ { title: 'Enterprise accounts', href: '/live/enterprise-accounts' },
{ title: 'Reverification', href: '/live/reverification' },
];
diff --git a/packages/swingset/src/app/(clerk)/live/enterprise-accounts/page.tsx b/packages/swingset/src/app/(clerk)/live/enterprise-accounts/page.tsx
new file mode 100644
index 00000000000..f91ab619133
--- /dev/null
+++ b/packages/swingset/src/app/(clerk)/live/enterprise-accounts/page.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import { UserProfileEnterpriseAccountsSection } from '@clerk/mosaic/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section';
+import { MosaicProvider } from '@clerk/mosaic/MosaicProvider';
+import { useUser } from '@clerk/nextjs';
+import Link from 'next/link';
+
+export default function EnterpriseAccountsLivePage() {
+ const { isLoaded, isSignedIn } = useUser();
+
+ return (
+
+
+
+
Enterprise accounts
+
+ Connect and view the signed-in user's enterprise accounts.
+
+
+ {!isLoaded ?
Loading…
: null}
+ {isLoaded && !isSignedIn ? (
+
+
+ Sign in
+ {' '}
+ to manage enterprise accounts.
+
+ ) : null}
+ {isLoaded && isSignedIn ?
: null}
+
+
+ );
+}
From 66e7657fcf9bc5403614753c365962365a5d0e60 Mon Sep 17 00:00:00 2001
From: austincalvelage
Date: Tue, 29 Sep 2026 09:50:37 -0600
Subject: [PATCH 3/7] refactor(mosaic): remove enterprise account
reverification flow
---
...rise-accounts-section.integration.test.tsx | 85 +++++--------------
...file-enterprise-accounts-section.model.tsx | 25 ++----
...er-profile-enterprise-accounts-section.tsx | 11 +--
3 files changed, 30 insertions(+), 91 deletions(-)
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
index 1b188b8e36f..91ea8f09faa 100644
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
@@ -1,11 +1,10 @@
import { ClerkAPIResponseError } from '@clerk/shared/error';
import type * as SharedReact from '@clerk/shared/react';
import { createDeferredPromise } from '@clerk/shared/utils';
-import { render, screen, waitFor, within } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { useReverification as useReverificationSource } from '../../../../../shared/src/react/hooks/useReverification';
import { MosaicProvider } from '../../../MosaicProvider';
import { UserProfileEnterpriseAccountsSection } from '../user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section';
@@ -16,24 +15,9 @@ let enterpriseAccounts: Record[];
let connections: Record[] | undefined;
let createExternalAccount: ReturnType;
let navigate: ReturnType | undefined;
-let setActive: ReturnType;
-let startVerification: ReturnType;
-let attemptFirstFactorVerification: ReturnType;
let fallbackNavigate: ReturnType;
let queryOptions: { enabled?: boolean; withOrganizationAccountLinking?: boolean } | undefined;
-const reverificationHint = {
- clerk_error: {
- type: 'forbidden',
- reason: 'reverification-error',
- metadata: { reverification: { level: 'first_factor', afterMinutes: 1 } },
- },
-};
-
-vi.mock('../../../../../shared/src/react/hooks/useClerk', () => ({
- useClerk: () => ({ __internal_openReverification: vi.fn(), telemetry: { record: vi.fn() } }),
-}));
-
vi.mock('@clerk/shared/internal/clerk-js/windowNavigate', () => ({
windowNavigate: (url: URL | string) => fallbackNavigate(url),
}));
@@ -57,16 +41,7 @@ vi.mock('@clerk/shared/react', async importOriginal => {
},
__internal_windowNavigate: navigate,
__internal_getOption: () => undefined,
- setActive,
}),
- useSession: () => ({
- session: {
- id: 'session_1',
- startVerification,
- attemptFirstFactorVerification,
- },
- }),
- useReverification: useReverificationSource,
};
});
@@ -112,12 +87,6 @@ describe('UserProfileEnterpriseAccountsSection', () => {
connections = [connection('okta'), connection('saml')];
createExternalAccount = vi.fn().mockResolvedValue(response());
navigate = vi.fn();
- setActive = vi.fn().mockResolvedValue(undefined);
- startVerification = vi.fn().mockResolvedValue({
- status: 'needs_first_factor',
- supportedFirstFactors: [{ strategy: 'password' }],
- });
- attemptFirstFactorVerification = vi.fn().mockResolvedValue({ status: 'complete' });
fallbackNavigate = vi.fn();
queryOptions = undefined;
});
@@ -261,59 +230,51 @@ describe('UserProfileEnterpriseAccountsSection', () => {
await waitFor(() => expect(createExternalAccount).toHaveBeenCalledTimes(2));
});
- it('runs a real Mosaic reverification challenge, then retries the same connection', async () => {
- createExternalAccount = vi
- .fn()
- .mockRejectedValueOnce(
- new ClerkAPIResponseError('reverification required', {
- data: [{ code: 'session_reverification_required', message: 'Verify your session.' }],
- status: 403,
- }),
- )
- .mockResolvedValueOnce(response());
+ it('shows a direct API failure without opening verification or retrying', async () => {
+ createExternalAccount = vi.fn().mockRejectedValueOnce(
+ new ClerkAPIResponseError('reverification required', {
+ data: [{ code: 'session_reverification_required', message: 'Verify your session.' }],
+ status: 403,
+ }),
+ );
const user = userEvent.setup();
renderSection();
await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- const dialog = await screen.findByRole('dialog', { name: 'Verification required' });
- await user.type(within(dialog).getByLabelText('Password'), 'secret');
- await user.click(within(dialog).getByRole('button', { name: 'Continue' }));
- await waitFor(() => expect(createExternalAccount).toHaveBeenCalledTimes(2));
- expect(createExternalAccount.mock.calls[0]?.[0]).toEqual(createExternalAccount.mock.calls[1]?.[0]);
- expect(attemptFirstFactorVerification).toHaveBeenCalledWith({ strategy: 'password', password: 'secret' });
- expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize'));
+ expect(await screen.findByRole('alert')).toHaveTextContent('Verify your session.');
+ expect(createExternalAccount).toHaveBeenCalledOnce();
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ expect(navigate).not.toHaveBeenCalled();
});
- it('shows a failed reverification retry on the selected row', async () => {
+ it('waits for a manual retry after a failed direct request', async () => {
createExternalAccount = vi
.fn()
- .mockResolvedValueOnce(reverificationHint)
.mockRejectedValueOnce(
new ClerkAPIResponseError('failed', {
data: [{ code: 'enterprise_error', message: 'failed', long_message: 'Account linking failed.' }],
status: 422,
}),
- );
+ )
+ .mockResolvedValueOnce(response());
const user = userEvent.setup();
renderSection();
await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- const dialog = await screen.findByRole('dialog', { name: 'Verification required' });
- await user.type(within(dialog).getByLabelText('Password'), 'secret');
- await user.click(within(dialog).getByRole('button', { name: 'Continue' }));
expect(await screen.findByRole('alert')).toHaveTextContent('Account linking failed.');
+ expect(createExternalAccount).toHaveBeenCalledOnce();
expect(navigate).not.toHaveBeenCalled();
expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(createExternalAccount).toHaveBeenCalledTimes(2));
+ expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize'));
});
- it('cancels a real Mosaic reverification challenge without showing an error', async () => {
- createExternalAccount = vi.fn().mockResolvedValue(reverificationHint);
+ it('shows a generic direct request error without opening a dialog', async () => {
+ createExternalAccount = vi.fn().mockRejectedValue(new Error('Network failed'));
const user = userEvent.setup();
renderSection();
await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- const dialog = await screen.findByRole('dialog', { name: 'Verification required' });
- await user.click(within(dialog).getByRole('button', { name: 'Close' }));
- await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
- await waitFor(() => expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled());
- expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ expect(await screen.findByRole('alert')).toHaveTextContent('Something went wrong. Please try again.');
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(createExternalAccount).toHaveBeenCalledOnce();
});
});
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
index c56257e84f4..4beb50d8270 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
@@ -1,14 +1,11 @@
-import { isClerkAPIResponseError, isReverificationCancelledError } from '@clerk/shared/error';
+import { isClerkAPIResponseError } from '@clerk/shared/error';
import { appendModalState } from '@clerk/shared/internal/clerk-js/queryStateParams';
import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate';
import { __internal_useUserEnterpriseConnections, useClerk, useUser } from '@clerk/shared/react';
import type { EnterpriseAccountResource, EnterpriseConnectionResource } from '@clerk/shared/types';
-import { useRef } from 'react';
import { useMosaicEnvironment } from '../../../hooks/useMosaicEnvironment';
import { useMessages } from '../../../localization';
-import { currentInteractionOrigin } from '../../../primitives/utils/interaction-origin';
-import { useReverificationFlow } from '../../reverification';
import type {
UserProfileEnterpriseAccount,
UserProfileEnterpriseConnection,
@@ -72,12 +69,6 @@ export function useUserProfileEnterpriseAccountsModel({ mode }: { mode?: 'modal'
enabled: Boolean(isLoaded && user && environment?.userSettings.enterpriseSSO.enabled),
});
const messages = useMessages('userProfileEnterpriseAccountsSection');
- const reverificationFocus = useRef(null);
- const [createExternalAccount, reverification] = useReverificationFlow((connectionId: string) => {
- const url = window.location.href;
- const redirectUrl = mode === 'modal' ? appendModalState({ url, componentName: 'UserProfile' }) : url;
- return user?.createExternalAccount({ enterpriseConnectionId: connectionId, redirectUrl });
- });
if (!isLoaded || !environment) {
return { status: 'loading' } as const;
@@ -99,16 +90,12 @@ export function useUserProfileEnterpriseAccountsModel({ mode }: { mode?: 'modal'
if (!projection.connections.some(connection => connection.id === connectionId)) {
throw new Error(messages.errors.generic);
}
- const active = document.activeElement;
- reverificationFocus.current = currentInteractionOrigin() ?? (active instanceof HTMLElement ? active : null);
-
- let account: Awaited>;
+ const url = window.location.href;
+ const redirectUrl = mode === 'modal' ? appendModalState({ url, componentName: 'UserProfile' }) : url;
+ let account: Awaited>;
try {
- account = await createExternalAccount(connectionId);
+ account = await user.createExternalAccount({ enterpriseConnectionId: connectionId, redirectUrl });
} catch (error) {
- if (isReverificationCancelledError(error)) {
- return;
- }
if (isClerkAPIResponseError(error)) {
const first = error.errors[0];
throw new Error(first?.longMessage || first?.message || messages.errors.generic);
@@ -127,7 +114,7 @@ export function useUserProfileEnterpriseAccountsModel({ mode }: { mode?: 'modal'
return 'redirecting';
};
- return { ...projection, onConnect: connect, reverification, reverificationFocus };
+ return { ...projection, onConnect: connect };
}
export type UserProfileEnterpriseAccountsModel = ReturnType;
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx
index 6091ee560af..0df423b5093 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx
@@ -1,6 +1,5 @@
import type { ReactNode } from 'react';
-import { ReverificationDialog } from '../../reverification';
import { useUserProfileEnterpriseAccountsController } from './user-profile-enterprise-accounts-section.controller';
import {
type UserProfileEnterpriseAccountsModel,
@@ -12,15 +11,7 @@ type Ready = Extract;
function ReadyEnterpriseAccountsSection({ model }: { model: Ready }) {
const controller = useUserProfileEnterpriseAccountsController(model);
- return (
- <>
-
-
- >
- );
+ return ;
}
export function UserProfileEnterpriseAccountsSection({
From 05d8795f10520e64d8f1170b02dddf089dda8da8 Mon Sep 17 00:00:00 2001
From: austincalvelage
Date: Tue, 29 Sep 2026 10:09:35 -0600
Subject: [PATCH 4/7] test(mosaic): check enterprise ordering against main
danger zone
---
.../user-profile-profile-panel.view.test.tsx | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx
index fcef008fee8..2ae436f3c09 100644
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx
@@ -309,15 +309,17 @@ describe('UserProfileProfilePanelView', () => {
});
it('places enterprise accounts after Web3 wallets and before the danger zone', () => {
- const { container } = renderView({
+ renderView({
web3WalletsSlot: Wallets
,
enterpriseAccountsSlot: Enterprise accounts
,
- deleteAccountSlot: Danger zone
,
+ onDeleteAccount: () => Promise.resolve(),
});
- expect(
- Array.from(container.querySelectorAll('[data-testid]')).map(element => element.getAttribute('data-testid')),
- ).toEqual(['wallets', 'enterprise', 'danger']);
+ const wallets = screen.getByTestId('wallets');
+ const enterprise = screen.getByTestId('enterprise');
+ const danger = screen.getByRole('heading', { name: 'Danger zone' });
+ expect(wallets.compareDocumentPosition(enterprise) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ expect(enterprise.compareDocumentPosition(danger) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it('renders connected provider and Web3 images inside icon frames', () => {
From 29bc6c5d3f32386f9bfef49e6737b62f36291616 Mon Sep 17 00:00:00 2001
From: austincalvelage
Date: Tue, 29 Sep 2026 11:23:00 -0600
Subject: [PATCH 5/7] test(mosaic): cover enterprise accounts through FAPI
---
.../mosaic/src/__tests__/feature/fake-fapi.ts | 30 +++
packages/mosaic/src/__tests__/feature/fapi.ts | 21 ++
...rise-accounts-section.integration.test.tsx | 187 +--------------
...-enterprise-accounts-section.view.test.tsx | 89 -------
...ofile-enterprise-accounts.feature.test.tsx | 223 ++++++++++++++++++
...prise-accounts-section.controller.test.tsx | 17 +-
6 files changed, 276 insertions(+), 291 deletions(-)
delete mode 100644 packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx
create mode 100644 packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx
diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi.ts b/packages/mosaic/src/__tests__/feature/fake-fapi.ts
index 3b52c224807..20e7360ba9b 100644
--- a/packages/mosaic/src/__tests__/feature/fake-fapi.ts
+++ b/packages/mosaic/src/__tests__/feature/fake-fapi.ts
@@ -1,6 +1,7 @@
import { OAUTH_PROVIDERS } from '@clerk/shared/oauth';
import type {
ClientJSON,
+ EnterpriseConnectionJSON,
OAuthProvider,
OrganizationMembershipJSON,
OrganizationSuggestionJSON,
@@ -36,6 +37,7 @@ export interface FakeFapiState {
memberships: OrganizationMembershipJSON[];
invitations: UserOrganizationInvitationJSON[];
suggestions: OrganizationSuggestionJSON[];
+ enterpriseConnections: EnterpriseConnectionJSON[];
}
export type FakeFapiSeed = Partial;
@@ -114,6 +116,7 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState {
memberships: [],
invitations: [],
suggestions: [],
+ enterpriseConnections: [],
...seed,
};
@@ -124,12 +127,39 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState {
const user = activeUser(state);
return user ? envelope(user, state.client) : missing();
}),
+ http.get(fapiUrl('/v1/me/enterprise_connections'), ({ request }) => {
+ const url = new URL(request.url);
+ const withLinking = url.searchParams.get('with_organization_account_linking') === 'true';
+ const connections = withLinking
+ ? state.enterpriseConnections.filter(connection => connection.allow_organization_account_linking)
+ : state.enterpriseConnections;
+ return envelope(connections, state.client);
+ }),
http.post(fapiUrl('/v1/me/external_accounts'), async ({ request }) => {
const user = activeUser(state);
if (!user) {
return missing();
}
const body = new URLSearchParams(await request.text());
+ const enterpriseConnectionId = body.get('enterprise_connection_id');
+ if (enterpriseConnectionId) {
+ const connection = state.enterpriseConnections.find(item => item.id === enterpriseConnectionId);
+ if (!connection) {
+ return missing();
+ }
+ return envelope(
+ fapiExternalAccount({
+ id: `idn_${connection.id}`,
+ provider: 'google',
+ verification: fapiVerification({
+ status: 'unverified',
+ strategy: 'enterprise_sso',
+ external_verification_redirect_url: 'https://accounts.example/enterprise-authorize',
+ }),
+ }),
+ state.client,
+ );
+ }
const strategy = body.get('strategy');
if (!strategy) {
return missing();
diff --git a/packages/mosaic/src/__tests__/feature/fapi.ts b/packages/mosaic/src/__tests__/feature/fapi.ts
index fca1f533de5..42a2fda4f92 100644
--- a/packages/mosaic/src/__tests__/feature/fapi.ts
+++ b/packages/mosaic/src/__tests__/feature/fapi.ts
@@ -6,6 +6,7 @@ import type {
DisplayConfigJSON,
EmailAddressJSON,
EnterpriseAccountJSON,
+ EnterpriseConnectionJSON,
EnvironmentJSON,
ExternalAccountJSON,
OAuthProviders,
@@ -332,6 +333,26 @@ export function fapiWeb3Wallet(
};
}
+export function fapiEnterpriseConnection(
+ overrides: Partial & Pick,
+): EnterpriseConnectionJSON {
+ return {
+ object: 'enterprise_connection',
+ name: overrides.id,
+ active: true,
+ provider: 'saml_okta',
+ logo_public_url: null,
+ domains: [],
+ organization_id: null,
+ sync_user_attributes: false,
+ disable_additional_identifications: false,
+ allow_organization_account_linking: true,
+ created_at: createdAt,
+ updated_at: createdAt,
+ ...overrides,
+ };
+}
+
export function fapiUser(overrides: Partial & Pick): UserJSON {
return {
object: 'user',
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
index 91ea8f09faa..72346d33dd5 100644
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
@@ -1,6 +1,4 @@
-import { ClerkAPIResponseError } from '@clerk/shared/error';
import type * as SharedReact from '@clerk/shared/react';
-import { createDeferredPromise } from '@clerk/shared/utils';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
@@ -16,7 +14,6 @@ let connections: Record[] | undefined;
let createExternalAccount: ReturnType;
let navigate: ReturnType | undefined;
let fallbackNavigate: ReturnType;
-let queryOptions: { enabled?: boolean; withOrganizationAccountLinking?: boolean } | undefined;
vi.mock('@clerk/shared/internal/clerk-js/windowNavigate', () => ({
windowNavigate: (url: URL | string) => fallbackNavigate(url),
@@ -30,8 +27,7 @@ vi.mock('@clerk/shared/react', async importOriginal => {
isLoaded,
user: signedIn ? { id: 'user_1', enterpriseAccounts, createExternalAccount } : null,
}),
- __internal_useUserEnterpriseConnections: (options: typeof queryOptions) => {
- queryOptions = options;
+ __internal_useUserEnterpriseConnections: () => {
return { data: connections };
},
useClerk: () => ({
@@ -55,17 +51,6 @@ function connection(id: string, overrides: Record = {}) {
};
}
-function account(id: string, connectionId: string, overrides: Record = {}) {
- return {
- id,
- enterpriseConnectionId: connectionId,
- emailAddress: `${id}@example.com`,
- enterpriseConnection: { active: true, name: connectionId, logoPublicUrl: null },
- verification: { error: null },
- ...overrides,
- };
-}
-
function response(url = 'https://accounts.example/authorize') {
return { verification: { externalVerificationRedirectURL: new URL(url) } };
}
@@ -88,83 +73,6 @@ describe('UserProfileEnterpriseAccountsSection', () => {
createExternalAccount = vi.fn().mockResolvedValue(response());
navigate = vi.fn();
fallbackNavigate = vi.fn();
- queryOptions = undefined;
- });
-
- it('hides data when SSO is disabled and handles loading, signed-out, and empty states', () => {
- enterpriseAccounts = [account('linked', 'okta')];
- enabled = false;
- const { container, rerender } = renderSection({ fallback: Loading accounts
});
- expect(container).toBeEmptyDOMElement();
- expect(queryOptions).toEqual({ enabled: false, withOrganizationAccountLinking: true });
-
- enabled = true;
- isLoaded = false;
- rerender(
-
- Loading accounts} />
- ,
- );
- expect(screen.getByText('Loading accounts')).toBeInTheDocument();
- expect(queryOptions?.enabled).toBe(false);
-
- isLoaded = true;
- signedIn = false;
- rerender(
-
-
- ,
- );
- expect(container).toBeEmptyDOMElement();
- expect(queryOptions?.enabled).toBe(false);
-
- signedIn = true;
- enterpriseAccounts = [];
- connections = [];
- rerender(
-
-
- ,
- );
- expect(container).toBeEmptyDOMElement();
- expect(queryOptions?.enabled).toBe(true);
- });
-
- it('shows rows by connection activity and only offers organization-linkable, unlinked connections', () => {
- enterpriseAccounts = [
- account('linked', 'okta', { active: false, verification: { error: { longMessage: 'Fix this account' } } }),
- account('inactive', 'saml', { active: true, enterpriseConnection: { active: false, name: 'Custom SAML' } }),
- ];
- connections = [
- connection('okta'),
- connection('saml'),
- connection('other', { allowOrganizationAccountLinking: false }),
- ];
- renderSection();
- expect(screen.getByText('linked@example.com')).toBeInTheDocument();
- expect(screen.getByText('Requires action')).toBeInTheDocument();
- expect(screen.queryByText('inactive@example.com')).not.toBeInTheDocument();
- expect(screen.queryByRole('button', { name: /^Connect/ })).not.toBeInTheDocument();
- });
-
- it('keeps linked rows while the connection query is unresolved', () => {
- enterpriseAccounts = [account('linked', 'okta')];
- connections = undefined;
- renderSection();
- expect(screen.getByText('linked@example.com')).toBeInTheDocument();
- });
-
- it('connects with the current URL and navigates using Clerk', async () => {
- const user = userEvent.setup();
- renderSection();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- await waitFor(() =>
- expect(createExternalAccount).toHaveBeenCalledWith({
- enterpriseConnectionId: 'okta',
- redirectUrl: window.location.href,
- }),
- );
- expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize'));
});
it('uses the shared navigation fallback when the Clerk instance lacks the internal method', async () => {
@@ -175,99 +83,6 @@ describe('UserProfileEnterpriseAccountsSection', () => {
await waitFor(() => expect(fallbackNavigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize')));
});
- it('holds the selected row pending and disables competing actions while connecting', async () => {
- const operation = createDeferredPromise();
- createExternalAccount = vi.fn(() => operation.promise);
- const user = userEvent.setup();
- renderSection();
- const okta = screen.getByRole('button', { name: 'Connect Acme Okta' });
- const saml = screen.getByRole('button', { name: 'Connect Custom SAML' });
- await user.click(okta);
- expect(okta).toHaveAttribute('aria-busy', 'true');
- expect(saml).toBeDisabled();
- await user.click(saml);
- expect(createExternalAccount).toHaveBeenCalledOnce();
- operation.resolve(response());
- await waitFor(() => expect(navigate).toHaveBeenCalledOnce());
- expect(okta).toHaveAttribute('aria-busy', 'true');
- });
-
- it('preserves the UserProfile modal return state', async () => {
- const user = userEvent.setup();
- renderSection({ mode: 'modal' });
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- await waitFor(() => expect(createExternalAccount).toHaveBeenCalledOnce());
- const redirectUrl = createExternalAccount.mock.calls[0]?.[0]?.redirectUrl;
- const encoded = new URL(redirectUrl).searchParams.get('__clerk_modal_state');
- expect(encoded).toBeTruthy();
- expect(JSON.parse(window.atob(encoded))).toMatchObject({ componentName: 'UserProfile' });
- });
-
- it('shows a recoverable error when verification has no redirect', async () => {
- createExternalAccount = vi.fn().mockResolvedValue({ verification: null });
- const user = userEvent.setup();
- renderSection();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- expect(await screen.findByRole('alert')).toHaveTextContent('The connection could not start. Please try again.');
- expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
- });
-
- it('shows the API long message and allows retry', async () => {
- createExternalAccount = vi
- .fn()
- .mockRejectedValueOnce(
- new ClerkAPIResponseError('failed', {
- data: [{ code: 'enterprise_error', message: 'failed', long_message: 'Acme is unavailable.' }],
- status: 422,
- }),
- )
- .mockResolvedValueOnce(response());
- const user = userEvent.setup();
- renderSection();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- expect(await screen.findByRole('alert')).toHaveTextContent('Acme is unavailable.');
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- await waitFor(() => expect(createExternalAccount).toHaveBeenCalledTimes(2));
- });
-
- it('shows a direct API failure without opening verification or retrying', async () => {
- createExternalAccount = vi.fn().mockRejectedValueOnce(
- new ClerkAPIResponseError('reverification required', {
- data: [{ code: 'session_reverification_required', message: 'Verify your session.' }],
- status: 403,
- }),
- );
- const user = userEvent.setup();
- renderSection();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- expect(await screen.findByRole('alert')).toHaveTextContent('Verify your session.');
- expect(createExternalAccount).toHaveBeenCalledOnce();
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
- expect(navigate).not.toHaveBeenCalled();
- });
-
- it('waits for a manual retry after a failed direct request', async () => {
- createExternalAccount = vi
- .fn()
- .mockRejectedValueOnce(
- new ClerkAPIResponseError('failed', {
- data: [{ code: 'enterprise_error', message: 'failed', long_message: 'Account linking failed.' }],
- status: 422,
- }),
- )
- .mockResolvedValueOnce(response());
- const user = userEvent.setup();
- renderSection();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- expect(await screen.findByRole('alert')).toHaveTextContent('Account linking failed.');
- expect(createExternalAccount).toHaveBeenCalledOnce();
- expect(navigate).not.toHaveBeenCalled();
- expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- await waitFor(() => expect(createExternalAccount).toHaveBeenCalledTimes(2));
- expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize'));
- });
-
it('shows a generic direct request error without opening a dialog', async () => {
createExternalAccount = vi.fn().mockRejectedValue(new Error('Network failed'));
const user = userEvent.setup();
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx
deleted file mode 100644
index d265a034b6d..00000000000
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.view.test.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { describe, expect, it, vi } from 'vitest';
-
-import { UserProfileEnterpriseAccountsSectionView } from '../user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.view';
-
-describe('UserProfileEnterpriseAccountsSectionView', () => {
- it.each([{ connections: [] }, { connections: [{ id: 'okta', name: 'Acme Okta' }] }])(
- 'renders nothing without accounts or actionable connections (%j)',
- ({ connections }) => {
- const { container } = render(
- ,
- );
- expect(container).toBeEmptyDOMElement();
- },
- );
-
- it('offers a keyboard-accessible Connect button for each available connection', async () => {
- const user = userEvent.setup();
- const onConnect = vi.fn();
- render(
- ,
- );
- const button = screen.getByRole('button', { name: 'Connect SSO' });
- expect(screen.getByText('SSO')).toBeVisible();
- expect(screen.getByText('S', { exact: true })).toBeInTheDocument();
- expect(screen.queryByRole('menu')).not.toBeInTheDocument();
- button.focus();
- await user.keyboard('{Enter}');
- expect(onConnect).toHaveBeenCalledExactlyOnceWith('sso');
- });
-
- it('blocks connection actions while pending and enables retry with a row error', async () => {
- const user = userEvent.setup();
- const onConnect = vi.fn();
- const connections = [
- { id: 'okta', name: 'Acme Okta' },
- { id: 'saml', name: 'Custom SAML' },
- ];
- const { rerender } = render(
- ,
- );
- expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toHaveAttribute('aria-busy', 'true');
- expect(screen.getByRole('button', { name: 'Connect Custom SAML' })).toBeDisabled();
- await user.click(screen.getByRole('button', { name: 'Connect Custom SAML' }));
- expect(onConnect).not.toHaveBeenCalled();
- rerender(
- ,
- );
- expect(screen.getByRole('alert')).toHaveTextContent('Unable to connect');
- expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
- expect(screen.getByRole('button', { name: 'Connect Custom SAML' })).toBeEnabled();
- });
-
- it('renders linked account identity and requires-action status from plain props', () => {
- render(
- ,
- );
- expect(screen.getByRole('region', { name: 'Enterprise accounts' })).toBeInTheDocument();
- expect(screen.getByText('Acme Okta')).toBeInTheDocument();
- expect(screen.getByText('test@acme.com')).toBeInTheDocument();
- expect(screen.getByText('Requires action')).toBeInTheDocument();
- expect(screen.getByText('A', { exact: true })).toBeInTheDocument();
- expect(screen.getByText('Custom SSO')).toBeVisible();
- expect(screen.queryByRole('img')).not.toBeInTheDocument();
- expect(screen.queryByRole('button')).not.toBeInTheDocument();
- });
-});
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx
new file mode 100644
index 00000000000..3fe37483e83
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx
@@ -0,0 +1,223 @@
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { http, HttpResponse } from 'msw';
+import { describe, expect, it, vi } from 'vitest';
+
+import { type FakeFapiSeed, fapiUrl, holdRequests, serveFapi, worker } from '../../../__tests__/feature/fake-fapi';
+import {
+ fapiClient,
+ fapiEnterpriseAccount,
+ fapiEnterpriseConnection,
+ fapiEnvironment,
+ fapiExternalAccount,
+ fapiSession,
+ fapiUser,
+ fapiVerification,
+} from '../../../__tests__/feature/fapi';
+import { renderWithClerk } from '../../../__tests__/feature/render';
+import { UserProfileEnterpriseAccountsSection } from '../user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section';
+
+const okta = fapiEnterpriseConnection({ id: 'okta', name: 'Acme Okta' });
+const custom = fapiEnterpriseConnection({ id: 'saml', name: 'Custom SAML' });
+
+function signedIn(overrides: FakeFapiSeed = {}) {
+ return {
+ client: fapiClient([fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_1' }) })]),
+ environment: fapiEnvironment({
+ user_settings: { enterprise_sso: { enabled: true, self_serve_sso: false, self_serve_directory_sync: false } },
+ }),
+ enterpriseConnections: [okta, custom],
+ ...overrides,
+ };
+}
+
+async function renderSection(seed: FakeFapiSeed = signedIn()) {
+ const fapi = serveFapi(seed);
+ const view = await renderWithClerk();
+ return { ...view, fapi };
+}
+
+describe('enterprise accounts', () => {
+ it('shows the fallback while Clerk loads', async () => {
+ serveFapi(signedIn());
+ const client = holdRequests('get', '/v1/client');
+ const rendering = renderWithClerk(Loading accounts} />);
+
+ await waitFor(() => expect(client.requests).toHaveLength(1));
+ expect(screen.getByText('Loading accounts')).toBeInTheDocument();
+ client.release();
+ await rendering;
+ expect(await screen.findByRole('button', { name: 'Connect Acme Okta' })).toBeInTheDocument();
+ });
+
+ it('hides the section when SSO is disabled', async () => {
+ await renderSection(signedIn({ environment: fapiEnvironment() }));
+ expect(screen.queryByRole('region', { name: 'Enterprise accounts' })).toBeNull();
+ });
+
+ it('hides the section when the user is signed out', async () => {
+ await renderSection(signedIn({ client: fapiClient() }));
+ expect(screen.queryByRole('region', { name: 'Enterprise accounts' })).toBeNull();
+ });
+
+ it('hides an empty section', async () => {
+ await renderSection(signedIn({ enterpriseConnections: [] }));
+ expect(screen.queryByRole('region', { name: 'Enterprise accounts' })).toBeNull();
+ });
+
+ it('shows only unlinked, organization-linkable connections', async () => {
+ const nonLinkable = fapiEnterpriseConnection({
+ id: 'other',
+ name: 'Other SAML',
+ allow_organization_account_linking: false,
+ });
+ await renderSection(signedIn({ enterpriseConnections: [okta, custom, nonLinkable] }));
+
+ expect(await screen.findByRole('button', { name: 'Connect Acme Okta' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Connect Custom SAML' })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Connect Other SAML' })).toBeNull();
+ });
+
+ it('shows linked accounts with errors and offers only unlinked connections', async () => {
+ const linked = fapiEnterpriseAccount({
+ id: 'enterprise_1',
+ enterprise_connection_id: 'okta',
+ email_address: 'linked@example.com',
+ verification: fapiVerification({
+ strategy: 'enterprise_sso',
+ error: { code: 'enterprise_error', message: 'Fix this account', long_message: 'Fix this account' },
+ }),
+ });
+ const client = fapiClient([
+ fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_1', enterprise_accounts: [linked] }) }),
+ ]);
+ await renderSection(signedIn({ client }));
+
+ expect(screen.getByText('linked@example.com')).toBeInTheDocument();
+ expect(screen.getByText('Requires action')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Connect Acme Okta' })).toBeNull();
+ expect(await screen.findByRole('button', { name: 'Connect Custom SAML' })).toBeInTheDocument();
+ });
+
+ it('keeps linked rows visible while connections load', async () => {
+ const linked = fapiEnterpriseAccount({ id: 'enterprise_1', email_address: 'linked@example.com' });
+ const client = fapiClient([
+ fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_1', enterprise_accounts: [linked] }) }),
+ ]);
+ serveFapi(signedIn({ client }));
+ const connections = holdRequests('get', '/v1/me/enterprise_connections');
+ const rendering = renderWithClerk();
+
+ expect(await screen.findByText('linked@example.com')).toBeInTheDocument();
+ await waitFor(() => expect(connections.requests).toHaveLength(1));
+ connections.release();
+ await rendering;
+ });
+
+ it('sends the selected connection and current URL, then opens its redirect', async () => {
+ const { clerk } = await renderSection();
+ const navigate = vi.spyOn(clerk, '__internal_windowNavigate').mockImplementation(() => {});
+ const request = holdRequests('post', '/v1/me/external_accounts');
+ const user = userEvent.setup();
+
+ const connect = await screen.findByRole('button', { name: 'Connect Acme Okta' });
+ connect.focus();
+ await user.keyboard('{Enter}');
+ await waitFor(() => expect(request.requests).toHaveLength(1));
+ const body = new URLSearchParams(await request.requests[0]?.text());
+ expect(body.get('enterprise_connection_id')).toBe('okta');
+ expect(body.get('redirect_url')).toBe(window.location.href);
+ expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toHaveAttribute('aria-busy', 'true');
+ expect(screen.getByRole('button', { name: 'Connect Custom SAML' })).toBeDisabled();
+ await user.click(screen.getByRole('button', { name: 'Connect Custom SAML' }));
+ expect(request.requests).toHaveLength(1);
+
+ request.release();
+ await waitFor(() =>
+ expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/enterprise-authorize')),
+ );
+ });
+
+ it('preserves modal return state in the redirect URL', async () => {
+ serveFapi(signedIn());
+ await renderWithClerk();
+ const request = holdRequests('post', '/v1/me/external_accounts');
+
+ await userEvent.setup().click(await screen.findByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(request.requests).toHaveLength(1));
+ const body = new URLSearchParams(await request.requests[0]?.text());
+ const encoded = new URL(body.get('redirect_url') || '').searchParams.get('__clerk_modal_state');
+ expect(encoded).toBeTruthy();
+ expect(JSON.parse(window.atob(encoded || ''))).toMatchObject({ componentName: 'UserProfile' });
+ request.fail();
+ });
+
+ it('shows a missing redirect error and allows another attempt', async () => {
+ serveFapi(signedIn());
+ worker.use(
+ http.post(fapiUrl('/v1/me/external_accounts'), () =>
+ HttpResponse.json({
+ response: fapiExternalAccount({
+ id: 'idn_okta',
+ provider: 'google',
+ verification: fapiVerification({ status: 'unverified', strategy: 'enterprise_sso' }),
+ }),
+ client: null,
+ }),
+ ),
+ );
+ const { clerk } = await renderWithClerk();
+ const navigate = vi.spyOn(clerk, '__internal_windowNavigate').mockImplementation(() => {});
+ const user = userEvent.setup();
+
+ await user.click(await screen.findByRole('button', { name: 'Connect Acme Okta' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent('The connection could not start. Please try again.');
+ expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
+ serveFapi(signedIn());
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() =>
+ expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/enterprise-authorize')),
+ );
+ });
+
+ it('shows the API long message and succeeds after a manual retry', async () => {
+ const { clerk } = await renderSection();
+ const navigate = vi.spyOn(clerk, '__internal_windowNavigate').mockImplementation(() => {});
+ const request = holdRequests('post', '/v1/me/external_accounts');
+ const user = userEvent.setup();
+
+ await user.click(await screen.findByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(request.requests).toHaveLength(1));
+ request.fail('enterprise_error', 'Acme is unavailable.');
+ expect(await screen.findByRole('alert')).toHaveTextContent('Acme is unavailable.');
+ expect(navigate).not.toHaveBeenCalled();
+ serveFapi(signedIn());
+ await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() =>
+ expect(navigate).toHaveBeenCalledWith(new URL('https://accounts.example/enterprise-authorize')),
+ );
+ });
+
+ it('shows a direct verification-required API error without opening a dialog', async () => {
+ await renderSection();
+ const request = holdRequests('post', '/v1/me/external_accounts');
+
+ await userEvent.setup().click(await screen.findByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(request.requests).toHaveLength(1));
+ request.fail('session_reverification_required', 'Verify your session.');
+ expect(await screen.findByRole('alert')).toHaveTextContent('Verify your session.');
+ expect(screen.queryByRole('dialog')).toBeNull();
+ expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
+ });
+
+ it('reports a network failure without opening a dialog', async () => {
+ serveFapi(signedIn());
+ worker.use(http.post(fapiUrl('/v1/me/external_accounts'), () => HttpResponse.error()));
+ await renderWithClerk();
+
+ await userEvent.setup().click(await screen.findByRole('button', { name: 'Connect Acme Okta' }));
+ expect(await screen.findByRole('alert')).toHaveTextContent('The connection could not start. Please try again.');
+ expect(screen.queryByRole('dialog')).toBeNull();
+ expect(screen.getByRole('button', { name: 'Connect Acme Okta' })).toBeEnabled();
+ });
+});
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx
index 2ec0d1151c5..6492dbb661c 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.controller.test.tsx
@@ -1,4 +1,4 @@
-import { act, renderHook, waitFor } from '@testing-library/react';
+import { act, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useUserProfileEnterpriseAccountsController } from './user-profile-enterprise-accounts-section.controller';
@@ -43,21 +43,6 @@ describe('useUserProfileEnterpriseAccountsController', () => {
expect(result.current.pendingConnectionId).toBe('okta');
});
- it('returns to idle with a row error on failure and permits a retry', async () => {
- const onConnect = vi
- .fn()
- .mockRejectedValueOnce(new Error('Network unavailable'))
- .mockResolvedValueOnce('redirecting');
- const { result } = renderHook(() =>
- useUserProfileEnterpriseAccountsController({ status: 'ready', accounts: [], connections, onConnect }),
- );
- act(() => result.current.onConnect?.('okta'));
- await waitFor(() => expect(result.current.connections[0]?.connectError).toBe('Network unavailable'));
- expect(result.current.pendingConnectionId).toBeUndefined();
- act(() => result.current.onConnect?.('okta'));
- await waitFor(() => expect(onConnect).toHaveBeenCalledTimes(2));
- });
-
it('releases redirect pending after two seconds', async () => {
vi.useFakeTimers();
const onConnect = vi.fn().mockResolvedValue('redirecting');
From a49b4bd89188ab67795d348173892ccc76a6cbec Mon Sep 17 00:00:00 2001
From: austincalvelage
Date: Tue, 29 Sep 2026 12:00:17 -0600
Subject: [PATCH 6/7] test(mosaic): remove enterprise layer tests covered by
features
---
...enterprise-accounts-compatibility.test.tsx | 48 ++++++++++
...rise-accounts-section.integration.test.tsx | 95 -------------------
...ofile-enterprise-accounts.feature.test.tsx | 17 +++-
...-enterprise-accounts-section.model.test.ts | 53 -----------
4 files changed, 63 insertions(+), 150 deletions(-)
create mode 100644 packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-compatibility.test.tsx
delete mode 100644 packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
delete mode 100644 packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-compatibility.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-compatibility.test.tsx
new file mode 100644
index 00000000000..0e1a302fd65
--- /dev/null
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-compatibility.test.tsx
@@ -0,0 +1,48 @@
+import type * as SharedReact from '@clerk/shared/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { expect, it, vi } from 'vitest';
+
+import { MosaicProvider } from '../../../MosaicProvider';
+import { UserProfileEnterpriseAccountsSection } from '../user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section';
+
+const fallbackNavigate = vi.fn();
+
+vi.mock('@clerk/shared/internal/clerk-js/windowNavigate', () => ({
+ windowNavigate: (url: URL | string) => fallbackNavigate(url),
+}));
+
+vi.mock('@clerk/shared/react', async importOriginal => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useUser: () => ({
+ isLoaded: true,
+ user: {
+ id: 'user_1',
+ enterpriseAccounts: [],
+ createExternalAccount: () =>
+ Promise.resolve({
+ verification: { externalVerificationRedirectURL: new URL('https://accounts.example/authorize') },
+ }),
+ },
+ }),
+ __internal_useUserEnterpriseConnections: () => ({
+ data: [{ id: 'okta', name: 'Acme Okta', allowOrganizationAccountLinking: true }],
+ }),
+ useClerk: () => ({
+ __internal_environment: { userSettings: { enterpriseSSO: { enabled: true } } },
+ __internal_getOption: () => undefined,
+ }),
+ };
+});
+
+it('uses the shared navigation fallback when an older Clerk lacks the internal method', async () => {
+ render(
+
+
+ ,
+ );
+ await userEvent.setup().click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
+ await waitFor(() => expect(fallbackNavigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize')));
+});
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
deleted file mode 100644
index 72346d33dd5..00000000000
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts-section.integration.test.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import type * as SharedReact from '@clerk/shared/react';
-import { render, screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-import { MosaicProvider } from '../../../MosaicProvider';
-import { UserProfileEnterpriseAccountsSection } from '../user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section';
-
-let isLoaded: boolean;
-let signedIn: boolean;
-let enabled: boolean;
-let enterpriseAccounts: Record[];
-let connections: Record[] | undefined;
-let createExternalAccount: ReturnType;
-let navigate: ReturnType | undefined;
-let fallbackNavigate: ReturnType;
-
-vi.mock('@clerk/shared/internal/clerk-js/windowNavigate', () => ({
- windowNavigate: (url: URL | string) => fallbackNavigate(url),
-}));
-
-vi.mock('@clerk/shared/react', async importOriginal => {
- const actual = await importOriginal();
- return {
- ...actual,
- useUser: () => ({
- isLoaded,
- user: signedIn ? { id: 'user_1', enterpriseAccounts, createExternalAccount } : null,
- }),
- __internal_useUserEnterpriseConnections: () => {
- return { data: connections };
- },
- useClerk: () => ({
- __internal_environment: {
- userSettings: { enterpriseSSO: { enabled } },
- displayConfig: { supportEmail: 'help@example.com' },
- },
- __internal_windowNavigate: navigate,
- __internal_getOption: () => undefined,
- }),
- };
-});
-
-function connection(id: string, overrides: Record = {}) {
- return {
- id,
- name: id === 'okta' ? 'Acme Okta' : 'Custom SAML',
- logoPublicUrl: null,
- allowOrganizationAccountLinking: true,
- ...overrides,
- };
-}
-
-function response(url = 'https://accounts.example/authorize') {
- return { verification: { externalVerificationRedirectURL: new URL(url) } };
-}
-
-function renderSection(props: Parameters[0] = {}) {
- return render(
-
-
- ,
- );
-}
-
-describe('UserProfileEnterpriseAccountsSection', () => {
- beforeEach(() => {
- isLoaded = true;
- signedIn = true;
- enabled = true;
- enterpriseAccounts = [];
- connections = [connection('okta'), connection('saml')];
- createExternalAccount = vi.fn().mockResolvedValue(response());
- navigate = vi.fn();
- fallbackNavigate = vi.fn();
- });
-
- it('uses the shared navigation fallback when the Clerk instance lacks the internal method', async () => {
- navigate = undefined;
- const user = userEvent.setup();
- renderSection();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- await waitFor(() => expect(fallbackNavigate).toHaveBeenCalledWith(new URL('https://accounts.example/authorize')));
- });
-
- it('shows a generic direct request error without opening a dialog', async () => {
- createExternalAccount = vi.fn().mockRejectedValue(new Error('Network failed'));
- const user = userEvent.setup();
- renderSection();
- await user.click(screen.getByRole('button', { name: 'Connect Acme Okta' }));
- expect(await screen.findByRole('alert')).toHaveTextContent('Something went wrong. Please try again.');
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
- expect(createExternalAccount).toHaveBeenCalledOnce();
- });
-});
diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx
index 3fe37483e83..48cbdcf8c0b 100644
--- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx
+++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-enterprise-accounts.feature.test.tsx
@@ -88,11 +88,24 @@ describe('enterprise accounts', () => {
error: { code: 'enterprise_error', message: 'Fix this account', long_message: 'Fix this account' },
}),
});
+ const inactive = fapiEnterpriseAccount({
+ id: 'enterprise_inactive',
+ enterprise_connection_id: 'inactive',
+ email_address: 'inactive@example.com',
+ enterprise_connection: fapiEnterpriseConnection({ id: 'inactive', name: 'Inactive SAML', active: false }),
+ });
const client = fapiClient([
- fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_1', enterprise_accounts: [linked] }) }),
+ fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_1', enterprise_accounts: [linked, inactive] }) }),
]);
- await renderSection(signedIn({ client }));
+ await renderSection(
+ signedIn({
+ client,
+ enterpriseConnections: [okta, custom, fapiEnterpriseConnection({ id: 'inactive', name: 'Inactive SAML' })],
+ }),
+ );
+ expect(screen.queryByText('inactive@example.com')).toBeNull();
+ expect(screen.queryByRole('button', { name: 'Connect Inactive SAML' })).toBeNull();
expect(screen.getByText('linked@example.com')).toBeInTheDocument();
expect(screen.getByText('Requires action')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Connect Acme Okta' })).toBeNull();
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts
deleted file mode 100644
index afb364bc03f..00000000000
--- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.test.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { projectEnterpriseAccounts } from './user-profile-enterprise-accounts-section.model';
-
-const accounts = [
- {
- id: 'active',
- enterpriseConnectionId: 'okta',
- emailAddress: 'alice@example.com',
- verification: { error: { longMessage: 'Needs attention' } },
- enterpriseConnection: { active: true, name: 'Acme Okta', logoPublicUrl: 'https://example.com/logo.svg' },
- },
- {
- id: 'inactive',
- enterpriseConnectionId: 'saml',
- emailAddress: 'bob@example.com',
- verification: { error: { longMessage: '' } },
- enterpriseConnection: { active: false, name: 'Custom SAML' },
- },
-];
-
-const connections = [
- { id: 'okta', name: 'Acme Okta', allowOrganizationAccountLinking: true },
- { id: 'saml', name: 'Custom SAML', allowOrganizationAccountLinking: true },
- { id: 'other', name: 'Other', allowOrganizationAccountLinking: true },
- { id: 'forbidden', name: 'Forbidden', allowOrganizationAccountLinking: false },
-];
-
-describe('projectEnterpriseAccounts', () => {
- it('hides existing accounts and links when enterprise SSO is disabled', () => {
- expect(projectEnterpriseAccounts({ enabled: false, accounts, connections })).toEqual({ status: 'hidden' });
- });
-
- it('uses connection activity for rows and excludes every linked connection', () => {
- expect(projectEnterpriseAccounts({ enabled: true, accounts, connections })).toEqual({
- status: 'ready',
- accounts: [
- {
- id: 'active',
- name: 'Acme Okta',
- emailAddress: 'alice@example.com',
- iconUrl: 'https://example.com/logo.svg',
- requiresAction: true,
- },
- ],
- connections: [{ id: 'other', name: 'Other', iconUrl: undefined }],
- });
- });
-
- it('hides an empty section', () => {
- expect(projectEnterpriseAccounts({ enabled: true, accounts: [], connections: [] })).toEqual({ status: 'hidden' });
- });
-});
From 48651b4d98975b28d5dd90d5dec6cd234ccedb38 Mon Sep 17 00:00:00 2001
From: austincalvelage
Date: Wed, 30 Sep 2026 01:31:32 -0600
Subject: [PATCH 7/7] chore(mosaic): mark session reverification as deferred
---
.../user-profile-enterprise-accounts-section.model.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
index 4beb50d8270..c78d07882de 100644
--- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
+++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.tsx
@@ -86,6 +86,7 @@ export function useUserProfileEnterpriseAccountsModel({ mode }: { mode?: 'modal'
return projection;
}
+ // TODO: Add session reverification for enterprise account linking; surface API errors until then.
const connect = async (connectionId: string): Promise<'redirecting' | void> => {
if (!projection.connections.some(connection => connection.id === connectionId)) {
throw new Error(messages.errors.generic);