From fddec7af796a4732bc434875aa97fc95f71575c7 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:27:23 -0300 Subject: [PATCH 01/22] feat(ui): SAML metadata URL submission in ConfigureSSO Configure step Adds the Okta SAML metadata URL path to the Configure step. The user pastes their IdP metadata URL and the wizard advances on a successful PATCH to user.updateEnterpriseConnection with { saml: { idpMetadataUrl } }. The mutation is wrapped in useReverification, matching the established convention for sensitive user.* mutations in @clerk/ui. useCardState drives the loading state; handleError routes backend errors inline under the field when the API returns idp_metadata_url, or to the card-level error surface otherwise. Locale keys added under configureSSO.configureStep in en-US. Manual entry, file upload, SP-side copy rows, and the Okta admin-console walkthrough are deferred to follow-up PRs. --- ...nfigure-sso-configure-step-metadata-url.md | 7 ++ packages/localizations/src/en-US.ts | 9 +++ packages/shared/src/types/localization.ts | 9 +++ .../ConfigureSSO/steps/ConfigureStep.tsx | 66 ++++++++++++++++--- 4 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 .changeset/configure-sso-configure-step-metadata-url.md diff --git a/.changeset/configure-sso-configure-step-metadata-url.md b/.changeset/configure-sso-configure-step-metadata-url.md new file mode 100644 index 00000000000..0fa7758492b --- /dev/null +++ b/.changeset/configure-sso-configure-step-metadata-url.md @@ -0,0 +1,7 @@ +--- +'@clerk/localizations': patch +'@clerk/shared': patch +'@clerk/ui': patch +--- + +Implement the Okta SAML metadata URL submission path in the Configure step of `<__experimental_ConfigureSSO />`. Adds a single text input for the IdP metadata URL; Continue posts `{ saml: { idpMetadataUrl } }` via `user.updateEnterpriseConnection` wrapped in `useReverification`, with `useCardState` driving the loading state and `handleError` routing backend errors inline to the field or to the card-level error surface. Locale keys added under `configureSSO.configureStep` in `en-US`. Manual entry, file upload, SP-side copy rows, and the Okta admin-console walkthrough ship in follow-up PRs. diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 4dc0dcd1bdc..49dd80256bf 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -247,6 +247,15 @@ export const enUS: LocalizationResource = { subtitle: "Contact the application's administrator to get access through the existing connection.", }, }, + configureStep: { + title: 'Configure Okta Workforce', + subtitle: 'Create a new enterprise application in your Okta Dashboard', + metadataUrl: { + label: 'Metadata URL', + placeholder: 'https://app.okta.com/.../metadata', + description: 'In your Okta SAML app, go to the Sign On tab and retrieve the metadata URL. Paste it below.', + }, + }, }, createOrganization: { formButtonSubmit: 'Create organization', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 95b1879a80c..e434adc3c7f 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1338,6 +1338,15 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; }; }; + configureStep: { + title: LocalizationValue; + subtitle: LocalizationValue; + metadataUrl: { + label: LocalizationValue; + placeholder: LocalizationValue; + description: LocalizationValue; + }; + }; }; apiKeys: { formTitle: LocalizationValue; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index af420302c57..51a0fe110cf 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -1,13 +1,58 @@ -import { descriptors, Flow } from '@/customizables'; +import { useReverification, useUser } from '@clerk/shared/react'; +import type { UpdateMeEnterpriseConnectionParams } from '@clerk/shared/types'; + +import { descriptors, Flow, localizationKeys } from '@/customizables'; +import { useCardState } from '@/elements/contexts'; +import { Form } from '@/elements/Form'; +import { handleError } from '@/utils/errorHandler'; +import { useFormControl } from '@/utils/useFormControl'; import { useConfigureSSO } from '../ConfigureSSOContext'; import { Step } from '../elements/Step'; import { useWizard } from '../elements/Wizard'; export const ConfigureStep = (): JSX.Element => { - const { goNext, goPrev, isFirstStep, isLastStep } = useWizard(); + const card = useCardState(); + const { user } = useUser(); + const { goNext, goPrev, isFirstStep } = useWizard(); const { enterpriseConnection } = useConfigureSSO(); + const updateEnterpriseConnection = useReverification( + (enterpriseConnectionId: string, params: UpdateMeEnterpriseConnectionParams) => + user?.updateEnterpriseConnection(enterpriseConnectionId, params), + ); + + const metadataUrlField = useFormControl('idpMetadataUrl', '', { + type: 'text', + label: localizationKeys('configureSSO.configureStep.metadataUrl.label'), + placeholder: localizationKeys('configureSSO.configureStep.metadataUrl.placeholder'), + infoText: localizationKeys('configureSSO.configureStep.metadataUrl.description'), + isRequired: true, + }); + + const trimmedMetadataUrl = metadataUrlField.value.trim(); + const canSubmit = trimmedMetadataUrl.length > 0 && !card.isLoading; + + const handleContinue = async () => { + if (!enterpriseConnection || !canSubmit) { + return; + } + + card.setError(undefined); + card.setLoading(); + + try { + await updateEnterpriseConnection(enterpriseConnection.id, { + saml: { idpMetadataUrl: trimmedMetadataUrl }, + }); + void goNext(); + } catch (err) { + handleError(err as Error, [metadataUrlField], card.setError); + } finally { + card.setIdle(); + } + }; + return ( { elementId={descriptors.configureSSOStep.setId('configure')} > - Single sign-on URL: {enterpriseConnection?.samlConnection?.acsUrl} + ({ gap: theme.space.$5 })}> + + + + goPrev()} - isDisabled={isFirstStep} + isDisabled={isFirstStep || card.isLoading} /> goNext()} - isDisabled={isLastStep} + onClick={handleContinue} + isLoading={card.isLoading} + isDisabled={!canSubmit} /> From 8de47c2b9e938d2349956a80951c67f1d082aeaf Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:33:35 -0300 Subject: [PATCH 02/22] fix(shared): add idpMetadataUrl to FieldId union Unblocks type-check for the SAML metadata URL input added to the ConfigureSSO Configure step. --- packages/shared/src/types/elementIds.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/src/types/elementIds.ts b/packages/shared/src/types/elementIds.ts index 77f71404daa..95ef95a6247 100644 --- a/packages/shared/src/types/elementIds.ts +++ b/packages/shared/src/types/elementIds.ts @@ -26,6 +26,7 @@ export type FieldId = | 'apiKeyExpirationDate' | 'apiKeyRevokeConfirmation' | 'apiKeySecret' + | 'idpMetadataUrl' | 'web3WalletName'; export type ProfileSectionId = | 'profile' From 80a1fdae20824d120113105496d2c5bc0e8ab12f Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:37:38 -0300 Subject: [PATCH 03/22] refactor(ui): hoist enterprise connection update to ConfigureSSO provider Mirrors the existing createConnection pattern: ConfigureSSOCardContent destructures updateEnterpriseConnection from __internal_useUserEnterpriseConnections and passes it as a prop to ConfigureSSOProvider, which wraps it once in useReverification and exposes it as updateConnection on the context. The id is taken implicitly from enterpriseConnection in context, so call sites don't thread it through. ConfigureStep now just calls updateConnection({ saml: { idpMetadataUrl } }) and gets both reverification and query revalidation for free, since the hook owns the revalidate call after a successful mutation. --- .../components/ConfigureSSO/ConfigureSSO.tsx | 12 +++++-- .../ConfigureSSO/ConfigureSSOContext.tsx | 31 +++++++++++++++++-- .../ConfigureSSO/steps/ConfigureStep.tsx | 15 ++------- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx index edb4fed4127..241dcdccc52 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx @@ -64,7 +64,11 @@ const AuthenticatedContent = withCoreUserGuard(() => { }); const ConfigureSSOCardContent = () => { - const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true }); + const { + data: enterpriseConnections, + isLoading, + updateEnterpriseConnection, + } = __internal_useUserEnterpriseConnections({ enabled: true }); // Currently FAPI only supports one enterprise connection per user const enterpriseConnection = enterpriseConnections?.[0]; @@ -74,7 +78,11 @@ const ConfigureSSOCardContent = () => { } return ( - + + ); diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx index 1df2c315e6a..23b4001704f 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx @@ -1,4 +1,5 @@ -import type { EnterpriseConnectionResource } from '@clerk/shared/types'; +import { useReverification } from '@clerk/shared/react'; +import type { EnterpriseConnectionResource, UpdateMeEnterpriseConnectionParams } from '@clerk/shared/types'; import React, { type PropsWithChildren } from 'react'; import { deriveInitialStep } from './deriveInitialStep'; @@ -24,10 +25,21 @@ export interface ConfigureSSOData { * connection has been created. */ setProvider: (provider: ProviderType) => void; + /** + * Updates the current enterprise connection with the supplied params. The id + * is taken implicitly from `enterpriseConnection` in context, so callers do + * not need to thread it through. Throws when no enterprise connection is + * loaded yet. + */ + updateConnection: (params: UpdateMeEnterpriseConnectionParams) => Promise; } interface ConfigureSSOProviderProps { enterpriseConnection: EnterpriseConnectionResource | undefined; + updateEnterpriseConnection: ( + enterpriseConnectionId: string, + params: UpdateMeEnterpriseConnectionParams, + ) => Promise; } const ConfigureSSOContext = React.createContext(null); @@ -35,6 +47,7 @@ ConfigureSSOContext.displayName = 'ConfigureSSOContext'; export const ConfigureSSOProvider = ({ enterpriseConnection, + updateEnterpriseConnection, children, }: PropsWithChildren): JSX.Element => { const [provider, setProvider] = React.useState( @@ -43,14 +56,28 @@ export const ConfigureSSOProvider = ({ const initialStepId = deriveInitialStep(enterpriseConnection); + const updateConnectionFetcher = React.useCallback( + async (params: UpdateMeEnterpriseConnectionParams) => { + if (!enterpriseConnection) { + throw new Error('Enterprise connection required'); + } + + return updateEnterpriseConnection(enterpriseConnection.id, params); + }, + [enterpriseConnection, updateEnterpriseConnection], + ); + + const updateConnection = useReverification(updateConnectionFetcher); + const value = React.useMemo( () => ({ initialStepId, enterpriseConnection, provider, setProvider, + updateConnection, }), - [initialStepId, enterpriseConnection, provider], + [initialStepId, enterpriseConnection, provider, updateConnection], ); return {children}; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 51a0fe110cf..99cb773ec26 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -1,6 +1,3 @@ -import { useReverification, useUser } from '@clerk/shared/react'; -import type { UpdateMeEnterpriseConnectionParams } from '@clerk/shared/types'; - import { descriptors, Flow, localizationKeys } from '@/customizables'; import { useCardState } from '@/elements/contexts'; import { Form } from '@/elements/Form'; @@ -13,14 +10,8 @@ import { useWizard } from '../elements/Wizard'; export const ConfigureStep = (): JSX.Element => { const card = useCardState(); - const { user } = useUser(); const { goNext, goPrev, isFirstStep } = useWizard(); - const { enterpriseConnection } = useConfigureSSO(); - - const updateEnterpriseConnection = useReverification( - (enterpriseConnectionId: string, params: UpdateMeEnterpriseConnectionParams) => - user?.updateEnterpriseConnection(enterpriseConnectionId, params), - ); + const { enterpriseConnection, updateConnection } = useConfigureSSO(); const metadataUrlField = useFormControl('idpMetadataUrl', '', { type: 'text', @@ -42,9 +33,7 @@ export const ConfigureStep = (): JSX.Element => { card.setLoading(); try { - await updateEnterpriseConnection(enterpriseConnection.id, { - saml: { idpMetadataUrl: trimmedMetadataUrl }, - }); + await updateConnection({ saml: { idpMetadataUrl: trimmedMetadataUrl } }); void goNext(); } catch (err) { handleError(err as Error, [metadataUrlField], card.setError); From f2c5262e378d40bb1522527bd0c91c55abd25bdc Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:47:26 -0300 Subject: [PATCH 04/22] fix(ui): render Configure step description above input Drops the infoText option on useFormControl (which renders the helper copy as a focus-triggered tooltip) and places the description as a static element above the input. Styling mirrors the muted-body treatment used in SelectProviderStep so the inline copy reads the same as the tooltip did. Also tightens the placeholder copy from the dummy metadata URL to a neutral 'Paste URL here...'. --- packages/localizations/src/en-US.ts | 4 ++++ .../src/components/ConfigureSSO/steps/ConfigureStep.tsx | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 49dd80256bf..b4a1d2ad4ac 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -252,7 +252,11 @@ export const enUS: LocalizationResource = { subtitle: 'Create a new enterprise application in your Okta Dashboard', metadataUrl: { label: 'Metadata URL', +<<<<<<< HEAD placeholder: 'https://app.okta.com/.../metadata', +======= + placeholder: 'Paste URL here...', +>>>>>>> 35011671b (fix(ui): render Configure step description above input) description: 'In your Okta SAML app, go to the Sign On tab and retrieve the metadata URL. Paste it below.', }, }, diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 99cb773ec26..c2b13753ff3 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -1,4 +1,4 @@ -import { descriptors, Flow, localizationKeys } from '@/customizables'; +import { descriptors, Flow, localizationKeys, Text } from '@/customizables'; import { useCardState } from '@/elements/contexts'; import { Form } from '@/elements/Form'; import { handleError } from '@/utils/errorHandler'; @@ -17,7 +17,6 @@ export const ConfigureStep = (): JSX.Element => { type: 'text', label: localizationKeys('configureSSO.configureStep.metadataUrl.label'), placeholder: localizationKeys('configureSSO.configureStep.metadataUrl.placeholder'), - infoText: localizationKeys('configureSSO.configureStep.metadataUrl.description'), isRequired: true, }); @@ -55,6 +54,12 @@ export const ConfigureStep = (): JSX.Element => { ({ gap: theme.space.$5 })}> + ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.metadataUrl.description')} + /> From 98c07ddf2d2a1abaea00bf0737d96b6566849b8f Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 16:06:34 -0300 Subject: [PATCH 05/22] refactor(ui): split Configure step into 4 inner Wizard sub-steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors VerifyDomainStep's nested wizard pattern: the outer Step is now a pure shell wrapping an inner Wizard with four Wizard.Step children — create-app, configure-attributes, assign-users, submit-saml-config. Step.Header renders an InnerStepCounter so the body shows Step X/4 as the user moves through the sub-steps. The existing metadata URL form moves into SubmitSamlConfigSubStep unchanged — same useReverification, useCardState, handleError wiring, same field, same PATCH. The first three sub-steps are placeholders with Previous/Continue scaffolding; content lands in follow-up commits. goNext/goPrev bubble across the wizard boundary natively (the Wizard primitive supports nested parent navigation), so the form's Continue handler still advances to the outer Test step on a successful PATCH without any cross-boundary plumbing. --- .../ConfigureSSO/steps/ConfigureStep.tsx | 189 ++++++++++++++---- 1 file changed, 153 insertions(+), 36 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index c2b13753ff3..9619dfa50f2 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -6,9 +6,138 @@ import { useFormControl } from '@/utils/useFormControl'; import { useConfigureSSO } from '../ConfigureSSOContext'; import { Step } from '../elements/Step'; -import { useWizard } from '../elements/Wizard'; +import { useWizard, Wizard } from '../elements/Wizard'; export const ConfigureStep = (): JSX.Element => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const InnerStepCounter = (): JSX.Element => { + const { currentIndex, totalSteps } = useWizard(); + return ( + + ); +}; + +export const CreateAppSubStep = (): JSX.Element => { + const { goNext, goPrev, isFirstStep, isLastStep } = useWizard(); + + return ( + <> + + UI goes here + + + + goPrev()} + isDisabled={isFirstStep} + /> + goNext()} + isDisabled={isLastStep} + /> + + + ); +}; + +export const ConfigureAttributesSubStep = (): JSX.Element => { + const { goNext, goPrev, isFirstStep, isLastStep } = useWizard(); + + return ( + <> + + UI goes here + + + + goPrev()} + isDisabled={isFirstStep} + /> + goNext()} + isDisabled={isLastStep} + /> + + + ); +}; + +export const AssignUsersSubStep = (): JSX.Element => { + const { goNext, goPrev, isFirstStep, isLastStep } = useWizard(); + + return ( + <> + + UI goes here + + + + goPrev()} + isDisabled={isFirstStep} + /> + goNext()} + isDisabled={isLastStep} + /> + + + ); +}; + +export const SubmitSamlConfigSubStep = (): JSX.Element => { const card = useCardState(); const { goNext, goPrev, isFirstStep } = useWizard(); const { enterpriseConnection, updateConnection } = useConfigureSSO(); @@ -42,42 +171,30 @@ export const ConfigureStep = (): JSX.Element => { }; return ( - - - + ({ gap: theme.space.$5 })}> + ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.metadataUrl.description')} /> + + + + - - ({ gap: theme.space.$5 })}> - ({ color: theme.colors.$colorMutedForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.metadataUrl.description')} - /> - - - - - - - - goPrev()} - isDisabled={isFirstStep || card.isLoading} - /> - - - - + + goPrev()} + isDisabled={isFirstStep || card.isLoading} + /> + + + ); }; From 8c4cd9c6a68af73763d5eaf0937bf18071824a48 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 16:20:52 -0300 Subject: [PATCH 06/22] fix(ui): make ConfigureSSO Configure sub-step sections fill the body Adds flex:1 to SubmitSamlConfigSubStep's Step.Section (was missing, so the footer didn't sit flush with the card edge) and drops the align/justify props on the placeholder sub-steps. Pure layout polish. --- .../ConfigureSSO/steps/ConfigureStep.tsx | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 9619dfa50f2..ab3b1657e3f 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -61,11 +61,7 @@ export const CreateAppSubStep = (): JSX.Element => { return ( <> - + UI goes here @@ -88,11 +84,7 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { return ( <> - + UI goes here @@ -115,11 +107,7 @@ export const AssignUsersSubStep = (): JSX.Element => { return ( <> - + UI goes here @@ -172,7 +160,7 @@ export const SubmitSamlConfigSubStep = (): JSX.Element => { return ( <> - ({ gap: theme.space.$5 })}> + ({ flex: 1, gap: theme.space.$5 })}> Date: Tue, 12 May 2026 16:26:02 -0300 Subject: [PATCH 07/22] fix(clerk-js): flatten SAML/OIDC body in toMeEnterpriseConnectionBody The function previously ran deepCamelToSnake(params), producing a nested body like { saml: { idp_metadata_url } }. The backend expects the SAML and OIDC fields prefixed at the top level (saml_idp_metadata_url, oidc_client_id, etc.), so IdP metadata submissions in <__experimental_ConfigureSSO /> were silently rejected. Replaces the helper with a manual flat-field mapper: top-level fields stay top-level, SAML fields get a saml_ prefix, OIDC fields get an oidc_ prefix. attribute_mapping and custom_attributes pass through unchanged since their inner keys are user-supplied and must not be transformed. A small setIfDefined helper makes the omit-undefined / forward-null semantics explicit, so users can clear a field by sending null without the SDK silently dropping it. Mirrors the fix Laura validated in the SAML POC PR. --- .../fix-enterprise-connection-flat-body.md | 5 ++ packages/clerk-js/src/core/resources/User.ts | 62 +++++++++++++++---- .../src/core/resources/__tests__/User.test.ts | 22 +++---- 3 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 .changeset/fix-enterprise-connection-flat-body.md diff --git a/.changeset/fix-enterprise-connection-flat-body.md b/.changeset/fix-enterprise-connection-flat-body.md new file mode 100644 index 00000000000..f9eefece296 --- /dev/null +++ b/.changeset/fix-enterprise-connection-flat-body.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Fix `toMeEnterpriseConnectionBody` to produce the flat snake_case body shape the backend expects for `user.createEnterpriseConnection` and `user.updateEnterpriseConnection`. SAML and OIDC fields are now top-level prefixed (e.g., `saml_idp_metadata_url`) rather than nested under `saml` / `oidc` objects. Without this fix, IdP metadata submission in `<__experimental_ConfigureSSO />` silently fails on the backend. diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index 3b96e83f342..9ec8dfd5ea3 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -44,7 +44,6 @@ import type { VerifyTOTPParams, Web3WalletResource, } from '@clerk/shared/types'; -import { deepCamelToSnake } from '@clerk/shared/underscore'; import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams'; import { unixEpochToDate } from '../../utils/date'; @@ -559,25 +558,64 @@ export class User extends BaseResource implements UserResource { * Serializes `CreateMeEnterpriseConnectionParams` / `UpdateMeEnterpriseConnectionParams` * for the `/me/enterprise_connections` FAPI endpoints. * - * Uses `deepCamelToSnake` but preserves `saml.attributeMapping` and `customAttributes` as-is. Their keys are + * The handler expects a flat form body where SAML and OIDC fields are + * prefixed (e.g. `saml_idp_metadata_url`, `oidc_client_id`) rather + * than nested under `saml`/`oidc` objects. `attribute_mapping` and + * `custom_attributes` stay as object values and are JSON-stringified + * by the form serializer downstream — their inner keys are * user-supplied data and must not be camel→snake transformed. */ function toMeEnterpriseConnectionBody( params: CreateMeEnterpriseConnectionParams | UpdateMeEnterpriseConnectionParams, ): Record { - const originalAttributeMapping = - params.saml && typeof params.saml === 'object' ? params.saml.attributeMapping : undefined; - const originalCustomAttributes = 'customAttributes' in params ? params.customAttributes : undefined; - - const body = deepCamelToSnake(params) as Record; - - if (originalAttributeMapping !== undefined && body.saml && typeof body.saml === 'object') { - body.saml.attribute_mapping = originalAttributeMapping; + const body: Record = {}; + + // Top-level fields. `provider` is only on Create, the rest are shared + setIfDefined(body, 'provider', (params as CreateMeEnterpriseConnectionParams).provider); + setIfDefined(body, 'name', params.name); + setIfDefined(body, 'organization_id', params.organizationId); + setIfDefined(body, 'active', (params as UpdateMeEnterpriseConnectionParams).active); + setIfDefined(body, 'sync_user_attributes', (params as UpdateMeEnterpriseConnectionParams).syncUserAttributes); + setIfDefined( + body, + 'disable_additional_identifications', + (params as UpdateMeEnterpriseConnectionParams).disableAdditionalIdentifications, + ); + setIfDefined(body, 'custom_attributes', (params as UpdateMeEnterpriseConnectionParams).customAttributes); + + if (params.saml) { + setIfDefined(body, 'saml_idp_entity_id', params.saml.idpEntityId); + setIfDefined(body, 'saml_idp_sso_url', params.saml.idpSsoUrl); + setIfDefined(body, 'saml_idp_certificate', params.saml.idpCertificate); + setIfDefined(body, 'saml_idp_metadata_url', params.saml.idpMetadataUrl); + setIfDefined(body, 'saml_idp_metadata', params.saml.idpMetadata); + setIfDefined(body, 'saml_attribute_mapping', params.saml.attributeMapping); + setIfDefined(body, 'saml_allow_subdomains', params.saml.allowSubdomains); + setIfDefined(body, 'saml_allow_idp_initiated', params.saml.allowIdpInitiated); + setIfDefined(body, 'saml_force_authn', params.saml.forceAuthn); } - if (originalCustomAttributes !== undefined) { - body.custom_attributes = originalCustomAttributes; + if (params.oidc) { + setIfDefined(body, 'oidc_client_id', params.oidc.clientId); + setIfDefined(body, 'oidc_client_secret', params.oidc.clientSecret); + setIfDefined(body, 'oidc_discovery_url', params.oidc.discoveryUrl); + setIfDefined(body, 'oidc_auth_url', params.oidc.authUrl); + setIfDefined(body, 'oidc_token_url', params.oidc.tokenUrl); + setIfDefined(body, 'oidc_user_info_url', params.oidc.userInfoUrl); + setIfDefined(body, 'oidc_requires_pkce', params.oidc.requiresPkce); } return body; } + +/** + * Adds `value` under `key` only when the caller actually provided it. + * Mirrors the SDK's existing semantics: `undefined` means "don't send + * this field"; `null` is forwarded so users can explicitly clear a + * value via the form-encoded body + */ +function setIfDefined(target: Record, key: string, value: unknown): void { + if (value !== undefined) { + target[key] = value; + } +} diff --git a/packages/clerk-js/src/core/resources/__tests__/User.test.ts b/packages/clerk-js/src/core/resources/__tests__/User.test.ts index 4e56eaf1b5d..20e8074cf16 100644 --- a/packages/clerk-js/src/core/resources/__tests__/User.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/User.test.ts @@ -184,7 +184,7 @@ describe('User', () => { provider: 'saml_okta', name: 'New SSO', organization_id: 'org_1', - saml: { idp_entity_id: 'https://idp.example.com' }, + saml_idp_entity_id: 'https://idp.example.com', }, }); @@ -291,13 +291,11 @@ describe('User', () => { body: { provider: 'saml_okta', name: 'New SSO', - saml: { - idp_entity_id: 'https://idp.example.com', - attribute_mapping: { - emailAddress: 'mail', - firstName: 'givenName', - 'custom:role': 'role', - }, + saml_idp_entity_id: 'https://idp.example.com', + saml_attribute_mapping: { + emailAddress: 'mail', + firstName: 'givenName', + 'custom:role': 'role', }, }, }); @@ -359,11 +357,9 @@ describe('User', () => { CustomValue: 'y', nestedCamelKey: { innerCamelKey: 'z' }, }, - saml: { - attribute_mapping: { - emailAddress: 'mail', - firstName: 'givenName', - }, + saml_attribute_mapping: { + emailAddress: 'mail', + firstName: 'givenName', }, }, }); From 3c5001961abbdb80f5d1db0378c2c94a7c6943f8 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 16:26:15 -0300 Subject: [PATCH 08/22] refactor(ui): add fill prop to ConfigureSSO Step.Section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step.Body already fills the vertical space between header and footer, but Step.Section as a flex item defaults to flex:0 — so sections inside the body shrink to content height unless told to grow. Until now each sub-step had to repeat sx={{ flex: 1 }} on its Step.Section. Defaulting flex:1 on Step.Section doesn't work because Step.Header reuses the same primitive internally and needs to stay content-height, and a single sub-step may stack multiple Sections where only one should fill. Adds an opt-in fill boolean prop. applies flex:1; the default behavior stays unchanged. Updates the four Configure sub-step bodies to use the new prop. Other consumers (VerifyDomain, SelectProvider) keep the old sx={{ flex: 1 }} pattern and can adopt the prop in follow-ups. --- .../src/components/ConfigureSSO/elements/Step.tsx | 14 +++++++++++--- .../ConfigureSSO/steps/ConfigureStep.tsx | 11 +++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/elements/Step.tsx b/packages/ui/src/components/ConfigureSSO/elements/Step.tsx index 10d39a377d1..b5d58e422a7 100644 --- a/packages/ui/src/components/ConfigureSSO/elements/Step.tsx +++ b/packages/ui/src/components/ConfigureSSO/elements/Step.tsx @@ -26,12 +26,20 @@ const Layout = ({ sx, ...props }: StepLayoutProps): JSX.Element => ( /> ); -type StepSectionProps = PropsOfComponent; +type StepSectionProps = PropsOfComponent & { + /** + * When true, the section grows to fill its parent's remaining vertical + * space (flex: 1). Defaults to false so the section sizes to its content + * — required for Step.Header, multi-section sub-steps, and other places + * where a section should stay natural-height. + */ + fill?: boolean; +}; -const Section = ({ sx, ...props }: StepSectionProps): JSX.Element => ( +const Section = ({ fill, sx, ...props }: StepSectionProps): JSX.Element => ( ({ padding: theme.space.$5 }), sx]} + sx={[theme => ({ padding: theme.space.$5 }), fill && { flex: 1 }, sx]} /> ); diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index ab3b1657e3f..96a5b983008 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -61,7 +61,7 @@ export const CreateAppSubStep = (): JSX.Element => { return ( <> - + UI goes here @@ -84,7 +84,7 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { return ( <> - + UI goes here @@ -107,7 +107,7 @@ export const AssignUsersSubStep = (): JSX.Element => { return ( <> - + UI goes here @@ -160,7 +160,10 @@ export const SubmitSamlConfigSubStep = (): JSX.Element => { return ( <> - ({ flex: 1, gap: theme.space.$5 })}> + ({ gap: theme.space.$5 })} + > Date: Tue, 12 May 2026 16:48:38 -0300 Subject: [PATCH 09/22] feat(ui): build CreateAppSubStep content in ConfigureSSO Configure step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the placeholder body of the first inner sub-step with three stacked content groups: 1. Create new Okta app — section heading + bulleted list of 5 Okta admin-console steps (Sign in to Okta, click Create App Integration, select SAML 2.0, fill General Settings, click Next). 2. Configure service provider — section heading + 2 description paragraphs + 2 read-only copy rows for the SP-side ACS URL and Audience URI. Values pull from connection.samlConnection.acsUrl and spEntityId in the provider context. Uses the existing ClipboardInput primitive so each row gets a copy-to-clipboard button. 3. Complete SAML integration — section heading + bulleted list of 2 follow-up Okta admin-console steps. All three groups live in one Step.Section fill with a generous gap so the body scrolls naturally if needed; Step.Header keeps the only border-bottom separator. Bold keywords inside instruction lines (Admin → Applications, Create App Integration, SAML 2.0, etc.) are split into prefix / bold / suffix localization keys per line. Clerk's localization helper only supports {{token}} string interpolation, so this keeps the bold span themable through the existing Text primitive while still letting translators see each instruction line as discrete units. Locale keys added under configureSSO.configureStep.createApp in en-US. --- packages/localizations/src/en-US.ts | 56 +++++++ packages/shared/src/types/localization.ts | 54 ++++++ .../ConfigureSSO/steps/ConfigureStep.tsx | 157 +++++++++++++++++- 3 files changed, 264 insertions(+), 3 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index b4a1d2ad4ac..6eef5d9748a 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -250,6 +250,62 @@ export const enUS: LocalizationResource = { configureStep: { title: 'Configure Okta Workforce', subtitle: 'Create a new enterprise application in your Okta Dashboard', + createApp: { + createApp: { + title: 'Create a new enterprise application in Okta', + step1: { + prefix: 'Sign in to Okta and go to ', + bold: 'Admin → Applications', + suffix: '.', + }, + step2: { + prefix: 'Click ', + bold: 'Create App Integration', + suffix: '.', + }, + step3: { + prefix: 'Select ', + bold: 'SAML 2.0', + suffix: '.', + }, + step4: { + prefix: 'Fill in the ', + bold: 'General Settings', + suffix: ' (App name is required).', + }, + step5: { + prefix: 'Click ', + bold: 'Next', + suffix: ' to complete creating the application.', + }, + }, + serviceProvider: { + title: 'Configure service provider', + paragraph1: + 'Once you have moved forward from the General Settings instructions, you will be presented with the Configure SAML page.', + paragraph2: + 'To configure your service provider (Clerk), you must add these two fields to your Okta application:', + acsUrl: { + label: 'Single sign-on URL', + }, + spEntityId: { + label: 'Audience URI', + }, + }, + completeSamlIntegration: { + title: 'Complete SAML integration', + step1: { + prefix: 'Select ', + bold: 'This is an internal app that we have created', + suffix: ' from the options menu.', + }, + step2: { + prefix: 'Complete the form with any comments and select ', + bold: '"Finish"', + suffix: '.', + }, + }, + }, metadataUrl: { label: 'Metadata URL', <<<<<<< HEAD diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index e434adc3c7f..7d977117076 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1341,6 +1341,60 @@ export type __internal_LocalizationResource = { configureStep: { title: LocalizationValue; subtitle: LocalizationValue; + createApp: { + createApp: { + title: LocalizationValue; + step1: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step2: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step3: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step4: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step5: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + }; + serviceProvider: { + title: LocalizationValue; + paragraph1: LocalizationValue; + paragraph2: LocalizationValue; + acsUrl: { + label: LocalizationValue; + }; + spEntityId: { + label: LocalizationValue; + }; + }; + completeSamlIntegration: { + title: LocalizationValue; + step1: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step2: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + }; + }; metadataUrl: { label: LocalizationValue; placeholder: LocalizationValue; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 96a5b983008..a33c7746393 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -1,4 +1,5 @@ -import { descriptors, Flow, localizationKeys, Text } from '@/customizables'; +import { Col, descriptors, Flow, Heading, type LocalizationKey, localizationKeys, Text } from '@/customizables'; +import { ClipboardInput } from '@/elements/ClipboardInput'; import { useCardState } from '@/elements/contexts'; import { Form } from '@/elements/Form'; import { handleError } from '@/utils/errorHandler'; @@ -56,13 +57,163 @@ const InnerStepCounter = (): JSX.Element => { ); }; +type InstructionStepKeys = { + prefix: LocalizationKey; + bold: LocalizationKey; + suffix: LocalizationKey; +}; + +const InstructionStep = ({ prefix, bold, suffix }: InstructionStepKeys): JSX.Element => ( + ({ color: theme.colors.$colorMutedForeground })} + > + + ({ fontWeight: theme.fontWeights.$semibold, color: theme.colors.$colorForeground })} + localizationKey={bold} + /> + + +); + export const CreateAppSubStep = (): JSX.Element => { const { goNext, goPrev, isFirstStep, isLastStep } = useWizard(); + const { enterpriseConnection } = useConfigureSSO(); + + const acsUrl = enterpriseConnection?.samlConnection?.acsUrl ?? ''; + const spEntityId = enterpriseConnection?.samlConnection?.spEntityId ?? ''; return ( <> - - UI goes here + ({ gap: theme.space.$6 })} + > + ({ gap: theme.space.$3 })}> + + ({ + gap: theme.space.$1, + margin: 0, + paddingInlineStart: theme.space.$4, + listStyleType: 'disc', + })} + > + + + + + + + + + ({ gap: theme.space.$3 })}> + + ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph1')} + /> + ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph2')} + /> + ({ gap: theme.space.$2 })}> + ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.acsUrl.label')} + /> + + + ({ gap: theme.space.$2 })}> + ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorForeground })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.createApp.serviceProvider.spEntityId.label', + )} + /> + + + + + ({ gap: theme.space.$3 })}> + + ({ + gap: theme.space.$1, + margin: 0, + paddingInlineStart: theme.space.$4, + listStyleType: 'disc', + })} + > + + + + From 4069203bfd3b9bf8199497f8a837a3ddb56c1b79 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 17:42:43 -0300 Subject: [PATCH 10/22] refactor(ui): tighten Configure step layout and form-wrap SP copy rows Layout tightening across the inner Configure wizard: - Move Step.Body inside each sub-step component so the wizard switches bodies cleanly between sub-steps instead of nesting Wizard.Step children under a single outer Step.Body. - Wrap the ACS URL and Audience URI copy rows in Form.ControlRow + Form.CommonInputWrapper + ClipboardInput so the rows reuse the standard form chrome (label rendering, error slot, spacing) and the ClipboardInput primitive's readOnly + copyIcon/copiedIcon API. Adds 'acsUrl' to the FieldId union to back the new useFormControl call sites. - Bring group headings down to textVariant='subtitle' so the body reads as supporting content under the existing Step.Header title. - Tighten vertical spacing: Step.Section gap drops from $6 to $5, inner-group heading-to-content gap from $3 to $1x5, list-item gap from $1 to $1x5. - Soften the bold span in instructional lines from $semibold + $colorForeground to $medium + $colorMutedForeground so the emphasis feels like keyword highlighting rather than full bold. --- packages/shared/src/types/elementIds.ts | 1 + .../ConfigureSSO/steps/ConfigureStep.tsx | 287 +++++++++--------- 2 files changed, 152 insertions(+), 136 deletions(-) diff --git a/packages/shared/src/types/elementIds.ts b/packages/shared/src/types/elementIds.ts index 95ef95a6247..bbd791603cb 100644 --- a/packages/shared/src/types/elementIds.ts +++ b/packages/shared/src/types/elementIds.ts @@ -27,6 +27,7 @@ export type FieldId = | 'apiKeyRevokeConfirmation' | 'apiKeySecret' | 'idpMetadataUrl' + | 'acsUrl' | 'web3WalletName'; export type ProfileSectionId = | 'profile' diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index a33c7746393..c77973bf333 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -2,6 +2,7 @@ import { Col, descriptors, Flow, Heading, type LocalizationKey, localizationKeys import { ClipboardInput } from '@/elements/ClipboardInput'; import { useCardState } from '@/elements/contexts'; import { Form } from '@/elements/Form'; +import { Check, ClipboardOutline } from '@/icons'; import { handleError } from '@/utils/errorHandler'; import { useFormControl } from '@/utils/useFormControl'; @@ -24,23 +25,21 @@ export const ConfigureStep = (): JSX.Element => { - - - - + + + - - - + + + - - - + + + - - - - + + + @@ -79,7 +78,7 @@ const InstructionStep = ({ prefix, bold, suffix }: InstructionStepKeys): JSX.Ele as='span' variant='body' colorScheme='inherit' - sx={theme => ({ fontWeight: theme.fontWeights.$semibold, color: theme.colors.$colorForeground })} + sx={theme => ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorMutedForeground })} localizationKey={bold} /> { const acsUrl = enterpriseConnection?.samlConnection?.acsUrl ?? ''; const spEntityId = enterpriseConnection?.samlConnection?.spEntityId ?? ''; + const acsUrlField = useFormControl('acsUrl', acsUrl, { + type: 'text', + label: localizationKeys('configureSSO.configureStep.createApp.serviceProvider.acsUrl.label'), + isRequired: false, + }); + const spEntityIdField = useFormControl('acsUrl', spEntityId, { + type: 'text', + label: localizationKeys('configureSSO.configureStep.createApp.serviceProvider.spEntityId.label'), + isRequired: false, + }); + return ( <> - ({ gap: theme.space.$6 })} - > - ({ gap: theme.space.$3 })}> - - ({ - gap: theme.space.$1, - margin: 0, - paddingInlineStart: theme.space.$4, - listStyleType: 'disc', - })} - > - - - - - + ({ gap: theme.space.$5 })}> + ({ gap: theme.space.$1x5 })}> + + ({ + gap: theme.space.$1x5, + margin: 0, + paddingInlineStart: theme.space.$4, + listStyleType: 'disc', + })} + > + + + + + + - - ({ gap: theme.space.$3 })}> - - ({ color: theme.colors.$colorMutedForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph1')} - /> - ({ color: theme.colors.$colorMutedForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph2')} - /> - ({ gap: theme.space.$2 })}> + ({ gap: theme.space.$1x5 })}> + ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.acsUrl.label')} + sx={theme => ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph1')} /> - - - ({ gap: theme.space.$2 })}> ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorForeground })} - localizationKey={localizationKeys( - 'configureSSO.configureStep.createApp.serviceProvider.spEntityId.label', - )} + sx={theme => ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph2')} /> - - - ({ gap: theme.space.$3 })}> - - ({ - gap: theme.space.$1, - margin: 0, - paddingInlineStart: theme.space.$4, - listStyleType: 'disc', - })} - > - + + + + + + + - + + ({ gap: theme.space.$1x5 })}> + + ({ + gap: theme.space.$1x5, + margin: 0, + paddingInlineStart: theme.space.$4, + listStyleType: 'disc', + })} + > + + + - - + + { return ( <> - - UI goes here - + + + UI goes here + + { return ( <> - - UI goes here - + + + UI goes here + + { return ( <> - ({ gap: theme.space.$5 })} - > - ({ color: theme.colors.$colorMutedForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.metadataUrl.description')} - /> - - - - + + ({ gap: theme.space.$5 })} + > + ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys('configureSSO.configureStep.metadataUrl.description')} + /> + + + + + Date: Tue, 12 May 2026 21:29:26 -0300 Subject: [PATCH 11/22] feat(ui): build ConfigureAttributesSubStep content in ConfigureSSO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the placeholder body of the second inner sub-step with two stacked content groups: 1. SAML attribute mapping — section heading + a 3-row attribute table built from the Table primitives (Thead / Tbody / Tr / Th / Td) and rendered with monospace claim-name cells. Each row pairs the attribute label with a Badge: warning colorScheme for the required Email row, secondary colorScheme for the optional First/Last name rows. Claim names render in an inline code span using the same monospace + neutralAlpha100 background + small radius styling as the InstructionStepWithCode helper. 2. Verify the attribute mappings in Okta — description paragraph + a numbered ordered list of 9 Okta admin-console steps. Shape-A lines (1, 4, 7) use the existing InstructionStep helper (prefix / bold / suffix); Shape-B lines (2, 3, 5, 6, 8, 9) use the new InstructionStepWithCode helper (prefix / bold / middle / code / suffix) so the mail / firstName / lastName values render as inline code spans. Mirrors the layout conventions established in the sibling sub-step (Step.Body inside the sub-step, single Step.Section with gap $5, inner groups in Cols with gap $1x5, headings as textVariant subtitle, medium-weight muted bold span). Adds the matching locale type entries and English copy under configureSSO.configureStep.configureAttributes. --- packages/localizations/src/en-US.ts | 88 ++++++ packages/shared/src/types/localization.ts | 87 ++++++ .../ConfigureSSO/steps/ConfigureStep.tsx | 293 +++++++++++++++++- 3 files changed, 465 insertions(+), 3 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 6eef5d9748a..c3e5403a135 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -306,6 +306,94 @@ export const enUS: LocalizationResource = { }, }, }, + configureAttributes: { + attributeMapping: { + title: 'We expect your SAML responses to have the following specific attributes:', + columns: { + attribute: 'Attribute', + claimName: 'Claim Name', + }, + badges: { + required: 'Required', + optional: 'Optional', + }, + rows: { + email: { + attribute: 'Email address', + claim: 'user.profile.email', + }, + firstName: { + attribute: 'First Name', + claim: 'user.firstName', + }, + lastName: { + attribute: 'Last Name', + claim: 'user.lastName', + }, + }, + }, + verifyMappings: { + paragraph: + "These are the defaults and probably won't need you to change them. However, many SAML configuration errors are due to incorrect attribute mappings, so it's worth double-checking. Here's how:", + step1: { + prefix: 'In the Okta dashboard, find the ', + bold: 'Attribute Statements', + suffix: ' section.', + }, + step2: { + prefix: 'For the ', + bold: 'Name', + middle: ' field, enter ', + code: 'mail', + suffix: '', + }, + step3: { + prefix: 'For the ', + bold: 'Value', + middle: ' field, choose ', + code: 'user.profile.mail', + suffix: ' from the dropdown.', + }, + step4: { + prefix: 'Select the ', + bold: 'Add Another', + suffix: ' button to add another attribute.', + }, + step5: { + prefix: 'For the ', + bold: 'Name', + middle: ' field, enter ', + code: 'firstName', + suffix: '', + }, + step6: { + prefix: 'For the ', + bold: 'Value', + middle: ' field, choose ', + code: 'user.firstName', + suffix: ' from the dropdown.', + }, + step7: { + prefix: 'Select the ', + bold: 'Add Another', + suffix: ' button to add another attribute.', + }, + step8: { + prefix: 'For the ', + bold: 'Name', + middle: ' field, enter ', + code: 'lastName', + suffix: '', + }, + step9: { + prefix: 'For the ', + bold: 'Value', + middle: ' field, choose ', + code: 'user.lastName', + suffix: ' from the dropdown.', + }, + }, + }, metadataUrl: { label: 'Metadata URL', <<<<<<< HEAD diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 7d977117076..927bfa8a649 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1395,6 +1395,93 @@ export type __internal_LocalizationResource = { }; }; }; + configureAttributes: { + attributeMapping: { + title: LocalizationValue; + columns: { + attribute: LocalizationValue; + claimName: LocalizationValue; + }; + badges: { + required: LocalizationValue; + optional: LocalizationValue; + }; + rows: { + email: { + attribute: LocalizationValue; + claim: LocalizationValue; + }; + firstName: { + attribute: LocalizationValue; + claim: LocalizationValue; + }; + lastName: { + attribute: LocalizationValue; + claim: LocalizationValue; + }; + }; + }; + verifyMappings: { + paragraph: LocalizationValue; + step1: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step2: { + prefix: LocalizationValue; + bold: LocalizationValue; + middle: LocalizationValue; + code: LocalizationValue; + suffix: LocalizationValue; + }; + step3: { + prefix: LocalizationValue; + bold: LocalizationValue; + middle: LocalizationValue; + code: LocalizationValue; + suffix: LocalizationValue; + }; + step4: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step5: { + prefix: LocalizationValue; + bold: LocalizationValue; + middle: LocalizationValue; + code: LocalizationValue; + suffix: LocalizationValue; + }; + step6: { + prefix: LocalizationValue; + bold: LocalizationValue; + middle: LocalizationValue; + code: LocalizationValue; + suffix: LocalizationValue; + }; + step7: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step8: { + prefix: LocalizationValue; + bold: LocalizationValue; + middle: LocalizationValue; + code: LocalizationValue; + suffix: LocalizationValue; + }; + step9: { + prefix: LocalizationValue; + bold: LocalizationValue; + middle: LocalizationValue; + code: LocalizationValue; + suffix: LocalizationValue; + }; + }; + }; metadataUrl: { label: LocalizationValue; placeholder: LocalizationValue; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index c77973bf333..3914e3f07a8 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -1,4 +1,20 @@ -import { Col, descriptors, Flow, Heading, type LocalizationKey, localizationKeys, Text } from '@/customizables'; +import { + Badge, + Col, + descriptors, + Flex, + Flow, + Heading, + type LocalizationKey, + localizationKeys, + Table, + Tbody, + Td, + Text, + Th, + Thead, + Tr, +} from '@/customizables'; import { ClipboardInput } from '@/elements/ClipboardInput'; import { useCardState } from '@/elements/contexts'; import { Form } from '@/elements/Form'; @@ -90,6 +106,61 @@ const InstructionStep = ({ prefix, bold, suffix }: InstructionStepKeys): JSX.Ele ); +type InstructionStepWithCodeKeys = { + prefix: LocalizationKey; + bold: LocalizationKey; + middle: LocalizationKey; + code: LocalizationKey; + suffix: LocalizationKey; +}; + +const InstructionStepWithCode = ({ prefix, bold, middle, code, suffix }: InstructionStepWithCodeKeys): JSX.Element => ( + ({ color: theme.colors.$colorMutedForeground })} + > + + ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorMutedForeground })} + localizationKey={bold} + /> + + ({ + fontFamily: 'monospace', + fontSize: theme.fontSizes.$sm, + backgroundColor: theme.colors.$neutralAlpha100, + borderRadius: theme.radii.$sm, + padding: `${theme.space.$0x25} ${theme.space.$1}`, + })} + localizationKey={code} + /> + + +); + export const CreateAppSubStep = (): JSX.Element => { const { goNext, goPrev, isFirstStep, isLastStep } = useWizard(); const { enterpriseConnection } = useConfigureSSO(); @@ -245,8 +316,224 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { return ( <> - - UI goes here + ({ gap: theme.space.$5 })}> + ({ gap: theme.space.$1x5 })}> + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ ({ gap: theme.space.$2 })} + > + + + + + ({ + fontFamily: 'monospace', + fontSize: theme.fontSizes.$sm, + backgroundColor: theme.colors.$neutralAlpha100, + borderRadius: theme.radii.$sm, + padding: `${theme.space.$0x25} ${theme.space.$1}`, + })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.configureAttributes.attributeMapping.rows.email.claim', + )} + /> +
+ ({ gap: theme.space.$2 })} + > + + + + + ({ + fontFamily: 'monospace', + fontSize: theme.fontSizes.$sm, + backgroundColor: theme.colors.$neutralAlpha100, + borderRadius: theme.radii.$sm, + padding: `${theme.space.$0x25} ${theme.space.$1}`, + })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.configureAttributes.attributeMapping.rows.firstName.claim', + )} + /> +
+ ({ gap: theme.space.$2 })} + > + + + + + ({ + fontFamily: 'monospace', + fontSize: theme.fontSizes.$sm, + backgroundColor: theme.colors.$neutralAlpha100, + borderRadius: theme.radii.$sm, + padding: `${theme.space.$0x25} ${theme.space.$1}`, + })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.configureAttributes.attributeMapping.rows.lastName.claim', + )} + /> +
+ + + ({ gap: theme.space.$1x5 })}> + ({ color: theme.colors.$colorMutedForeground })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.configureAttributes.verifyMappings.paragraph', + )} + /> + ({ + gap: theme.space.$1x5, + margin: 0, + paddingInlineStart: theme.space.$4, + listStyleType: 'decimal', + })} + > + + + + + + + + + + +
From 890a71098f597bc1bbc6a9b5ff2ec986132b094f Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 23:02:38 -0300 Subject: [PATCH 12/22] refactor(ui): adopt Text colorScheme=secondary and tighten attribute table styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces sx={{ color: $colorMutedForeground }} with Text colorScheme='secondary' across ConfigureAttributesSubStep — the prop already resolves to the same color token, so the inline sx call drops out cleanly. Tightens the attribute mapping table chrome: column headers shrink to fontSize=$xs, the first column picks up an inline-start pad so the leading cell breathes against the table edge, and the claim-name cells reduce to fontFamily: monospace only (drops the background, border-radius, and padding from the earlier 'code chip' treatment for a flatter look that reads as data, not as inline code). Inner Cols inside ConfigureAttributesSubStep step up from gap $1x5 to $3. Numbered/bulleted list indents grow from paddingInlineStart $4 to $5. --- .../ConfigureSSO/steps/ConfigureStep.tsx | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 3914e3f07a8..8f43e0424fe 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -194,7 +194,7 @@ export const CreateAppSubStep = (): JSX.Element => { sx={theme => ({ gap: theme.space.$1x5, margin: 0, - paddingInlineStart: theme.space.$4, + paddingInlineStart: theme.space.$5, listStyleType: 'disc', })} > @@ -317,7 +317,7 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { <> ({ gap: theme.space.$5 })}> - ({ gap: theme.space.$1x5 })}> + ({ gap: theme.space.$3 })}> { 'configureSSO.configureStep.configureAttributes.attributeMapping.title', )} /> - + +
({ + 'tr > th:first-of-type': { + paddingInlineStart: theme.space.$4, + }, + })} + > + + + + + + +
({ fontSize: theme.fontSizes.$xs })} localizationKey={localizationKeys( 'configureSSO.configureStep.configureAttributes.attributeMapping.columns.attribute', )} /> ({ fontSize: theme.fontSizes.$xs })} localizationKey={localizationKeys( 'configureSSO.configureStep.configureAttributes.attributeMapping.columns.claimName', )} @@ -344,19 +354,22 @@ export const ConfigureAttributesSubStep = (): JSX.Element => {
({ gap: theme.space.$2 })} > + { /> ({ - fontFamily: 'monospace', - fontSize: theme.fontSizes.$sm, - backgroundColor: theme.colors.$neutralAlpha100, - borderRadius: theme.radii.$sm, - padding: `${theme.space.$0x25} ${theme.space.$1}`, - })} + sx={{ fontFamily: 'monospace' }} localizationKey={localizationKeys( 'configureSSO.configureStep.configureAttributes.attributeMapping.rows.email.claim', )} />
({ gap: theme.space.$2 })} > + ({ - fontFamily: 'monospace', - fontSize: theme.fontSizes.$sm, - backgroundColor: theme.colors.$neutralAlpha100, - borderRadius: theme.radii.$sm, - padding: `${theme.space.$0x25} ${theme.space.$1}`, - })} + sx={{ fontFamily: 'monospace' }} localizationKey={localizationKeys( 'configureSSO.configureStep.configureAttributes.attributeMapping.rows.firstName.claim', )} />
({ gap: theme.space.$2 })} > + ({ - fontFamily: 'monospace', - fontSize: theme.fontSizes.$sm, - backgroundColor: theme.colors.$neutralAlpha100, - borderRadius: theme.radii.$sm, - padding: `${theme.space.$0x25} ${theme.space.$1}`, - })} + sx={{ fontFamily: 'monospace' }} localizationKey={localizationKeys( 'configureSSO.configureStep.configureAttributes.attributeMapping.rows.lastName.claim', )} @@ -457,21 +459,21 @@ export const ConfigureAttributesSubStep = (): JSX.Element => {
- ({ gap: theme.space.$1x5 })}> + ({ gap: theme.space.$3 })}> ({ color: theme.colors.$colorMutedForeground })} + colorScheme='secondary' localizationKey={localizationKeys( 'configureSSO.configureStep.configureAttributes.verifyMappings.paragraph', )} /> + ({ gap: theme.space.$1x5, margin: 0, - paddingInlineStart: theme.space.$4, + paddingInlineStart: theme.space.$5, listStyleType: 'decimal', })} > From 66994c23e64c899f40c2c533b75eb3d4eaa7db3e Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 23:16:14 -0300 Subject: [PATCH 13/22] refactor(ui): scope ConfigureSSO Configure locales by provider and adopt inline rich-text markup Restructures localization keys under configureSSO.configureStep so future SAML providers (Custom SAML) and OIDC can drop in alongside Okta without duplicating shared copy: - spFields and attributeMapping live at the top level since their labels, table content, badges, and the "These are the defaults..." paragraph read the same regardless of provider. - samlOkta now owns provider-specific copy: title, subtitle, createApp walkthrough, serviceProvider narrative, completeSamlIntegration steps, configureAttributes pairs, metadataUrl. When Custom SAML lands, a sibling samlCustom namespace mirrors this shape. Replaces the InstructionStep and InstructionStepWithCode helpers (which required 3 or 5 separate keys per sentence) with a single RichText component that parses inline ... and ... markup in a localized string. One key per sentence, translators see the whole context, emphasis stays themable through Text spans. ConfigureAttributesSubStep redesigned to match Figma 8032:14794: - Claim names in the attribute mapping table are now user.email, user.firstName, user.lastName (corrects user.profile.email). - The verify-mappings list collapses from 9 separate numbered steps to 2, with a nested bulleted sub-list of the name/expression pairs the user enters in Okta. - Table rows render from an ATTRIBUTE_ROWS constant so the row markup isn't duplicated three times. - Verify-mappings pairs render from an ATTRIBUTE_PAIRS constant for the same reason. Sweeps remaining sx={{ color: $colorMutedForeground }} call sites in the Configure sub-steps over to Text colorScheme='secondary' to match the pattern used elsewhere in the file. --- packages/localizations/src/en-US.ts | 215 ++----- packages/shared/src/types/localization.ts | 186 ++---- .../ConfigureSSO/steps/ConfigureStep.tsx | 539 ++++++++---------- 3 files changed, 345 insertions(+), 595 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index c3e5403a135..608b0e537a7 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -223,186 +223,81 @@ export const enUS: LocalizationResource = { }, warning: 'Once a provider is selected you cannot change again until the configuration is over', }, - verifyEmailDomainStep: { - title: 'Verify email address', - subtitle: 'Verify the email address you want to enable the enterprise connection on.', - addEmailAddress: { - formTitle: 'We need your email', - formSubtitle: 'In order to start we will need your email address', - inputPlaceholder: 'name@company.com', - inputLabel: 'Email address', - }, - emailCode: { - formTitle: 'Verify your email address', - formSubtitle: 'Enter the verification code sent to {{identifier}}', - resendButton: "Didn't receive a code? Resend", - verified: { - title: 'We got your email', - subtitle: "You've verified your email address with the following email", - inputLabel: 'Verified email address', + configureStep: { + spFields: { + acsUrl: { + label: 'Single sign-on URL', + }, + spEntityId: { + label: 'Audience URI', }, }, - domainTaken: { - title: 'This domain ({{domain}}) already has an SSO connection', - subtitle: "Contact the application's administrator to get access through the existing connection.", - }, - }, - configureStep: { - title: 'Configure Okta Workforce', - subtitle: 'Create a new enterprise application in your Okta Dashboard', - createApp: { - createApp: { - title: 'Create a new enterprise application in Okta', - step1: { - prefix: 'Sign in to Okta and go to ', - bold: 'Admin → Applications', - suffix: '.', - }, - step2: { - prefix: 'Click ', - bold: 'Create App Integration', - suffix: '.', - }, - step3: { - prefix: 'Select ', - bold: 'SAML 2.0', - suffix: '.', + attributeMapping: { + title: 'We expect your SAML responses to have the following specific attributes:', + paragraph: + "These are the defaults and probably won't need you to change them. However, many SAML configuration errors are due to incorrect attribute mappings, so it's worth double-checking. Here's how:", + columns: { + attribute: 'Attribute', + claimName: 'Claim Name', + }, + badges: { + required: 'Required', + optional: 'Optional', + }, + rows: { + email: { + attribute: 'Email address', + claim: 'user.email', }, - step4: { - prefix: 'Fill in the ', - bold: 'General Settings', - suffix: ' (App name is required).', + firstName: { + attribute: 'First Name', + claim: 'user.firstName', }, - step5: { - prefix: 'Click ', - bold: 'Next', - suffix: ' to complete creating the application.', + lastName: { + attribute: 'Last Name', + claim: 'user.lastName', }, }, + }, + samlOkta: { + title: 'Configure Okta Workforce', + subtitle: 'Create a new enterprise application in your Okta Dashboard', + createApp: { + title: 'Create a new enterprise application in Okta', + step1: 'Sign in to Okta and go to Admin → Applications.', + step2: 'Click Create App Integration.', + step3: 'Select SAML 2.0.', + step4: 'Fill in the General Settings (App name is required).', + step5: 'Click Next to complete creating the application.', + }, serviceProvider: { title: 'Configure service provider', paragraph1: 'Once you have moved forward from the General Settings instructions, you will be presented with the Configure SAML page.', paragraph2: 'To configure your service provider (Clerk), you must add these two fields to your Okta application:', - acsUrl: { - label: 'Single sign-on URL', - }, - spEntityId: { - label: 'Audience URI', - }, }, completeSamlIntegration: { title: 'Complete SAML integration', - step1: { - prefix: 'Select ', - bold: 'This is an internal app that we have created', - suffix: ' from the options menu.', - }, - step2: { - prefix: 'Complete the form with any comments and select ', - bold: '"Finish"', - suffix: '.', - }, + step1: 'Select This is an internal app that we have created from the options menu.', + step2: 'Complete the form with any comments and select "Finish".', }, - }, - configureAttributes: { - attributeMapping: { - title: 'We expect your SAML responses to have the following specific attributes:', - columns: { - attribute: 'Attribute', - claimName: 'Claim Name', - }, - badges: { - required: 'Required', - optional: 'Optional', - }, - rows: { - email: { - attribute: 'Email address', - claim: 'user.profile.email', - }, - firstName: { - attribute: 'First Name', - claim: 'user.firstName', - }, - lastName: { - attribute: 'Last Name', - claim: 'user.lastName', - }, + configureAttributes: { + step1: 'In the Okta dashboard, find the Attribute Statements section.', + step2: + 'Select Add Expression for each attribute, and enter the following name and expression pairs:', + pairs: { + email: 'mail and user.profile.mail', + firstName: 'firstName and user.profile.firstName', + lastName: 'lastName and user.profile.lastName', }, }, - verifyMappings: { - paragraph: - "These are the defaults and probably won't need you to change them. However, many SAML configuration errors are due to incorrect attribute mappings, so it's worth double-checking. Here's how:", - step1: { - prefix: 'In the Okta dashboard, find the ', - bold: 'Attribute Statements', - suffix: ' section.', - }, - step2: { - prefix: 'For the ', - bold: 'Name', - middle: ' field, enter ', - code: 'mail', - suffix: '', - }, - step3: { - prefix: 'For the ', - bold: 'Value', - middle: ' field, choose ', - code: 'user.profile.mail', - suffix: ' from the dropdown.', - }, - step4: { - prefix: 'Select the ', - bold: 'Add Another', - suffix: ' button to add another attribute.', - }, - step5: { - prefix: 'For the ', - bold: 'Name', - middle: ' field, enter ', - code: 'firstName', - suffix: '', - }, - step6: { - prefix: 'For the ', - bold: 'Value', - middle: ' field, choose ', - code: 'user.firstName', - suffix: ' from the dropdown.', - }, - step7: { - prefix: 'Select the ', - bold: 'Add Another', - suffix: ' button to add another attribute.', - }, - step8: { - prefix: 'For the ', - bold: 'Name', - middle: ' field, enter ', - code: 'lastName', - suffix: '', - }, - step9: { - prefix: 'For the ', - bold: 'Value', - middle: ' field, choose ', - code: 'user.lastName', - suffix: ' from the dropdown.', - }, + metadataUrl: { + label: 'Metadata URL', + placeholder: 'Paste URL here...', + description: 'In your Okta SAML app, go to the Sign On tab and retrieve the metadata URL. Paste it below.', }, }, - metadataUrl: { - label: 'Metadata URL', -<<<<<<< HEAD - placeholder: 'https://app.okta.com/.../metadata', -======= - placeholder: 'Paste URL here...', ->>>>>>> 35011671b (fix(ui): render Configure step description above input) - description: 'In your Okta SAML app, go to the Sign On tab and retrieve the metadata URL. Paste it below.', - }, }, }, createOrganization: { diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 927bfa8a649..608a4d5d3a8 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1339,154 +1339,76 @@ export type __internal_LocalizationResource = { }; }; configureStep: { - title: LocalizationValue; - subtitle: LocalizationValue; - createApp: { - createApp: { - title: LocalizationValue; - step1: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step2: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step3: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; + spFields: { + acsUrl: { + label: LocalizationValue; + }; + spEntityId: { + label: LocalizationValue; + }; + }; + attributeMapping: { + title: LocalizationValue; + paragraph: LocalizationValue; + columns: { + attribute: LocalizationValue; + claimName: LocalizationValue; + }; + badges: { + required: LocalizationValue; + optional: LocalizationValue; + }; + rows: { + email: { + attribute: LocalizationValue; + claim: LocalizationValue; }; - step4: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; + firstName: { + attribute: LocalizationValue; + claim: LocalizationValue; }; - step5: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; + lastName: { + attribute: LocalizationValue; + claim: LocalizationValue; }; }; + }; + samlOkta: { + title: LocalizationValue; + subtitle: LocalizationValue; + createApp: { + title: LocalizationValue; + step1: LocalizationValue; + step2: LocalizationValue; + step3: LocalizationValue; + step4: LocalizationValue; + step5: LocalizationValue; + }; serviceProvider: { title: LocalizationValue; paragraph1: LocalizationValue; paragraph2: LocalizationValue; - acsUrl: { - label: LocalizationValue; - }; - spEntityId: { - label: LocalizationValue; - }; }; completeSamlIntegration: { title: LocalizationValue; - step1: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step2: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; + step1: LocalizationValue; + step2: LocalizationValue; }; - }; - configureAttributes: { - attributeMapping: { - title: LocalizationValue; - columns: { - attribute: LocalizationValue; - claimName: LocalizationValue; - }; - badges: { - required: LocalizationValue; - optional: LocalizationValue; - }; - rows: { - email: { - attribute: LocalizationValue; - claim: LocalizationValue; - }; - firstName: { - attribute: LocalizationValue; - claim: LocalizationValue; - }; - lastName: { - attribute: LocalizationValue; - claim: LocalizationValue; - }; + configureAttributes: { + step1: LocalizationValue; + step2: LocalizationValue; + pairs: { + email: LocalizationValue; + firstName: LocalizationValue; + lastName: LocalizationValue; }; }; - verifyMappings: { - paragraph: LocalizationValue; - step1: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step2: { - prefix: LocalizationValue; - bold: LocalizationValue; - middle: LocalizationValue; - code: LocalizationValue; - suffix: LocalizationValue; - }; - step3: { - prefix: LocalizationValue; - bold: LocalizationValue; - middle: LocalizationValue; - code: LocalizationValue; - suffix: LocalizationValue; - }; - step4: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step5: { - prefix: LocalizationValue; - bold: LocalizationValue; - middle: LocalizationValue; - code: LocalizationValue; - suffix: LocalizationValue; - }; - step6: { - prefix: LocalizationValue; - bold: LocalizationValue; - middle: LocalizationValue; - code: LocalizationValue; - suffix: LocalizationValue; - }; - step7: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step8: { - prefix: LocalizationValue; - bold: LocalizationValue; - middle: LocalizationValue; - code: LocalizationValue; - suffix: LocalizationValue; - }; - step9: { - prefix: LocalizationValue; - bold: LocalizationValue; - middle: LocalizationValue; - code: LocalizationValue; - suffix: LocalizationValue; - }; + metadataUrl: { + label: LocalizationValue; + placeholder: LocalizationValue; + description: LocalizationValue; }; }; - metadataUrl: { - label: LocalizationValue; - placeholder: LocalizationValue; - description: LocalizationValue; - }; }; }; apiKeys: { diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 8f43e0424fe..7a36d636a3a 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -14,6 +14,7 @@ import { Th, Thead, Tr, + useLocalizations, } from '@/customizables'; import { ClipboardInput } from '@/elements/ClipboardInput'; import { useCardState } from '@/elements/contexts'; @@ -35,8 +36,8 @@ export const ConfigureStep = (): JSX.Element => { > @@ -72,94 +73,105 @@ const InnerStepCounter = (): JSX.Element => { ); }; -type InstructionStepKeys = { - prefix: LocalizationKey; - bold: LocalizationKey; - suffix: LocalizationKey; +const RICH_TEXT_PATTERN = /<(strong|code)>(.*?)<\/\1>/g; + +type RichTextSegment = + | { type: 'text'; value: string } + | { type: 'strong'; value: string } + | { type: 'code'; value: string }; + +const parseRichText = (input: string): RichTextSegment[] => { + const segments: RichTextSegment[] = []; + let lastIndex = 0; + RICH_TEXT_PATTERN.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = RICH_TEXT_PATTERN.exec(input)) !== null) { + if (match.index > lastIndex) { + segments.push({ type: 'text', value: input.slice(lastIndex, match.index) }); + } + segments.push({ type: match[1] as 'strong' | 'code', value: match[2] }); + lastIndex = RICH_TEXT_PATTERN.lastIndex; + } + if (lastIndex < input.length) { + segments.push({ type: 'text', value: input.slice(lastIndex) }); + } + return segments; }; -const InstructionStep = ({ prefix, bold, suffix }: InstructionStepKeys): JSX.Element => ( - ({ color: theme.colors.$colorMutedForeground })} - > - - ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorMutedForeground })} - localizationKey={bold} - /> - - -); - -type InstructionStepWithCodeKeys = { - prefix: LocalizationKey; - bold: LocalizationKey; - middle: LocalizationKey; - code: LocalizationKey; - suffix: LocalizationKey; +const RichText = ({ localizationKey }: { localizationKey: LocalizationKey }): JSX.Element => { + const { t } = useLocalizations(); + const text = t(localizationKey); + if (!text) { + return <>; + } + return ( + <> + {parseRichText(text).map((segment, index) => { + if (segment.type === 'strong') { + return ( + ({ fontWeight: theme.fontWeights.$medium })} + > + {segment.value} + + ); + } + if (segment.type === 'code') { + return ( + + {segment.value} + + ); + } + return segment.value; + })} + + ); }; -const InstructionStepWithCode = ({ prefix, bold, middle, code, suffix }: InstructionStepWithCodeKeys): JSX.Element => ( - ({ color: theme.colors.$colorMutedForeground })} - > - - ({ fontWeight: theme.fontWeights.$medium, color: theme.colors.$colorMutedForeground })} - localizationKey={bold} - /> - - ({ - fontFamily: 'monospace', - fontSize: theme.fontSizes.$sm, - backgroundColor: theme.colors.$neutralAlpha100, - borderRadius: theme.radii.$sm, - padding: `${theme.space.$0x25} ${theme.space.$1}`, - })} - localizationKey={code} - /> - - -); +const ATTRIBUTE_ROWS = [ + { + id: 'email', + isRequired: true, + attribute: localizationKeys('configureSSO.configureStep.attributeMapping.rows.email.attribute'), + claim: localizationKeys('configureSSO.configureStep.attributeMapping.rows.email.claim'), + }, + { + id: 'firstName', + isRequired: false, + attribute: localizationKeys('configureSSO.configureStep.attributeMapping.rows.firstName.attribute'), + claim: localizationKeys('configureSSO.configureStep.attributeMapping.rows.firstName.claim'), + }, + { + id: 'lastName', + isRequired: false, + attribute: localizationKeys('configureSSO.configureStep.attributeMapping.rows.lastName.attribute'), + claim: localizationKeys('configureSSO.configureStep.attributeMapping.rows.lastName.claim'), + }, +] as const; + +const ATTRIBUTE_PAIRS = [ + { + id: 'email', + value: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.email'), + }, + { + id: 'firstName', + value: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.firstName'), + }, + { + id: 'lastName', + value: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.lastName'), + }, +] as const; export const CreateAppSubStep = (): JSX.Element => { const { goNext, goPrev, isFirstStep, isLastStep } = useWizard(); @@ -170,12 +182,12 @@ export const CreateAppSubStep = (): JSX.Element => { const acsUrlField = useFormControl('acsUrl', acsUrl, { type: 'text', - label: localizationKeys('configureSSO.configureStep.createApp.serviceProvider.acsUrl.label'), + label: localizationKeys('configureSSO.configureStep.spFields.acsUrl.label'), isRequired: false, }); const spEntityIdField = useFormControl('acsUrl', spEntityId, { type: 'text', - label: localizationKeys('configureSSO.configureStep.createApp.serviceProvider.spEntityId.label'), + label: localizationKeys('configureSSO.configureStep.spFields.spEntityId.label'), isRequired: false, }); @@ -183,11 +195,11 @@ export const CreateAppSubStep = (): JSX.Element => { <> ({ gap: theme.space.$5 })}> - ({ gap: theme.space.$1x5 })}> + ({ gap: theme.space.$3 })}> { listStyleType: 'disc', })} > - - - - - + + + + + + + + + + + + + + + - ({ gap: theme.space.$1x5 })}> + ({ gap: theme.space.$3 })}> ({ color: theme.colors.$colorMutedForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph1')} + colorScheme='secondary' + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.serviceProvider.paragraph1')} /> ({ color: theme.colors.$colorMutedForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.createApp.serviceProvider.paragraph2')} + colorScheme='secondary' + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.serviceProvider.paragraph2')} /> @@ -266,31 +281,41 @@ export const CreateAppSubStep = (): JSX.Element => { /> - ({ gap: theme.space.$1x5 })}> + ({ gap: theme.space.$3 })}> ({ gap: theme.space.$1x5, margin: 0, - paddingInlineStart: theme.space.$4, + paddingInlineStart: theme.space.$5, listStyleType: 'disc', })} > - - + + + + + + @@ -321,9 +346,7 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { { ({ fontSize: theme.fontSizes.$xs })} localizationKey={localizationKeys( - 'configureSSO.configureStep.configureAttributes.attributeMapping.columns.attribute', + 'configureSSO.configureStep.attributeMapping.columns.attribute', )} /> @@ -348,7 +371,7 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { ({ fontSize: theme.fontSizes.$xs })} localizationKey={localizationKeys( - 'configureSSO.configureStep.configureAttributes.attributeMapping.columns.claimName', + 'configureSSO.configureStep.attributeMapping.columns.claimName', )} /> @@ -356,105 +379,39 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { - - - - - - - - + + + - - - - - - - - - + + + ))}
- ({ gap: theme.space.$2 })} - > - - - - - - -
- ({ gap: theme.space.$2 })} - > + {ATTRIBUTE_ROWS.map(row => ( +
+ ({ gap: theme.space.$2 })} + > + + + + + - - - - - -
- ({ gap: theme.space.$2 })} - > - - - - - - -
@@ -463,9 +420,7 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { { listStyleType: 'decimal', })} > - - - - - - - - - + + + + + + ({ + gap: theme.space.$1, + margin: 0, + marginTop: theme.space.$1, + paddingInlineStart: theme.space.$5, + listStyleType: 'disc', + })} + > + {ATTRIBUTE_PAIRS.map(pair => ( + + + + ))} + +
@@ -585,8 +519,8 @@ export const SubmitSamlConfigSubStep = (): JSX.Element => { const metadataUrlField = useFormControl('idpMetadataUrl', '', { type: 'text', - label: localizationKeys('configureSSO.configureStep.metadataUrl.label'), - placeholder: localizationKeys('configureSSO.configureStep.metadataUrl.placeholder'), + label: localizationKeys('configureSSO.configureStep.samlOkta.metadataUrl.label'), + placeholder: localizationKeys('configureSSO.configureStep.samlOkta.metadataUrl.placeholder'), isRequired: true, }); @@ -620,9 +554,8 @@ export const SubmitSamlConfigSubStep = (): JSX.Element => { > ({ color: theme.colors.$colorMutedForeground })} - localizationKey={localizationKeys('configureSSO.configureStep.metadataUrl.description')} + colorScheme='secondary' + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.metadataUrl.description')} /> From d411528d00c0eefac3b93c89543a8015906107ee Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Wed, 13 May 2026 07:54:47 -0300 Subject: [PATCH 14/22] refactor(ui): tweak ConfigureAttributes list spacing and bullet style --- .../ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 7a36d636a3a..b78b3c9fb69 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -450,11 +450,11 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { ({ - gap: theme.space.$1, + gap: theme.space.$1x5, margin: 0, - marginTop: theme.space.$1, + marginTop: theme.space.$1x5, paddingInlineStart: theme.space.$5, - listStyleType: 'disc', + listStyleType: '"- "', })} > {ATTRIBUTE_PAIRS.map(pair => ( From b82c1be7e996063fce124cae6f49e2b1028616f2 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Wed, 13 May 2026 08:12:38 -0300 Subject: [PATCH 15/22] feat(ui): build AssignUsersSubStep and switch ConfigureSSO instructions back to multi-key inline spans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the RichText helper, parseRichText helper, and the / markup convention added in the previous commit. Each instructional line in the Configure sub-steps now uses three (or more) separate localization keys per sentence — prefix, bold, suffix — rendered inline as Text-as-span children of a single Text-as-li parent. Matches Laura's verifyEmailDomainStep precedent and uses vanilla Clerk localization primitives end-to-end. The attribute-mapping pairs in ConfigureAttributesSubStep render as Badge components rather than inline monospace code spans. Each pair is now name + conjunction + expression — three locale keys per pair, with the conjunction localized so other languages can pick their own. Adds the third inner sub-step, AssignUsersSubStep, with the same structure as the other sub-steps: section heading, description paragraph, and a numbered ordered list of five Okta admin-console steps walking the user through the Assignments tab flow. Step 2 has three emphasized keywords ("Assign", "Assign to people", "Assign to groups") and uses a seven-key inline shape; the other steps use the standard three-key shape, except step 3 which is plain prose and renders as a single Text-as-li. All new copy lives under configureSSO.configureStep.samlOkta so Custom SAML and OIDC siblings can drop in without colliding. --- packages/localizations/src/en-US.ts | 100 ++++- packages/shared/src/types/localization.ts | 99 ++++- .../ConfigureSSO/steps/ConfigureStep.tsx | 405 ++++++++++++++---- 3 files changed, 491 insertions(+), 113 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 608b0e537a7..8772eda2a28 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -264,11 +264,31 @@ export const enUS: LocalizationResource = { subtitle: 'Create a new enterprise application in your Okta Dashboard', createApp: { title: 'Create a new enterprise application in Okta', - step1: 'Sign in to Okta and go to Admin → Applications.', - step2: 'Click Create App Integration.', - step3: 'Select SAML 2.0.', - step4: 'Fill in the General Settings (App name is required).', - step5: 'Click Next to complete creating the application.', + step1: { + prefix: 'Sign in to Okta and go to ', + bold: 'Admin → Applications', + suffix: '.', + }, + step2: { + prefix: 'Click ', + bold: 'Create App Integration', + suffix: '.', + }, + step3: { + prefix: 'Select ', + bold: 'SAML 2.0', + suffix: '.', + }, + step4: { + prefix: 'Fill in the ', + bold: 'General Settings', + suffix: ' (App name is required).', + }, + step5: { + prefix: 'Click ', + bold: 'Next', + suffix: ' to complete creating the application.', + }, }, serviceProvider: { title: 'Configure service provider', @@ -279,17 +299,71 @@ export const enUS: LocalizationResource = { }, completeSamlIntegration: { title: 'Complete SAML integration', - step1: 'Select This is an internal app that we have created from the options menu.', - step2: 'Complete the form with any comments and select "Finish".', + step1: { + prefix: 'Select ', + bold: 'This is an internal app that we have created', + suffix: ' from the options menu.', + }, + step2: { + prefix: 'Complete the form with any comments and select ', + bold: '"Finish"', + suffix: '.', + }, }, configureAttributes: { - step1: 'In the Okta dashboard, find the Attribute Statements section.', - step2: - 'Select Add Expression for each attribute, and enter the following name and expression pairs:', + step1: { + prefix: 'In the Okta dashboard, find the ', + bold: 'Attribute Statements', + suffix: ' section.', + }, + step2: { + prefix: 'Select ', + bold: 'Add Expression', + suffix: ' for each attribute, and enter the following name and expression pairs:', + }, pairs: { - email: 'mail and user.profile.mail', - firstName: 'firstName and user.profile.firstName', - lastName: 'lastName and user.profile.lastName', + conjunction: ' and ', + email: { + name: 'mail', + expression: 'user.profile.mail', + }, + firstName: { + name: 'firstName', + expression: 'user.profile.firstName', + }, + lastName: { + name: 'lastName', + expression: 'user.profile.lastName', + }, + }, + }, + assignUsers: { + title: 'Assign selected user or group in Okta', + paragraph: 'You need to assign users or groups to your enterprise app before they can use it to sign in.', + step1: { + prefix: 'In the Okta dashboard, select the ', + bold: 'Assignments', + suffix: ' tab.', + }, + step2: { + prefix: 'Select the ', + bold1: 'Assign', + middle1: ' dropdown. You can either select ', + bold2: 'Assign to people', + middle2: ' or ', + bold3: 'Assign to groups', + suffix: '.', + }, + step3: 'In the search field, enter the user or group of users that you want to assign to the application.', + step4: { + prefix: 'Select the ', + bold: 'Assign', + suffix: ' button next to the user or group that you want to assign.', + }, + step5: { + prefix: 'Select the ', + bold: 'Done', + suffix: ' button to complete the assignment.', }, }, metadataUrl: { diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 608a4d5d3a8..0c72ecf790a 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1378,11 +1378,31 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; createApp: { title: LocalizationValue; - step1: LocalizationValue; - step2: LocalizationValue; - step3: LocalizationValue; - step4: LocalizationValue; - step5: LocalizationValue; + step1: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step2: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step3: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step4: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step5: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; }; serviceProvider: { title: LocalizationValue; @@ -1391,16 +1411,71 @@ export type __internal_LocalizationResource = { }; completeSamlIntegration: { title: LocalizationValue; - step1: LocalizationValue; - step2: LocalizationValue; + step1: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step2: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; }; configureAttributes: { - step1: LocalizationValue; - step2: LocalizationValue; + step1: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step2: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; pairs: { - email: LocalizationValue; - firstName: LocalizationValue; - lastName: LocalizationValue; + conjunction: LocalizationValue; + email: { + name: LocalizationValue; + expression: LocalizationValue; + }; + firstName: { + name: LocalizationValue; + expression: LocalizationValue; + }; + lastName: { + name: LocalizationValue; + expression: LocalizationValue; + }; + }; + }; + assignUsers: { + title: LocalizationValue; + paragraph: LocalizationValue; + step1: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step2: { + prefix: LocalizationValue; + bold1: LocalizationValue; + middle1: LocalizationValue; + bold2: LocalizationValue; + middle2: LocalizationValue; + bold3: LocalizationValue; + suffix: LocalizationValue; + }; + step3: LocalizationValue; + step4: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; + }; + step5: { + prefix: LocalizationValue; + bold: LocalizationValue; + suffix: LocalizationValue; }; }; metadataUrl: { diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index b78b3c9fb69..eaf22a4aa1c 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -5,7 +5,6 @@ import { Flex, Flow, Heading, - type LocalizationKey, localizationKeys, Table, Tbody, @@ -14,7 +13,6 @@ import { Th, Thead, Tr, - useLocalizations, } from '@/customizables'; import { ClipboardInput } from '@/elements/ClipboardInput'; import { useCardState } from '@/elements/contexts'; @@ -73,70 +71,6 @@ const InnerStepCounter = (): JSX.Element => { ); }; -const RICH_TEXT_PATTERN = /<(strong|code)>(.*?)<\/\1>/g; - -type RichTextSegment = - | { type: 'text'; value: string } - | { type: 'strong'; value: string } - | { type: 'code'; value: string }; - -const parseRichText = (input: string): RichTextSegment[] => { - const segments: RichTextSegment[] = []; - let lastIndex = 0; - RICH_TEXT_PATTERN.lastIndex = 0; - let match: RegExpExecArray | null; - while ((match = RICH_TEXT_PATTERN.exec(input)) !== null) { - if (match.index > lastIndex) { - segments.push({ type: 'text', value: input.slice(lastIndex, match.index) }); - } - segments.push({ type: match[1] as 'strong' | 'code', value: match[2] }); - lastIndex = RICH_TEXT_PATTERN.lastIndex; - } - if (lastIndex < input.length) { - segments.push({ type: 'text', value: input.slice(lastIndex) }); - } - return segments; -}; - -const RichText = ({ localizationKey }: { localizationKey: LocalizationKey }): JSX.Element => { - const { t } = useLocalizations(); - const text = t(localizationKey); - if (!text) { - return <>; - } - return ( - <> - {parseRichText(text).map((segment, index) => { - if (segment.type === 'strong') { - return ( - ({ fontWeight: theme.fontWeights.$medium })} - > - {segment.value} - - ); - } - if (segment.type === 'code') { - return ( - - {segment.value} - - ); - } - return segment.value; - })} - - ); -}; - const ATTRIBUTE_ROWS = [ { id: 'email', @@ -161,15 +95,18 @@ const ATTRIBUTE_ROWS = [ const ATTRIBUTE_PAIRS = [ { id: 'email', - value: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.email'), + name: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.email.name'), + expression: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.email.expression'), }, { id: 'firstName', - value: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.firstName'), + name: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.firstName.name'), + expression: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.firstName.expression'), }, { id: 'lastName', - value: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.lastName'), + name: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.lastName.name'), + expression: localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.pairs.lastName.expression'), }, ] as const; @@ -214,31 +151,106 @@ export const CreateAppSubStep = (): JSX.Element => { as='li' colorScheme='secondary' > - + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step1.bold')} + /> + - + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step2.bold')} + /> + - + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step3.bold')} + /> + - + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step4.bold')} + /> + - + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step5.bold')} + /> + @@ -300,9 +312,26 @@ export const CreateAppSubStep = (): JSX.Element => { as='li' colorScheme='secondary' > - + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.samlOkta.completeSamlIntegration.step1.bold', + )} + /> + @@ -310,9 +339,26 @@ export const CreateAppSubStep = (): JSX.Element => { as='li' colorScheme='secondary' > - + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.samlOkta.completeSamlIntegration.step2.bold', + )} + /> + @@ -436,25 +482,63 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { as='li' colorScheme='secondary' > - + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.samlOkta.configureAttributes.step1.bold', + )} + /> + - + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys( + 'configureSSO.configureStep.samlOkta.configureAttributes.step2.bold', + )} + /> + ({ - gap: theme.space.$1x5, + gap: theme.space.$1, margin: 0, - marginTop: theme.space.$1x5, + marginTop: theme.space.$1, paddingInlineStart: theme.space.$5, - listStyleType: '"- "', + listStyleType: 'disc', })} > {ATTRIBUTE_PAIRS.map(pair => ( @@ -463,7 +547,15 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { as='li' colorScheme='secondary' > - + + + ))} @@ -493,8 +585,145 @@ export const AssignUsersSubStep = (): JSX.Element => { return ( <> - - UI goes here + ({ gap: theme.space.$5 })}> + ({ gap: theme.space.$3 })}> + + + + ({ + gap: theme.space.$1x5, + margin: 0, + paddingInlineStart: theme.space.$5, + listStyleType: 'decimal', + })} + > + + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step1.bold')} + /> + + + + + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step2.bold1')} + /> + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step2.bold2')} + /> + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step2.bold3')} + /> + + + + + + + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step4.bold')} + /> + + + + + + ({ fontWeight: theme.fontWeights.$medium })} + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step5.bold')} + /> + + + + From cccfbb03881cf55f3f0852ddcc66f6da8ba8f3ec Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Wed, 13 May 2026 09:48:09 -0300 Subject: [PATCH 16/22] refactor(ui): tighten ConfigureAttributes badge and pairs list styling Sets the Optional badge to colorScheme='primary' explicitly rather than falling through to Badge's default, so the styling is intentional. Swaps the pairs sub-list bullet from listStyleType: 'disc' to a literal '- ' marker so the dash sits closer to the badges and reads as a softer delimiter than the filled disc. --- .../ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index eaf22a4aa1c..c4c35615b1d 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -439,7 +439,7 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { /> { margin: 0, marginTop: theme.space.$1, paddingInlineStart: theme.space.$5, - listStyleType: 'disc', + listStyleType: '"- "', })} > {ATTRIBUTE_PAIRS.map(pair => ( From 31a7478e91ec16988c8f0f04fc4b2a7eb6658621 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Wed, 13 May 2026 10:01:13 -0300 Subject: [PATCH 17/22] refactor(ui): call __internal_useUserEnterpriseConnections directly in SubmitSamlConfigSubStep Mirrors the same pattern Iago adopted for SelectProviderStep: ConfigureSSOProvider no longer plumbs the enterprise-connection mutation through. SubmitSamlConfigSubStep now calls __internal_useUserEnterpriseConnections itself, grabs updateEnterpriseConnection, and wraps it in a local useReverification(useCallback(...)). Drops updateConnection from ConfigureSSOData, drops updateEnterpriseConnection from ConfigureSSOProviderProps, and stops threading the function through ConfigureSSO.tsx. The provider keeps ownership of provider-selection state (provider, setProvider, createConnection) since SelectProviderStep's create-flow still expects that shape until it gets the same treatment next. --- .../components/ConfigureSSO/ConfigureSSO.tsx | 12 ++----- .../ConfigureSSO/ConfigureSSOContext.tsx | 31 ++----------------- .../ConfigureSSO/steps/ConfigureStep.tsx | 20 +++++++++++- 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx index 241dcdccc52..edb4fed4127 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx @@ -64,11 +64,7 @@ const AuthenticatedContent = withCoreUserGuard(() => { }); const ConfigureSSOCardContent = () => { - const { - data: enterpriseConnections, - isLoading, - updateEnterpriseConnection, - } = __internal_useUserEnterpriseConnections({ enabled: true }); + const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true }); // Currently FAPI only supports one enterprise connection per user const enterpriseConnection = enterpriseConnections?.[0]; @@ -78,11 +74,7 @@ const ConfigureSSOCardContent = () => { } return ( - - + ); diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx index 23b4001704f..1df2c315e6a 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx @@ -1,5 +1,4 @@ -import { useReverification } from '@clerk/shared/react'; -import type { EnterpriseConnectionResource, UpdateMeEnterpriseConnectionParams } from '@clerk/shared/types'; +import type { EnterpriseConnectionResource } from '@clerk/shared/types'; import React, { type PropsWithChildren } from 'react'; import { deriveInitialStep } from './deriveInitialStep'; @@ -25,21 +24,10 @@ export interface ConfigureSSOData { * connection has been created. */ setProvider: (provider: ProviderType) => void; - /** - * Updates the current enterprise connection with the supplied params. The id - * is taken implicitly from `enterpriseConnection` in context, so callers do - * not need to thread it through. Throws when no enterprise connection is - * loaded yet. - */ - updateConnection: (params: UpdateMeEnterpriseConnectionParams) => Promise; } interface ConfigureSSOProviderProps { enterpriseConnection: EnterpriseConnectionResource | undefined; - updateEnterpriseConnection: ( - enterpriseConnectionId: string, - params: UpdateMeEnterpriseConnectionParams, - ) => Promise; } const ConfigureSSOContext = React.createContext(null); @@ -47,7 +35,6 @@ ConfigureSSOContext.displayName = 'ConfigureSSOContext'; export const ConfigureSSOProvider = ({ enterpriseConnection, - updateEnterpriseConnection, children, }: PropsWithChildren): JSX.Element => { const [provider, setProvider] = React.useState( @@ -56,28 +43,14 @@ export const ConfigureSSOProvider = ({ const initialStepId = deriveInitialStep(enterpriseConnection); - const updateConnectionFetcher = React.useCallback( - async (params: UpdateMeEnterpriseConnectionParams) => { - if (!enterpriseConnection) { - throw new Error('Enterprise connection required'); - } - - return updateEnterpriseConnection(enterpriseConnection.id, params); - }, - [enterpriseConnection, updateEnterpriseConnection], - ); - - const updateConnection = useReverification(updateConnectionFetcher); - const value = React.useMemo( () => ({ initialStepId, enterpriseConnection, provider, setProvider, - updateConnection, }), - [initialStepId, enterpriseConnection, provider, updateConnection], + [initialStepId, enterpriseConnection, provider], ); return {children}; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index c4c35615b1d..217c5d82b87 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -1,3 +1,7 @@ +import { __internal_useUserEnterpriseConnections, useReverification } from '@clerk/shared/react'; +import type { UpdateMeEnterpriseConnectionParams } from '@clerk/shared/types'; +import React from 'react'; + import { Badge, Col, @@ -744,7 +748,21 @@ export const AssignUsersSubStep = (): JSX.Element => { export const SubmitSamlConfigSubStep = (): JSX.Element => { const card = useCardState(); const { goNext, goPrev, isFirstStep } = useWizard(); - const { enterpriseConnection, updateConnection } = useConfigureSSO(); + const { enterpriseConnection } = useConfigureSSO(); + const { updateEnterpriseConnection } = __internal_useUserEnterpriseConnections(); + + const updateConnection = useReverification( + React.useCallback( + async (params: UpdateMeEnterpriseConnectionParams) => { + if (!enterpriseConnection) { + throw new Error('Enterprise connection required'); + } + + return updateEnterpriseConnection(enterpriseConnection.id, params); + }, + [enterpriseConnection, updateEnterpriseConnection], + ), + ); const metadataUrlField = useFormControl('idpMetadataUrl', '', { type: 'text', From 6c5dc5e501e3e84edfa9964d03e1a3129631972f Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Wed, 13 May 2026 16:48:57 -0300 Subject: [PATCH 18/22] refactor(ui): flatten Configure step instruction locales to single key per line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the prefix/bold/suffix (and the seven-key prefix/bold1/middle1/.../suffix on Assign Users step 2) shapes that we were threading through nested Text-as-span chains in the Configure sub-steps. Each Okta-walkthrough sentence now lives under one LocalizationValue, rendered as a single Text-as-li with localizationKey. A guide for safely embedding bold or other inline emphasis inside LocalizationValue strings is being prepared. Until that primitive lands the sub-steps read as plain prose — visually less hierarchy on the emphasized keywords, but a much smaller key surface and no fragile multi-span composition. The attribute-mapping pairs in Configure Attributes still use a pair of Badges plus a localized conjunction so each value stays themable as a distinct chip rather than collapsing into the surrounding sentence. --- packages/localizations/src/en-US.ts | 82 +---- packages/shared/src/types/localization.ts | 82 +---- .../ConfigureSSO/steps/ConfigureStep.tsx | 303 ++---------------- 3 files changed, 51 insertions(+), 416 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 8772eda2a28..7844f2c6592 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -264,31 +264,11 @@ export const enUS: LocalizationResource = { subtitle: 'Create a new enterprise application in your Okta Dashboard', createApp: { title: 'Create a new enterprise application in Okta', - step1: { - prefix: 'Sign in to Okta and go to ', - bold: 'Admin → Applications', - suffix: '.', - }, - step2: { - prefix: 'Click ', - bold: 'Create App Integration', - suffix: '.', - }, - step3: { - prefix: 'Select ', - bold: 'SAML 2.0', - suffix: '.', - }, - step4: { - prefix: 'Fill in the ', - bold: 'General Settings', - suffix: ' (App name is required).', - }, - step5: { - prefix: 'Click ', - bold: 'Next', - suffix: ' to complete creating the application.', - }, + step1: 'Sign in to Okta and go to Admin → Applications.', + step2: 'Click Create App Integration.', + step3: 'Select SAML 2.0.', + step4: 'Fill in the General Settings (App name is required).', + step5: 'Click Next to complete creating the application.', }, serviceProvider: { title: 'Configure service provider', @@ -299,28 +279,12 @@ export const enUS: LocalizationResource = { }, completeSamlIntegration: { title: 'Complete SAML integration', - step1: { - prefix: 'Select ', - bold: 'This is an internal app that we have created', - suffix: ' from the options menu.', - }, - step2: { - prefix: 'Complete the form with any comments and select ', - bold: '"Finish"', - suffix: '.', - }, + step1: 'Select This is an internal app that we have created from the options menu.', + step2: 'Complete the form with any comments and select "Finish".', }, configureAttributes: { - step1: { - prefix: 'In the Okta dashboard, find the ', - bold: 'Attribute Statements', - suffix: ' section.', - }, - step2: { - prefix: 'Select ', - bold: 'Add Expression', - suffix: ' for each attribute, and enter the following name and expression pairs:', - }, + step1: 'In the Okta dashboard, find the Attribute Statements section.', + step2: 'Select Add Expression for each attribute, and enter the following name and expression pairs:', pairs: { conjunction: ' and ', email: { @@ -340,31 +304,11 @@ export const enUS: LocalizationResource = { assignUsers: { title: 'Assign selected user or group in Okta', paragraph: 'You need to assign users or groups to your enterprise app before they can use it to sign in.', - step1: { - prefix: 'In the Okta dashboard, select the ', - bold: 'Assignments', - suffix: ' tab.', - }, - step2: { - prefix: 'Select the ', - bold1: 'Assign', - middle1: ' dropdown. You can either select ', - bold2: 'Assign to people', - middle2: ' or ', - bold3: 'Assign to groups', - suffix: '.', - }, + step1: 'In the Okta dashboard, select the Assignments tab.', + step2: 'Select the Assign dropdown. You can either select Assign to people or Assign to groups.', step3: 'In the search field, enter the user or group of users that you want to assign to the application.', - step4: { - prefix: 'Select the ', - bold: 'Assign', - suffix: ' button next to the user or group that you want to assign.', - }, - step5: { - prefix: 'Select the ', - bold: 'Done', - suffix: ' button to complete the assignment.', - }, + step4: 'Select the Assign button next to the user or group that you want to assign.', + step5: 'Select the Done button to complete the assignment.', }, metadataUrl: { label: 'Metadata URL', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 0c72ecf790a..3ce3be259e3 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1378,31 +1378,11 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; createApp: { title: LocalizationValue; - step1: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step2: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step3: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step4: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step5: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; + step1: LocalizationValue; + step2: LocalizationValue; + step3: LocalizationValue; + step4: LocalizationValue; + step5: LocalizationValue; }; serviceProvider: { title: LocalizationValue; @@ -1411,28 +1391,12 @@ export type __internal_LocalizationResource = { }; completeSamlIntegration: { title: LocalizationValue; - step1: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step2: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; + step1: LocalizationValue; + step2: LocalizationValue; }; configureAttributes: { - step1: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step2: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; + step1: LocalizationValue; + step2: LocalizationValue; pairs: { conjunction: LocalizationValue; email: { @@ -1452,31 +1416,11 @@ export type __internal_LocalizationResource = { assignUsers: { title: LocalizationValue; paragraph: LocalizationValue; - step1: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step2: { - prefix: LocalizationValue; - bold1: LocalizationValue; - middle1: LocalizationValue; - bold2: LocalizationValue; - middle2: LocalizationValue; - bold3: LocalizationValue; - suffix: LocalizationValue; - }; + step1: LocalizationValue; + step2: LocalizationValue; step3: LocalizationValue; - step4: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; - step5: { - prefix: LocalizationValue; - bold: LocalizationValue; - suffix: LocalizationValue; - }; + step4: LocalizationValue; + step5: LocalizationValue; }; metadataUrl: { label: LocalizationValue; diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 217c5d82b87..cdad0f7687b 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -154,108 +154,28 @@ export const CreateAppSubStep = (): JSX.Element => { - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step1.bold')} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step1')} + /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step2.bold')} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step2')} + /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step3.bold')} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step3')} + /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step4.bold')} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step4')} + /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step5.bold')} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.createApp.step5')} + /> @@ -315,57 +235,13 @@ export const CreateAppSubStep = (): JSX.Element => { - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys( - 'configureSSO.configureStep.samlOkta.completeSamlIntegration.step1.bold', - )} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.completeSamlIntegration.step1')} + /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys( - 'configureSSO.configureStep.samlOkta.completeSamlIntegration.step2.bold', - )} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.completeSamlIntegration.step2')} + /> @@ -485,30 +361,8 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys( - 'configureSSO.configureStep.samlOkta.configureAttributes.step1.bold', - )} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.configureAttributes.step1')} + /> { - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys( - 'configureSSO.configureStep.samlOkta.configureAttributes.step2.bold', - )} - /> - { - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step1.bold')} - /> - - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step1')} + /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step2.bold1')} - /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step2.bold2')} - /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step2.bold3')} - /> - - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step2')} + /> - - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step4.bold')} - /> - - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step4')} + /> - - ({ fontWeight: theme.fontWeights.$medium })} - localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step5.bold')} - /> - - + localizationKey={localizationKeys('configureSSO.configureStep.samlOkta.assignUsers.step5')} + /> From 73c1213a63f51b14287371b371fa6d78120b9acb Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Wed, 13 May 2026 17:18:02 -0300 Subject: [PATCH 19/22] refactor(ui): polish ConfigureSSO step header, spacing, and scrollbar Switch Step header to h2 with secondary description text, apply unstyled scrollbar to the body, and tighten Col/Section spacing across CreateApp and ConfigureAttributes sub-steps. --- .../components/ConfigureSSO/elements/Step.tsx | 22 ++++++++++--------- .../ConfigureSSO/steps/ConfigureStep.tsx | 17 +++++++------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/elements/Step.tsx b/packages/ui/src/components/ConfigureSSO/elements/Step.tsx index b5d58e422a7..71618955cba 100644 --- a/packages/ui/src/components/ConfigureSSO/elements/Step.tsx +++ b/packages/ui/src/components/ConfigureSSO/elements/Step.tsx @@ -13,7 +13,7 @@ import { useLocalizations, } from '@/customizables'; import { CaretLeft, CaretRight } from '@/icons'; -import type { PropsOfComponent } from '@/styledSystem'; +import { common, type PropsOfComponent } from '@/styledSystem'; import { ProfileCardFooter } from './ProfileCard'; @@ -67,18 +67,12 @@ const Header = ({ title, description, children }: StepHeaderProps): JSX.Element sx={theme => ({ gap: theme.space.$4 })} > ({ gap: theme.space.$2, minWidth: 0 })}> - ({ color: theme.colors.$colorForeground, fontSize: theme.fontSizes.$lg })} - > - {titleText} - + {titleText} {descriptionText && ( ({ color: theme.colors.$colorMutedForeground })} + colorScheme='secondary' > {descriptionText} @@ -97,7 +91,15 @@ const Body = ({ sx, ...props }: StepBodyProps): JSX.Element => ( ({ + flex: 1, + minHeight: 0, + overflowY: 'auto', + ...common.unstyledScrollbar(t), + }), + sx, + ]} /> ); diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index cdad0f7687b..9676e12e858 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -136,7 +136,7 @@ export const CreateAppSubStep = (): JSX.Element => { <> ({ gap: theme.space.$5 })}> - ({ gap: theme.space.$3 })}> + ({ gap: theme.space.$1x5 })}> { - ({ gap: theme.space.$3 })}> + ({ gap: theme.space.$1x5 })}> { /> - ({ gap: theme.space.$3 })}> + ({ gap: theme.space.$1x5 })}> { return ( <> - ({ gap: theme.space.$5 })}> + ({ gap: theme.space.$3 })}> ({ gap: theme.space.$3 })}> { > ({ - gap: theme.space.$1, + gap: theme.space.$1x5, margin: 0, - marginTop: theme.space.$1, + marginTop: theme.space.$1x5, paddingInlineStart: theme.space.$5, listStyleType: '"- "', })} @@ -386,16 +385,16 @@ export const ConfigureAttributesSubStep = (): JSX.Element => { + + ))} From c1bcbdf8012f02d5bb5471f8a621fa8f08fc6841 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Thu, 14 May 2026 09:26:13 -0300 Subject: [PATCH 20/22] fix(ui): give Audience URI its own FieldId and fix the useFormControl import path Three small fixes folded together: - Add 'spEntityId' to the FieldId union and switch the Audience URI field to use it. The field was previously sharing 'acsUrl' as its id, which collides with the Single Sign-On URL field above it. CodeRabbit flagged this on PR review. - Wrap the Audience URI clipboard input in Form.ControlRow so it matches the Single Sign-On URL row's chrome. The previous markup rendered the input without the row wrapper and was inconsistent. - Fix the useFormControl import from '@/utils/useFormControl' to '@/ui/utils/useFormControl' so it resolves through vitest's UI alias instead of the clerk-js catch-all. ConfigureSSO unit tests were failing on CI because Vite couldn't resolve the file. --- packages/shared/src/types/elementIds.ts | 1 + .../ConfigureSSO/steps/ConfigureStep.tsx | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/types/elementIds.ts b/packages/shared/src/types/elementIds.ts index bbd791603cb..4d2d9ab72f2 100644 --- a/packages/shared/src/types/elementIds.ts +++ b/packages/shared/src/types/elementIds.ts @@ -28,6 +28,7 @@ export type FieldId = | 'apiKeySecret' | 'idpMetadataUrl' | 'acsUrl' + | 'spEntityId' | 'web3WalletName'; export type ProfileSectionId = | 'profile' diff --git a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx index 9676e12e858..48c92857b49 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ConfigureStep.tsx @@ -23,7 +23,7 @@ import { useCardState } from '@/elements/contexts'; import { Form } from '@/elements/Form'; import { Check, ClipboardOutline } from '@/icons'; import { handleError } from '@/utils/errorHandler'; -import { useFormControl } from '@/utils/useFormControl'; +import { useFormControl } from '@/ui/utils/useFormControl'; import { useConfigureSSO } from '../ConfigureSSOContext'; import { Step } from '../elements/Step'; @@ -126,7 +126,7 @@ export const CreateAppSubStep = (): JSX.Element => { label: localizationKeys('configureSSO.configureStep.spFields.acsUrl.label'), isRequired: false, }); - const spEntityIdField = useFormControl('acsUrl', spEntityId, { + const spEntityIdField = useFormControl('spEntityId', spEntityId, { type: 'text', label: localizationKeys('configureSSO.configureStep.spFields.spEntityId.label'), isRequired: false, @@ -208,14 +208,16 @@ export const CreateAppSubStep = (): JSX.Element => { - - - + + + + + ({ gap: theme.space.$1x5 })}> Date: Thu, 14 May 2026 09:53:13 -0300 Subject: [PATCH 21/22] fix(ui,shared,localizations): restore verifyEmailDomainStep keys lost in rebase The Verify Domain step's English copy and matching LocalizationResource type entries were dropped from en-US.ts and shared/types/localization.ts during a rebase conflict resolution. Other locale files (es-ES, fr-FR, de-DE, pt-BR, etc.) still carried the keys, so the prod runtime worked for those locales but English consumers fell back to raw keys. Restores the verifyEmailDomainStep block at its original position under configureSSO in both files, matching what main has today. --- packages/localizations/src/en-US.ts | 24 +++++++++++++++++++++++ packages/shared/src/types/localization.ts | 24 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 7844f2c6592..bc7560fa39e 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -223,6 +223,30 @@ export const enUS: LocalizationResource = { }, warning: 'Once a provider is selected you cannot change again until the configuration is over', }, + verifyEmailDomainStep: { + title: 'Verify email address', + subtitle: 'Verify the email address you want to enable the enterprise connection on.', + addEmailAddress: { + formTitle: 'We need your email', + formSubtitle: 'In order to start we will need your email address', + inputPlaceholder: 'name@company.com', + inputLabel: 'Email address', + }, + emailCode: { + formTitle: 'Verify your email address', + formSubtitle: 'Enter the verification code sent to {{identifier}}', + resendButton: "Didn't receive a code? Resend", + verified: { + title: 'We got your email', + subtitle: "You've verified your email address with the following email", + inputLabel: 'Verified email address', + }, + }, + domainTaken: { + title: 'This domain ({{domain}}) already has an SSO connection', + subtitle: "Contact the application's administrator to get access through the existing connection.", + }, + }, configureStep: { spFields: { acsUrl: { diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 3ce3be259e3..79bd2faf255 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1338,6 +1338,30 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; }; }; + verifyEmailDomainStep: { + title: LocalizationValue; + subtitle: LocalizationValue; + addEmailAddress: { + formTitle: LocalizationValue; + formSubtitle: LocalizationValue; + inputPlaceholder: LocalizationValue; + inputLabel: LocalizationValue; + }; + emailCode: { + formTitle: LocalizationValue; + formSubtitle: LocalizationValue<'identifier'>; + resendButton: LocalizationValue; + verified: { + title: LocalizationValue; + subtitle: LocalizationValue; + inputLabel: LocalizationValue; + }; + }; + domainTaken: { + title: LocalizationValue<'domain'>; + subtitle: LocalizationValue; + }; + }; configureStep: { spFields: { acsUrl: { From 1dc6f1289476f246360de4b6a2138e9f091afbe4 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Thu, 14 May 2026 10:04:22 -0300 Subject: [PATCH 22/22] fix(shared): drop duplicate verifyEmailDomainStep type entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous restore commit added a verifyEmailDomainStep block under configureSSO in __internal_LocalizationResource, but the same block was still present further down — vitest's typecheck flagged it as a duplicate identifier and the shared package's test job failed in CI. Keeps the original (higher) declaration intact and removes the redundant copy. The en-US.ts side wasn't duplicated, so this is a shared-types-only fix. --- packages/shared/src/types/localization.ts | 24 ----------------------- 1 file changed, 24 deletions(-) diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 79bd2faf255..3ce3be259e3 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1338,30 +1338,6 @@ export type __internal_LocalizationResource = { subtitle: LocalizationValue; }; }; - verifyEmailDomainStep: { - title: LocalizationValue; - subtitle: LocalizationValue; - addEmailAddress: { - formTitle: LocalizationValue; - formSubtitle: LocalizationValue; - inputPlaceholder: LocalizationValue; - inputLabel: LocalizationValue; - }; - emailCode: { - formTitle: LocalizationValue; - formSubtitle: LocalizationValue<'identifier'>; - resendButton: LocalizationValue; - verified: { - title: LocalizationValue; - subtitle: LocalizationValue; - inputLabel: LocalizationValue; - }; - }; - domainTaken: { - title: LocalizationValue<'domain'>; - subtitle: LocalizationValue; - }; - }; configureStep: { spFields: { acsUrl: {