Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/purple-paths-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@asgardeo/javascript': minor
'@asgardeo/browser': minor
'@asgardeo/react': minor
---

Implement component rendering extensions and context for customizable SDK behavior
3 changes: 3 additions & 0 deletions packages/javascript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
17 changes: 16 additions & 1 deletion packages/javascript/src/models/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -54,7 +55,7 @@ export type SignOutOptions = Record<string, unknown>;
*/
export type SignUpOptions = Record<string, unknown>;

export interface BaseConfig<T = unknown> extends WithPreferences {
export interface BaseConfig<T = unknown> 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.
Expand Down Expand Up @@ -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<T = unknown> = BaseConfig<T>;

export interface ThemePreferences {
Expand Down
95 changes: 95 additions & 0 deletions packages/javascript/src/models/v2/extensions/components.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
/**
* Authentication flow type currently being rendered.
*/
authType: 'signin' | 'signup';
/**
* Validation messages keyed by field name.
*/
formErrors: Record<string, string>;
/**
* Current form values keyed by field name.
*/
formValues: Record<string, string>;
/**
* 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<string, any>, skipValidation?: boolean) => void;
/**
* Tracks whether each field has been interacted with.
*/
touchedFields: Record<string, boolean>;
}

/**
* 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<TElement = unknown> = (
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<string, ComponentRenderer<unknown>>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ 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 {OrganizationUnitPicker} from './OrganizationUnitPicker';
import ComponentRendererContext, {
ComponentRenderer,
ComponentRenderContext,
ComponentRendererMap,
} from '../../../contexts/ComponentRenderer/ComponentRendererContext';
import useTheme from '../../../contexts/Theme/useTheme';
import {UseTranslation} from '../../../hooks/useTranslation';
import Consent from '../../adapters/Consent';
Expand Down Expand Up @@ -212,9 +217,29 @@ const createAuthComponentFromFlow = (
} = {},
): ReactElement | null => {
const {theme} = useTheme();
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);

const key: string | number = options.key || component.id;

const customRenderer: ComponentRenderer | undefined =
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)) {
Expand Down
6 changes: 5 additions & 1 deletion packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -65,6 +66,7 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
baseUrl: initialBaseUrl,
clientId,
children,
extensions,
scopes,
preferences,
signInUrl,
Expand Down Expand Up @@ -708,7 +710,9 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
onOrganizationSwitch={switchOrganization}
revalidateMyOrganizations={async (): Promise<Organization[]> => asgardeo.getMyOrganizations()}
>
{children}
<ComponentRendererProvider renderers={(extensions?.components?.renderers ?? {}) as any}>
{children}
</ComponentRendererProvider>
</OrganizationProvider>
</UserProvider>
</FlowProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {Context, createContext, ReactElement} from 'react';

export interface ComponentRenderContext {
additionalData?: Record<string, any>;
authType: 'signin' | 'signup';
formErrors: Record<string, string>;
formValues: Record<string, string>;
isFormValid: boolean;
isLoading: boolean;
meta?: FlowMetadataResponse | null;
onInputBlur?: (name: string) => void;
onInputChange: (name: string, value: string) => void;
onSubmit?: (component: EmbeddedFlowComponent, data?: Record<string, any>, skipValidation?: boolean) => void;
touchedFields: Record<string, boolean>;
}

export type ComponentRenderer = (
component: EmbeddedFlowComponent,
context: ComponentRenderContext,
) => ReactElement | null;

export type ComponentRendererMap = Record<string, ComponentRenderer>;

const ComponentRendererContext: Context<ComponentRendererMap> = createContext<ComponentRendererMap>({});

export default ComponentRendererContext;
Original file line number Diff line number Diff line change
@@ -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<PropsWithChildren<ComponentRendererProviderProps>> = ({
renderers,
children,
}: PropsWithChildren<ComponentRendererProviderProps>): ReactElement => (
<ComponentRendererContext.Provider value={renderers}>{children}</ComponentRendererContext.Provider>
);

export default ComponentRendererProvider;
Loading