From 81f55250626a6c9d0b7dcd703af6b439078b9e38 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 22 Apr 2026 00:06:03 +0530 Subject: [PATCH 1/3] Implement component rendering extensions and context for customizable SDK behavior --- packages/javascript/src/index.ts | 3 + packages/javascript/src/models/config.ts | 17 +++- .../src/models/v2/extensions/components.ts | 95 +++++++++++++++++++ .../presentation/auth/AuthOptionFactory.tsx | 24 ++++- .../contexts/Asgardeo/AsgardeoProvider.tsx | 6 +- .../ComponentRendererContext.ts | 45 +++++++++ .../ComponentRendererProvider.tsx | 33 +++++++ 7 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 packages/javascript/src/models/v2/extensions/components.ts create mode 100644 packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts create mode 100644 packages/react/src/contexts/ComponentRenderer/ComponentRendererProvider.tsx diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 64ae8279..0cfbf568 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -156,10 +156,13 @@ export { I18nPreferences, I18nStorageStrategy, WithPreferences, + Extensions, + WithExtensions, SignInOptions, SignOutOptions, SignUpOptions, } from './models/config'; +export type {ComponentRenderContext, ComponentRenderer, ComponentsExtensions} from './models/v2/extensions/components'; export {TokenResponse, IdToken, TokenExchangeRequestConfig} from './models/token'; export {AgentConfig} from './models/agent'; export {AuthCodeResponse} from './models/auth-code-response'; diff --git a/packages/javascript/src/models/config.ts b/packages/javascript/src/models/config.ts index 7d671c03..8af5e166 100644 --- a/packages/javascript/src/models/config.ts +++ b/packages/javascript/src/models/config.ts @@ -17,6 +17,7 @@ */ import {I18nBundle} from '@asgardeo/i18n'; +import {ComponentsExtensions} from './v2/extensions/components'; import {Platform} from './platforms'; import {RecursivePartial} from './utility-types'; import {ThemeConfig, ThemeMode} from '../theme/types'; @@ -54,7 +55,7 @@ export type SignOutOptions = Record; */ export type SignUpOptions = Record; -export interface BaseConfig extends WithPreferences { +export interface BaseConfig extends WithPreferences, WithExtensions { /** * Optional URL where the authorization server should redirect after authentication. * This must match one of the allowed redirect URIs configured in your IdP. @@ -357,6 +358,20 @@ export interface WithPreferences { preferences?: Preferences; } +export interface Extensions { + /** + * Extension configuration for flow component rendering. + */ + components?: ComponentsExtensions; +} + +export interface WithExtensions { + /** + * Extensions for customizing SDK behavior at defined integration points. + */ + extensions?: Extensions; +} + export type Config = BaseConfig; export interface ThemePreferences { diff --git a/packages/javascript/src/models/v2/extensions/components.ts b/packages/javascript/src/models/v2/extensions/components.ts new file mode 100644 index 00000000..3404860c --- /dev/null +++ b/packages/javascript/src/models/v2/extensions/components.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type {EmbeddedFlowComponent as EmbeddedFlowComponentV2} from '../embedded-flow-v2'; +import type {FlowMetadataResponse} from '../flow-meta-v2'; + +/** + * Framework-agnostic context passed to every custom component renderer. + * Contains form state and callbacks needed to render and submit flow components. + */ +export interface ComponentRenderContext { + /** + * Extra payload propagated by the flow engine for component rendering. + */ + additionalData?: Record; + /** + * Authentication flow type currently being rendered. + */ + authType: 'signin' | 'signup'; + /** + * Validation messages keyed by field name. + */ + formErrors: Record; + /** + * Current form values keyed by field name. + */ + formValues: Record; + /** + * Whether the current form state passes validation. + */ + isFormValid: boolean; + /** + * Indicates whether a submit action is currently in progress. + */ + isLoading: boolean; + /** + * Optional flow metadata associated with the current step. + */ + meta?: FlowMetadataResponse | null; + /** + * Optional callback fired when an input loses focus. + */ + onInputBlur?: (name: string) => void; + /** + * Callback to update the value of a named input field. + */ + onInputChange: (name: string, value: string) => void; + /** + * Optional submit handler for progressing the flow. + */ + onSubmit?: (component: EmbeddedFlowComponentV2, data?: Record, skipValidation?: boolean) => void; + /** + * Tracks whether each field has been interacted with. + */ + touchedFields: Record; +} + +/** + * A function that renders a flow component of a given type. + * `TElement` is `unknown` at the JS SDK level; each framework narrows it + * (React: `ReactElement`, Vue: `VNode`, etc.). + * + * Returning `null` hides the component. If no renderer is registered for a + * component type, the SDK falls back to its built-in rendering. + */ +export type ComponentRenderer = ( + component: EmbeddedFlowComponentV2, + context: ComponentRenderContext, +) => TElement | null; + +/** + * Extension configuration for flow component rendering. + * Keyed by component type string (e.g. `"PASSWORD_INPUT"`, `"ACTION"`). + */ +export interface ComponentsExtensions { + /** + * Custom renderers keyed by flow component type. + */ + renderers?: Record>; +} diff --git a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx index 4634c74b..dec7d8ff 100644 --- a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx +++ b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx @@ -35,7 +35,10 @@ import { } from '@asgardeo/browser'; import {css} from '@emotion/css'; import DOMPurify from 'dompurify'; -import {cloneElement, CSSProperties, ReactElement} from 'react'; +import {cloneElement, CSSProperties, ReactElement, useContext} from 'react'; +import ComponentRendererContext, { + ComponentRenderContext, +} from '../../../contexts/ComponentRenderer/ComponentRendererContext'; import {OrganizationUnitPicker} from './OrganizationUnitPicker'; import useTheme from '../../../contexts/Theme/useTheme'; import {UseTranslation} from '../../../hooks/useTranslation'; @@ -212,9 +215,28 @@ const createAuthComponentFromFlow = ( } = {}, ): ReactElement | null => { const {theme} = useTheme(); + const customRenderers = useContext(ComponentRendererContext); const key: string | number = options.key || component.id; + const customRenderer = customRenderers[component.id] ?? customRenderers[component.type as string]; + if (customRenderer) { + const renderCtx: ComponentRenderContext = { + additionalData: options.additionalData, + authType, + formErrors, + formValues, + isFormValid, + isLoading, + meta: options.meta, + onInputBlur: options.onInputBlur, + onInputChange, + onSubmit: options.onSubmit, + touchedFields, + }; + return customRenderer(component, renderCtx); + } + /** Resolve any remaining {{t()}} or {{meta()}} template expressions in a string at render time. */ const resolve = (text: string | undefined): string => { if (!text || (!options.t && !options.meta)) { diff --git a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx index e91d67a3..05a5f950 100644 --- a/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx +++ b/packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx @@ -42,6 +42,7 @@ import AsgardeoReactClient from '../../AsgardeoReactClient'; import useBrowserUrl from '../../hooks/useBrowserUrl'; import {AsgardeoReactConfig} from '../../models/config'; import BrandingProvider from '../Branding/BrandingProvider'; +import ComponentRendererProvider from '../ComponentRenderer/ComponentRendererProvider'; import FlowProvider from '../Flow/FlowProvider'; import FlowMetaProvider from '../FlowMeta/FlowMetaProvider'; import I18nProvider from '../I18n/I18nProvider'; @@ -65,6 +66,7 @@ const AsgardeoProvider: FC> = ({ baseUrl: initialBaseUrl, clientId, children, + extensions, scopes, preferences, signInUrl, @@ -708,7 +710,9 @@ const AsgardeoProvider: FC> = ({ onOrganizationSwitch={switchOrganization} revalidateMyOrganizations={async (): Promise => asgardeo.getMyOrganizations()} > - {children} + + {children} + diff --git a/packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts b/packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts new file mode 100644 index 00000000..a5201e06 --- /dev/null +++ b/packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {EmbeddedFlowComponentV2 as EmbeddedFlowComponent, FlowMetadataResponse} from '@asgardeo/browser'; +import {createContext, ReactElement} from 'react'; + +export interface ComponentRenderContext { + additionalData?: Record; + authType: 'signin' | 'signup'; + formErrors: Record; + formValues: Record; + isFormValid: boolean; + isLoading: boolean; + meta?: FlowMetadataResponse | null; + onInputBlur?: (name: string) => void; + onInputChange: (name: string, value: string) => void; + onSubmit?: (component: EmbeddedFlowComponent, data?: Record, skipValidation?: boolean) => void; + touchedFields: Record; +} + +export type ComponentRenderer = ( + component: EmbeddedFlowComponent, + context: ComponentRenderContext, +) => ReactElement | null; + +export type ComponentRendererMap = Record; + +const ComponentRendererContext = createContext({}); + +export default ComponentRendererContext; diff --git a/packages/react/src/contexts/ComponentRenderer/ComponentRendererProvider.tsx b/packages/react/src/contexts/ComponentRenderer/ComponentRendererProvider.tsx new file mode 100644 index 00000000..97465d58 --- /dev/null +++ b/packages/react/src/contexts/ComponentRenderer/ComponentRendererProvider.tsx @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import {FC, PropsWithChildren, ReactElement} from 'react'; +import ComponentRendererContext, {ComponentRendererMap} from './ComponentRendererContext'; + +interface ComponentRendererProviderProps { + renderers: ComponentRendererMap; +} + +const ComponentRendererProvider: FC> = ({ + renderers, + children, +}: PropsWithChildren): ReactElement => ( + {children} +); + +export default ComponentRendererProvider; From 03c4306e10c4ca70c03d5cb6394ba2553e1b4dee Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 22 Apr 2026 00:06:07 +0530 Subject: [PATCH 2/3] =?UTF-8?q?Add=20changeset=20=F0=9F=A6=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/purple-paths-retire.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/purple-paths-retire.md diff --git a/.changeset/purple-paths-retire.md b/.changeset/purple-paths-retire.md new file mode 100644 index 00000000..e4cfe782 --- /dev/null +++ b/.changeset/purple-paths-retire.md @@ -0,0 +1,7 @@ +--- +'@asgardeo/javascript': minor +'@asgardeo/browser': minor +'@asgardeo/react': minor +--- + +Implement component rendering extensions and context for customizable SDK behavior From d95d1d79b73496563845682bd0173100d9e17bc3 Mon Sep 17 00:00:00 2001 From: Brion Date: Wed, 22 Apr 2026 00:17:12 +0530 Subject: [PATCH 3/3] Fix lint issues --- .../components/presentation/auth/AuthOptionFactory.tsx | 9 ++++++--- .../ComponentRenderer/ComponentRendererContext.ts | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx index dec7d8ff..8e1d7d9d 100644 --- a/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx +++ b/packages/react/src/components/presentation/auth/AuthOptionFactory.tsx @@ -36,10 +36,12 @@ import { import {css} from '@emotion/css'; import DOMPurify from 'dompurify'; import {cloneElement, CSSProperties, ReactElement, useContext} from 'react'; +import {OrganizationUnitPicker} from './OrganizationUnitPicker'; import ComponentRendererContext, { + ComponentRenderer, ComponentRenderContext, + ComponentRendererMap, } from '../../../contexts/ComponentRenderer/ComponentRendererContext'; -import {OrganizationUnitPicker} from './OrganizationUnitPicker'; import useTheme from '../../../contexts/Theme/useTheme'; import {UseTranslation} from '../../../hooks/useTranslation'; import Consent from '../../adapters/Consent'; @@ -215,11 +217,12 @@ const createAuthComponentFromFlow = ( } = {}, ): ReactElement | null => { const {theme} = useTheme(); - const customRenderers = useContext(ComponentRendererContext); + const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext); const key: string | number = options.key || component.id; - const customRenderer = customRenderers[component.id] ?? customRenderers[component.type as string]; + const customRenderer: ComponentRenderer | undefined = + customRenderers[component.id] ?? customRenderers[component.type as string]; if (customRenderer) { const renderCtx: ComponentRenderContext = { additionalData: options.additionalData, diff --git a/packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts b/packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts index a5201e06..842390dc 100644 --- a/packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts +++ b/packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts @@ -17,7 +17,7 @@ */ import {EmbeddedFlowComponentV2 as EmbeddedFlowComponent, FlowMetadataResponse} from '@asgardeo/browser'; -import {createContext, ReactElement} from 'react'; +import {Context, createContext, ReactElement} from 'react'; export interface ComponentRenderContext { additionalData?: Record; @@ -40,6 +40,6 @@ export type ComponentRenderer = ( export type ComponentRendererMap = Record; -const ComponentRendererContext = createContext({}); +const ComponentRendererContext: Context = createContext({}); export default ComponentRendererContext;