From e5995a7f97a4674bf0d2fc640a0028ecac859336 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:27:23 -0300 Subject: [PATCH 01/13] 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 | 10 +++ packages/shared/src/types/localization.ts | 9 +++ .../ConfigureSSO/steps/ConfigureStep.tsx | 66 ++++++++++++++++--- 4 files changed, 84 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 407aa6f5634..092f1d6b746 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -223,6 +223,16 @@ export const enUS: LocalizationResource = { }, warning: 'Once a provider is selected you cannot change again until the configuration is over', }, + 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 1123b6e88fb..654d5e28b11 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1314,6 +1314,15 @@ export type __internal_LocalizationResource = { }; warning: 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 eb2d90f7368f65dac6a0497b9f365ee5d9fc3ef8 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:33:35 -0300 Subject: [PATCH 02/13] 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 7d32b73a99a0e8972ab30458c5c91fd3a89196e2 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:37:38 -0300 Subject: [PATCH 03/13] 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 | 2 ++ .../ConfigureSSO/ConfigureSSOContext.tsx | 34 +++++++++++++++++-- .../ConfigureSSO/steps/ConfigureStep.tsx | 15 ++------ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx index 9be9db0a9ea..56ee537c026 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx @@ -68,6 +68,7 @@ const ConfigureSSOCardContent = () => { data: enterpriseConnections, isLoading, createEnterpriseConnection, + updateEnterpriseConnection, } = __internal_useUserEnterpriseConnections({ enabled: true }); // Currently FAPI only supports one enterprise connection per user @@ -81,6 +82,7 @@ const ConfigureSSOCardContent = () => { diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx index 42a3b2f67c7..c6ee00b8f95 100644 --- a/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx @@ -1,5 +1,9 @@ import { useReverification, useSession, useUser } from '@clerk/shared/react'; -import type { CreateMeEnterpriseConnectionParams, EnterpriseConnectionResource } from '@clerk/shared/types'; +import type { + CreateMeEnterpriseConnectionParams, + EnterpriseConnectionResource, + UpdateMeEnterpriseConnectionParams, +} from '@clerk/shared/types'; import React, { type PropsWithChildren } from 'react'; import { deriveInitialStep } from './deriveInitialStep'; @@ -31,6 +35,13 @@ export interface ConfigureSSOData { * an enterprise connection already exists so callers can safely re-trigger. */ createConnection: (provider: ProviderType) => Promise; + /** + * 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 { @@ -38,6 +49,10 @@ interface ConfigureSSOProviderProps { createEnterpriseConnection: ( params: CreateMeEnterpriseConnectionParams, ) => Promise; + updateEnterpriseConnection: ( + enterpriseConnectionId: string, + params: UpdateMeEnterpriseConnectionParams, + ) => Promise; } const ConfigureSSOContext = React.createContext(null); @@ -46,6 +61,7 @@ ConfigureSSOContext.displayName = 'ConfigureSSOContext'; export const ConfigureSSOProvider = ({ enterpriseConnection, createEnterpriseConnection, + updateEnterpriseConnection, children, }: PropsWithChildren): JSX.Element => { const { user } = useUser(); @@ -79,6 +95,19 @@ export const ConfigureSSOProvider = ({ const createConnection = useReverification(createConnectionFetcher); + 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, @@ -86,8 +115,9 @@ export const ConfigureSSOProvider = ({ provider, setProvider, createConnection, + updateConnection, }), - [initialStepId, enterpriseConnection, provider, createConnection], + [initialStepId, enterpriseConnection, provider, createConnection, 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 a2f4b0e421c49e5f9a72479b47d506aa28ec654b Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 15:47:26 -0300 Subject: [PATCH 04/13] 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 | 5 ++--- .../src/components/ConfigureSSO/steps/ConfigureStep.tsx | 9 +++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 092f1d6b746..1de007e4b40 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -228,9 +228,8 @@ export const enUS: LocalizationResource = { 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.', + placeholder: 'Paste URL here...', + 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 80f83cc9cde205004aefb6cf7dd4876c2ad99ef1 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 16:06:34 -0300 Subject: [PATCH 05/13] 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 cefdb50f6fffc96585affb49c548711cb2887836 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 16:20:52 -0300 Subject: [PATCH 06/13] 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/13] 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 af80f6704bb..d8bc28b2507 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -43,7 +43,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'; @@ -551,25 +550,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 0dad85bc27e..0f43cf341bd 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 4ac77d93483e2e3afad620db8ca7ec8c6505a1ec Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 16:26:15 -0300 Subject: [PATCH 08/13] 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/13] 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 1de007e4b40..912bfe32f16 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -226,6 +226,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', placeholder: 'Paste URL here...', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 654d5e28b11..9ec899d6167 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1317,6 +1317,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 fc5c2b1ddaa8190162a078d78ca6be20fa297eae Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 17:42:43 -0300 Subject: [PATCH 10/13] 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/13] 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 912bfe32f16..6f3c4d671bf 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -282,6 +282,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', placeholder: 'Paste URL here...', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 9ec899d6167..8a93fc33082 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1371,6 +1371,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 1b49000e596545c582cc0f7882e884a77c8766d9 Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 23:02:38 -0300 Subject: [PATCH 12/13] 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 85847418f4c2b9f46a8da684274e8af332105e9c Mon Sep 17 00:00:00 2001 From: Iago Dahlem Lorensini Date: Tue, 12 May 2026 23:16:14 -0300 Subject: [PATCH 13/13] 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 | 189 ++---- packages/shared/src/types/localization.ts | 186 ++---- .../ConfigureSSO/steps/ConfigureStep.tsx | 539 ++++++++---------- 3 files changed, 346 insertions(+), 568 deletions(-) diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 6f3c4d671bf..608b0e537a7 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -224,157 +224,80 @@ export const enUS: LocalizationResource = { warning: 'Once a provider is selected you cannot change again until the configuration is over', }, 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: '.', + spFields: { + acsUrl: { + label: 'Single sign-on URL', + }, + spEntityId: { + label: 'Audience URI', + }, + }, + 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', - placeholder: 'Paste URL here...', - 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 8a93fc33082..cab20a199d1 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1315,154 +1315,76 @@ export type __internal_LocalizationResource = { warning: LocalizationValue; }; 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')} />