diff --git a/.changeset/moody-carrots-make.md b/.changeset/moody-carrots-make.md new file mode 100644 index 00000000000..a01603de3c8 --- /dev/null +++ b/.changeset/moody-carrots-make.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/shared': patch +'@clerk/ui': patch +--- + +The SSO setup flow now ends on an explicit Activate step: after configuring and testing a connection you confirm activation with an Activate SSO action (or skip and activate later) instead of a static confirmation summary. diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 8d8f6e68a3f..2bcd237dcff 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -211,6 +211,16 @@ export const enUS: LocalizationResource = { yearPerUnit: 'Year per {{unitName}}', }, configureSSO: { + activate: { + activateButton: 'Activate SSO', + activeSubtitle: 'Anyone signing in with {{domain}} must use your identity provider.', + activeTitle: 'SSO connection is active', + doneButton: 'Done', + skipButton: 'Skip for now', + subtitle: + 'Your SSO connection is ready. Once activated, anyone signing in with {{domain}} must use your identity provider.', + title: 'SSO connection configured', + }, configureStep: { attributeMappingTable: { badges: { @@ -635,35 +645,6 @@ export const enUS: LocalizationResource = { mainHeaderTitle: 'Configure Okta Workforce', }, }, - confirmation: { - configurationSection: { - configureAgainLink: 'Configure again', - issuerLabel: 'Issuer', - ssoUrlLabel: 'Sign on URL', - title: 'Configuration details', - }, - domainSection: { - title: 'Domain', - }, - enableSection: { - title: 'Enable SSO', - }, - inactiveBanner: { - title: 'SSO is inactive and you need to enable it to authenticate', - }, - resetSection: { - confirmationFieldLabel: 'Type "{{name}}" to confirm', - submitButton: 'Reset connection', - title: 'Reset connection', - warning: - 'This will permanently remove the SSO configuration. Members will no longer be able to sign in with SSO.', - }, - statusSection: { - activeBadge: 'Active', - inactiveBadge: 'Inactive', - title: 'SSO Successfully configured', - }, - }, missingManageEnterpriseConnectionsPermission: { subtitle: "Contact your organization's administrator to upgrade your permissions.", title: 'You do not have permission to manage Single Sign-on (SSO)', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index ca7e8e8a3d5..cef53127261 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1830,33 +1830,14 @@ export type __internal_LocalizationResource = { }; }; }; - confirmation: { - statusSection: { - title: LocalizationValue; - activeBadge: LocalizationValue; - inactiveBadge: LocalizationValue; - }; - enableSection: { - title: LocalizationValue; - }; - domainSection: { - title: LocalizationValue; - }; - configurationSection: { - title: LocalizationValue; - ssoUrlLabel: LocalizationValue; - issuerLabel: LocalizationValue; - configureAgainLink: LocalizationValue; - }; - resetSection: { - title: LocalizationValue; - warning: LocalizationValue; - confirmationFieldLabel: LocalizationValue<'name'>; - submitButton: LocalizationValue; - }; - inactiveBanner: { - title: LocalizationValue; - }; + activate: { + title: LocalizationValue; + subtitle: LocalizationValue<'domain'>; + activateButton: LocalizationValue; + skipButton: LocalizationValue; + activeTitle: LocalizationValue; + activeSubtitle: LocalizationValue<'domain'>; + doneButton: LocalizationValue; }; }; apiKeys: { diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSOWizard.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSOWizard.tsx index 837af856821..f4a4d1aeba3 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSOWizard.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSOWizard.tsx @@ -7,8 +7,8 @@ import { ConfigureSSOHeader } from './ConfigureSSOHeader'; import { areAllOrganizationDomainsVerified } from './domain/organizationEnterpriseConnection'; import { Wizard, type WizardStepConfig } from './elements/Wizard'; import { + ActivateStep, ConfigureStep, - ConfirmationStep, OrganizationDomainsStep, SelectProviderStep, TestConfigurationStep, @@ -26,11 +26,11 @@ export const ConfigureSSOWizard = ({ title, forceInitialStep, ...props }: Config const steps = React.useMemo( () => [ - { id: 'verify-domain', label: 'Verify domain' }, + { id: 'verify-domain', label: 'Domains' }, { id: 'select-provider', guard: () => allDomainsVerified }, - { id: 'configure', label: 'Configure', guard: () => c.hasConnection }, + { id: 'configure', label: 'Connection', guard: () => c.hasConnection }, { id: 'test', label: 'Test', guard: () => c.hasMinimumConfiguration || c.isActive }, - { id: 'confirmation', label: 'Confirmation', guard: () => c.hasSuccessfulTestRun || c.isActive }, + { id: 'activate', label: 'Activate', guard: () => c.hasSuccessfulTestRun || c.isActive }, ], [c, allDomainsVerified], ); @@ -69,9 +69,9 @@ export const ConfigureSSOWizard = ({ title, forceInitialStep, ...props }: Config - + - + diff --git a/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.navigation.test.tsx b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.navigation.test.tsx index 5c97120aaf5..6551e5f195f 100644 --- a/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.navigation.test.tsx +++ b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.navigation.test.tsx @@ -225,4 +225,30 @@ describe('ConfigureSSO wizard navigation (integration)', () => { expect(queryByText(/at least one successful test run/i)).not.toBeInTheDocument(); }); }); + + // The labelled steps surface in the stepper under their renamed labels, and an + // active connection short-circuits to the (reachable) activate step. + it('renders the renamed stepper labels and reaches the activate step', async () => { + const { wrapper, fixtures } = await createFixtures(withAdminOrgUser); + + fixtures.clerk.organization?.getEnterpriseConnections.mockResolvedValue([ + { ...configuredConnection, id: 'ent_active', active: true } as any, + ]); + fixtures.clerk.organization?.getEnterpriseConnectionTestRuns.mockResolvedValue({ + data: [], + total_count: 0, + } as any); + mockVerifiedDomains(fixtures); + + const { findByText, getByText } = render(, { wrapper }); + + // The activate step body renders (active connection short-circuits to the already-active variant). + await findByText(/sso connection is active/i); + + // The stepper carries the renamed labels (select-provider stays unlabelled). + expect(getByText('Domains')).toBeInTheDocument(); + expect(getByText('Connection')).toBeInTheDocument(); + expect(getByText('Test')).toBeInTheDocument(); + expect(getByText('Activate')).toBeInTheDocument(); + }); }); diff --git a/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx index 454f4a15de9..b5309a864c5 100644 --- a/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx +++ b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx @@ -158,7 +158,7 @@ describe('ConfigureSSO', () => { expect(queryByText(/select your identity provider/i)).not.toBeInTheDocument(); }); - it('short-circuits to the confirmation step for an active connection', async () => { + it('short-circuits to the activate step for an active connection', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEnterpriseSso({ selfServeSSO: true }); f.withEmailAddress(); @@ -177,7 +177,7 @@ describe('ConfigureSSO', () => { active: true, // Owned by the active organization (matches the membership above), so // the domain is not "taken by another org" and the machine can - // short-circuit to confirmation. + // short-circuit to activate. organizationId: 'Org1', domains: ['clerk.com'], samlConnection: { @@ -195,8 +195,8 @@ describe('ConfigureSSO', () => { const { findByText, queryByText } = render(, { wrapper }); - // An active connection lands on confirmation even if never tested. - await findByText(/configuration/i); + // An active connection lands on the activate-step's already-active variant. + await findByText(/sso connection is active/i); expect(queryByText(/select your identity provider/i)).not.toBeInTheDocument(); }); @@ -229,7 +229,7 @@ describe('ConfigureSSO', () => { } as any, ]); mockOrganizationDomains(fixtures, [verifiedDomain]); - // No successful run yet, so the confirmation guard fails and the + // No successful run yet, so the activate guard fails and the // furthest-reachable step is `test`. fixtures.clerk.organization?.getEnterpriseConnectionTestRuns.mockResolvedValue({ data: [], @@ -239,12 +239,12 @@ describe('ConfigureSSO', () => { const { findByText, queryByText } = render(, { wrapper }); // Configured + inactive + no successful run ⇒ lands on the test step, not - // confirmation. + // activate. await findByText(/test your sso connection/i); - expect(queryByText(/configuration details/i)).not.toBeInTheDocument(); + expect(queryByText(/sso connection configured/i)).not.toBeInTheDocument(); }); - it('mounts on confirmation for a configured-but-inactive connection that has a successful test run', async () => { + it('mounts on activate for a configured-but-inactive connection that has a successful test run', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withEnterpriseSso({ selfServeSSO: true }); f.withEmailAddress(); @@ -272,10 +272,10 @@ describe('ConfigureSSO', () => { } as any, ]); mockOrganizationDomains(fixtures, [verifiedDomain]); - // A successful run satisfies the confirmation guard (`hasSuccessfulTestRun`) + // A successful run satisfies the activate guard (`hasSuccessfulTestRun`) // even though the connection is still inactive — the success-filtered probe // returns a row, so the furthest-reachable step clears `test` and lands on - // confirmation. Distinct from the active short-circuit above (here + // activate. Distinct from the active short-circuit above (here // `active: false`, so it is the test run — not activation — that carries the // wizard past the test step). fixtures.clerk.organization?.getEnterpriseConnectionTestRuns.mockResolvedValue({ @@ -285,9 +285,8 @@ describe('ConfigureSSO', () => { const { findByText, queryByText } = render(, { wrapper }); - // Lands on confirmation (the inactive badge + configuration details render), - // not the test step. - await findByText(/configuration details/i); + // Lands on the activate step (not-active variant: "SSO connection configured" renders). + await findByText(/sso connection configured/i); expect(queryByText(/test your sso connection/i)).not.toBeInTheDocument(); }); }); diff --git a/packages/ui/src/components/ConfigureSSO/elements/Step.tsx b/packages/ui/src/components/ConfigureSSO/elements/Step.tsx index e1b5ef32aa5..a26ac3c5b23 100644 --- a/packages/ui/src/components/ConfigureSSO/elements/Step.tsx +++ b/packages/ui/src/components/ConfigureSSO/elements/Step.tsx @@ -222,7 +222,7 @@ const FooterReset = (): JSX.Element | null => { size='sm' colorScheme='danger' onClick={() => setIsOpen(true)} - localizationKey={localizationKeys('configureSSO.confirmation.resetSection.title')} + localizationKey={localizationKeys('configureSSO.resetConnectionDialog.resetButton')} sx={{ marginInlineEnd: 'auto' }} /> { + const { + enterpriseConnection, + organizationEnterpriseConnection, + enterpriseConnectionMutations: { setConnectionActive }, + onExit, + } = useConfigureSSO(); + const card = useCardState(); + + // The activate step is only reachable with a configured connection, so the + // domains are set; join multiples for the subtitle copy. + const domain = (enterpriseConnection?.domains ?? []).join(', '); + const isActive = organizationEnterpriseConnection.isActive; + + const handleActivate = async (): Promise => { + if (!enterpriseConnection || card.isLoading) { + return; + } + + card.setError(undefined); + card.setLoading(); + + try { + await setConnectionActive(enterpriseConnection.id, true); + onExit?.(); + } catch (err) { + handleError(err as Error, [], card.setError); + } finally { + card.setIdle(); + } + }; + + return ( + + + + + ({ textAlign: 'center', maxWidth: '20.75rem', gap: t.space.$3x5 })} + > + ({ width: t.sizes.$8, height: t.sizes.$8 })} + /> + + + + + + + {card.error && ( + + )} + + + {isActive ? ( + + + )} + + + + + ); +}; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfirmationStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfirmationStep.tsx deleted file mode 100644 index b0518e00c3d..00000000000 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfirmationStep.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { useOrganization } from '@clerk/shared/react'; -import { useState } from 'react'; - -import { AlertIcon, Badge, Button, Col, descriptors, Flex, Flow, Link, localizationKeys, Text } from '@/customizables'; -import { useCardState } from '@/elements/contexts'; -import { ProfileSection } from '@/elements/Section'; -import { Switch } from '@/elements/Switch'; -import { handleError } from '@/utils/errorHandler'; - -import { useConfigureSSO } from '../ConfigureSSOContext'; -import { Step } from '../elements/Step'; -import { useWizard } from '../elements/Wizard'; -import { ResetConnectionDialog } from '../ResetConnectionDialog'; - -export const ConfirmationStep = (): JSX.Element => { - const { enterpriseConnection } = useConfigureSSO(); - const isActive = !!enterpriseConnection?.active; - - return ( - - - - } - /> - - - - - - - - - - - - - {!isActive && ( - - ({ marginInlineEnd: t.space.$2 })} - /> - - - - )} - - - - ); -}; - -const EnableSsoSection = (): JSX.Element => { - const { - enterpriseConnection, - enterpriseConnectionMutations: { setConnectionActive }, - } = useConfigureSSO(); - const card = useCardState(); - - const [isChecked, setIsChecked] = useState(!!enterpriseConnection?.active); - - const onActiveChange = async (active: boolean) => { - if (card.isLoading) { - return; - } - - card.setError(undefined); - card.setLoading(); - setIsChecked(active); - - try { - // Enterprise connection is guaranteed to be set at this point - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const updated = await setConnectionActive(enterpriseConnection!.id, active); - if (updated) { - setIsChecked(updated.active); - } - } catch (err) { - setIsChecked(!active); - handleError(err as Error, [], card.setError); - } finally { - card.setIdle(); - } - }; - - return ( - - void onActiveChange(active)} - aria-label='Enable SSO' - /> - - ); -}; - -const DomainSection = (): JSX.Element | null => { - const { enterpriseConnection } = useConfigureSSO(); - const domain = enterpriseConnection?.domains?.[0]; - - // A type guard only, domains are guaranteed to be set at this point - if (!domain) { - return null; - } - - return ( - - - {domain} - - - ); -}; - -const ConfigurationDetailsSection = (): JSX.Element => { - const { enterpriseConnection } = useConfigureSSO(); - const { goToStep } = useWizard(); - - // This will later be expanded to support OIDC connections as well - const samlConnection = enterpriseConnection?.samlConnection; - - return ( - - - ({ gap: t.sizes.$2 })} - > - ({ gap: t.space.$3, paddingInlineStart: 0 })} - > - ({ width: t.space.$36, flexShrink: 0, whiteSpace: 'nowrap' })} - /> - - - {samlConnection?.idpSsoUrl} - - - - ({ gap: t.space.$3, paddingInlineStart: 0 })} - > - ({ width: t.space.$36, flexShrink: 0, whiteSpace: 'nowrap' })} - /> - - {samlConnection?.idpEntityId} - - - - - -