Skip to content

Custom Component Renderers for Flow UI Extensibility#471

Merged
brionmario merged 3 commits intoasgardeo:mainfrom
brionmario:feat-extension-architecture
Apr 21, 2026
Merged

Custom Component Renderers for Flow UI Extensibility#471
brionmario merged 3 commits intoasgardeo:mainfrom
brionmario:feat-extension-architecture

Conversation

@brionmario
Copy link
Copy Markdown
Member

@brionmario brionmario commented Apr 21, 2026

Purpose

Introduce support for customizing SDK UI components using the ComponentRenderer extension point in @asgardeo/react.

This enables applications to override default components and build fully customized experiences on top of the authentication flow.

Usage Example

Below is an example of how to override a default component using a custom renderer.

Expected Output

Screenshot 2026-04-22 at 00 11 14

Flow Graph

Click to see the flow graph
{
    "executionId": "019db150-0eb5-799e-9d9f-5bbe6fce26fa",
    "flowStatus": "INCOMPLETE",
    "type": "VIEW",
    "challengeToken": "2cbad16bd9264ac96f719b27c7bd2ba5daa0333bcf285181ccd1f5e4f6ca159d",
    "data": {
        "actions": [
            {
                "ref": "action_v9hh",
                "nextNode": "ID_g5do"
            }
        ],
        "meta": {
            "components": [
                {
                    "category": "DISPLAY",
                    "id": "text_69uy",
                    "label": "Sign In",
                    "resourceType": "ELEMENT",
                    "type": "TEXT",
                    "variant": "HEADING_3"
                },
                {
                    "category": "BLOCK",
                    "components": [
                        {
                            "category": "MISCELLANEOUS",
                            "id": "LOGIN_ID",
                            "resourceType": "ELEMENT",
                            "type": "CUSTOM"
                        },
                        {
                            "category": "ACTION",
                            "eventType": "SUBMIT",
                            "id": "action_v9hh",
                            "label": "Continue",
                            "resourceType": "ELEMENT",
                            "type": "ACTION",
                            "variant": "PRIMARY"
                        },
                        {
                            "category": "DISPLAY",
                            "id": "rich_text_la4l",
                            "label": "\u003cp class=\"rich-text-paragraph\"\u003e\u003cspan class=\"rich-text-pre-wrap\"\u003eDon't have an account? \u003c/span\u003e\u003ca href=\"{{meta(application.sign_up_url)}}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"rich-text-link\"\u003e\u003cspan class=\"rich-text-pre-wrap\"\u003eSign up\u003c/span\u003e\u003c/a\u003e\u003c/p\u003e",
                            "resourceType": "ELEMENT",
                            "type": "RICH_TEXT"
                        }
                    ],
                    "id": "block_l7ea",
                    "resourceType": "ELEMENT",
                    "type": "BLOCK"
                }
            ]
        }
    }
}

1. Register a custom renderer via AsgardeoProvider

import { AsgardeoProvider } from '@asgardeo/react';
import { customRenderer } from './components/CustomComponent';

<AsgardeoProvider
  baseUrl={import.meta.env.VITE_ASGARDEO_BASE_URL}
  platform="AsgardeoV2"
  applicationId={applicationId}
  preferences={{
    resolveFromMeta: false,
    theme: {
      mode: 'light',
    },
  }}
  extensions={{
    components: {
      renderers: {
        LOGIN_ID: customRenderer,
      },
    },
  }}
>
  <App />
</AsgardeoProvider>

2. Example of the customRenderer

Flow Graph
import { useEffect, useState } from 'react';

function CustomComponent({ component, context }) {
  const [value, setValue] = useState('');

  useEffect(() => {
    context.onInputChange(component.id, '');
  }, [component.id, context]);

  const handleChange = (e) => {
    const newValue = e.target.value;
    setValue(newValue);
    context.onInputChange(component.id, newValue);
  };

  return (
    <input
      type="text"
      value={value}
      onChange={handleChange}
      disabled={context.isLoading}
      className="w-full px-3 py-2 border rounded"
      placeholder="Enter value"
    />
  );
}

Notes

  • component contains metadata about the rendered component.
  • context provides:
    • formValues → current form state
    • onInputChange(field, value) → update form values
    • isLoading → loading state
  • Custom renderers allow complete control over UI and behavior.

Related Issues

Related PRs

  • N/A

Checklist

  • Followed the CONTRIBUTING guidelines.
  • Manual test round performed and verified.
  • Documentation provided. (Add links if there are any)
  • Unit tests provided. (Add links if there are any)

Security checks

Summary by CodeRabbit

  • New Features

    • Added component rendering extensions framework enabling custom renderers for flow components.
    • Introduced configuration options for registering custom component renderers by component type.
    • Added rendering context with form state, validation, and input handling support for customizable behavior.
  • Documentation

    • Added changelog entry documenting extension implementation and context mechanism.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@brionmario has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 39 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 52 minutes and 39 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 020fe1f7-509c-4019-b5de-549391508122

📥 Commits

Reviewing files that changed from the base of the PR and between 03c4306 and d95d1d7.

📒 Files selected for processing (2)
  • packages/react/src/components/presentation/auth/AuthOptionFactory.tsx
  • packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts
📝 Walkthrough

Walkthrough

This PR adds an extension mechanism for custom component renderers in the Asgardeo SDK. It introduces new configuration interfaces, types for component rendering context, and React context infrastructure to allow consumers to register custom render functions for flow components, both known and unknown types.

Changes

Cohort / File(s) Summary
Changeset & Release
.changeset/purple-paths-retire.md
Added changeset entry marking minor version bump for JavaScript, Browser, and React packages with description of component rendering extensions feature.
Core Configuration Models
packages/javascript/src/models/config.ts, packages/javascript/src/index.ts
Extended BaseConfig to support optional extensions configuration; added Extensions and WithExtensions interfaces; exported new types from models for public API surface.
Extension Type Definitions
packages/javascript/src/models/v2/extensions/components.ts
Introduced framework-agnostic types: ComponentRenderContext (render payload with form state, auth metadata, callbacks), ComponentRenderer (generic render function), and ComponentsExtensions (renderer registration map).
React Context Infrastructure
packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts, packages/react/src/contexts/ComponentRenderer/ComponentRendererProvider.tsx
Created new React context module for component renderer management with ComponentRendererContext, ComponentRenderer, and ComponentRendererMap types; added ComponentRendererProvider wrapper component to inject renderers into component tree.
React Provider & Component Factory Integration
packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx, packages/react/src/components/presentation/auth/AuthOptionFactory.tsx
Modified AsgardeoProvider to accept extensions prop and wrap children with ComponentRendererProvider; updated createAuthComponentFromFlow to check for custom renderers by component ID/type and invoke them with built ComponentRenderContext before falling back to default rendering.

Sequence Diagram

sequenceDiagram
    participant App as Application
    participant ASP as AsgardeoProvider
    participant CRP as ComponentRendererProvider
    participant CTX as ComponentRendererContext
    participant CAF as AuthOptionFactory
    participant CR as Custom Renderer

    App->>ASP: Pass extensions config with renderers
    ASP->>CRP: Wrap with ComponentRendererProvider(renderers)
    CRP->>CTX: Set context value to renderers map
    
    App->>CAF: Render flow component
    CAF->>CTX: useContext to read renderers map
    
    alt Custom renderer exists
        CAF->>CAF: Build ComponentRenderContext
        CAF->>CR: Invoke customRenderer(component, context)
        CR-->>CAF: Return custom rendered element
    else No custom renderer
        CAF->>CAF: Use default switch-based rendering
    end
    
    CAF-->>App: Rendered component
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related Issues

Possibly Related PRs

  • [V2] Support per step challenge token #466 — Also modifies AsgardeoProvider to add provider-level features; understanding the interaction between component renderer extensions and other provider enhancements may be important.

Suggested Reviewers

  • thiva-k

Poem

🐰 A garden of renderers, now open so wide,
Custom components can flourish inside!
No more locked gates on the UI display,
Extensions bloom brightly in every which way! 🌸

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Custom Component Renderers for Flow UI Extensibility' clearly and specifically describes the main change: introducing custom component renderers as an extension point for UI customization.
Linked Issues check ✅ Passed The code changes fully implement the objectives from issue #470: allowing custom renderers for known component types [#470], handling unknown types [#470], and exposing the extension API [#470].
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing component renderer extensions: new extension types, context providers, renderer integration in AuthOptionFactory, and configuration support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description is comprehensive with clear purpose, usage examples, flow graph, and related issue reference, though some checklist items remain unchecked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

DonOmalVindula
DonOmalVindula previously approved these changes Apr 21, 2026
@asgardeo-github-bot
Copy link
Copy Markdown

🦋 Changeset detected

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts (1)

19-41: Eliminate the duplicated renderer contract by aliasing the shared types.

The React package duplicates ComponentRenderContext and ComponentRenderer which are already defined in the JavaScript SDK (@asgardeo/javascript) and re-exported via @asgardeo/browser. Aliasing these shared types prevents future drift between configured renderers and the runtime context.

♻️ Proposed type aliasing
-import {EmbeddedFlowComponentV2 as EmbeddedFlowComponent, FlowMetadataResponse} from '@asgardeo/browser';
+import type {
+  ComponentRenderContext,
+  ComponentRenderer,
+} from '@asgardeo/browser';
 import {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 {ComponentRenderContext} from '@asgardeo/browser';
 
-export type ComponentRenderer = (
-  component: EmbeddedFlowComponent,
-  context: ComponentRenderContext,
-) => ReactElement | null;
+export type {ComponentRenderer} from '@asgardeo/browser';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts`
around lines 19 - 41, Replace the duplicated local types with aliases to the
shared SDK types: import ComponentRenderContext and ComponentRenderer from
'@asgardeo/browser' (alongside EmbeddedFlowComponent and FlowMetadataResponse),
then remove the local interface and type declarations and re-export them as type
ComponentRenderContext = ImportedComponentRenderContext and type
ComponentRenderer = ImportedComponentRenderer; keep ComponentRendererMap (which
can continue to reference ComponentRenderer) and ensure function/type names
referenced are ComponentRenderContext, ComponentRenderer, ComponentRendererMap,
EmbeddedFlowComponent, and FlowMetadataResponse so the module uses the SDK types
instead of duplicating the contract.
packages/javascript/src/models/v2/extensions/components.ts (1)

90-95: Propagate the renderer element type through ComponentsExtensions.

ComponentRenderer is generic, but ComponentsExtensions.renderers fixes it to unknown, requiring React to erase types with as any in AsgardeoProvider.tsx. Making ComponentsExtensions generic with a default type parameter maintains backward compatibility while enabling proper type specialization:

♻️ Proposed type refinement
-export interface ComponentsExtensions {
+export interface ComponentsExtensions<TElement = unknown> {
   /**
    * Custom renderers keyed by flow component type.
    */
-  renderers?: Record<string, ComponentRenderer<unknown>>;
+  renderers?: Record<string, ComponentRenderer<TElement>>;
 }

The default type parameter preserves backward compatibility for all existing usages. This eliminates the type erasure cast in React and allows framework packages to specialize the element type when needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/javascript/src/models/v2/extensions/components.ts` around lines 90 -
95, Make ComponentsExtensions generic so the renderer element type is
propagated: change the declaration of ComponentsExtensions to accept a type
parameter with a default (e.g., export interface ComponentsExtensions<E =
unknown>) and update renderers to use that type (renderers?: Record<string,
ComponentRenderer<E>>). This preserves backward compatibility while allowing
callers (and files like AsgardeoProvider.tsx) to specialize the element type
instead of casting to any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/react/src/components/presentation/auth/AuthOptionFactory.tsx`:
- Around line 217-218: The factory createAuthComponentFromFlow must not call
hooks directly; remove useContext(ComponentRendererContext) and useTheme() usage
inside that function and instead make createAuthComponentFromFlow a pure
function that receives theme and customRenderers as parameters (or create a
separate custom hook wrapper, e.g., useAuthComponentFactory, that calls useTheme
and useContext once and passes those values into createAuthComponentFromFlow);
update all callers to obtain theme/customRenderers from the hook/component and
forward them into createAuthComponentFromFlow so hook order no longer depends on
the JSON structure.
- Around line 222-238: The lookup of customRenderer should ensure entries come
from customRenderers' own properties and are functions: instead of directly
reading customRenderers[component.id] or [component.type], check
Object.prototype.hasOwnProperty.call(customRenderers, component.id) or
component.type and that the resolved value is typeof 'function' before assigning
to customRenderer; then only call customRenderer(component, renderCtx) when both
checks pass (references: customRenderers, customRenderer, component.id,
component.type, renderCtx, and the return call).

---

Nitpick comments:
In `@packages/javascript/src/models/v2/extensions/components.ts`:
- Around line 90-95: Make ComponentsExtensions generic so the renderer element
type is propagated: change the declaration of ComponentsExtensions to accept a
type parameter with a default (e.g., export interface ComponentsExtensions<E =
unknown>) and update renderers to use that type (renderers?: Record<string,
ComponentRenderer<E>>). This preserves backward compatibility while allowing
callers (and files like AsgardeoProvider.tsx) to specialize the element type
instead of casting to any.

In `@packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts`:
- Around line 19-41: Replace the duplicated local types with aliases to the
shared SDK types: import ComponentRenderContext and ComponentRenderer from
'@asgardeo/browser' (alongside EmbeddedFlowComponent and FlowMetadataResponse),
then remove the local interface and type declarations and re-export them as type
ComponentRenderContext = ImportedComponentRenderContext and type
ComponentRenderer = ImportedComponentRenderer; keep ComponentRendererMap (which
can continue to reference ComponentRenderer) and ensure function/type names
referenced are ComponentRenderContext, ComponentRenderer, ComponentRendererMap,
EmbeddedFlowComponent, and FlowMetadataResponse so the module uses the SDK types
instead of duplicating the contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c20867d-6667-4432-9b22-f4815dc70612

📥 Commits

Reviewing files that changed from the base of the PR and between 61d3d6a and 03c4306.

📒 Files selected for processing (8)
  • .changeset/purple-paths-retire.md
  • packages/javascript/src/index.ts
  • packages/javascript/src/models/config.ts
  • packages/javascript/src/models/v2/extensions/components.ts
  • packages/react/src/components/presentation/auth/AuthOptionFactory.tsx
  • packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
  • packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts
  • packages/react/src/contexts/ComponentRenderer/ComponentRendererProvider.tsx

Comment on lines +217 to +218
const {theme} = useTheme();
const customRenderers = useContext(ComponentRendererContext);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect hook calls inside AuthOptionFactory and where the render helpers are invoked.

rg -n -C3 '\b(useContext|useTheme)\s*\(' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx
rg -n -C3 '\b(renderSignInComponents|renderSignUpComponents|renderInviteUserComponents)\s*\(' --type=ts --type=tsx

Repository: asgardeo/javascript

Length of output: 374


🏁 Script executed:

# Read the function definition and surrounding context
cat -n packages/react/src/components/presentation/auth/AuthOptionFactory.tsx | sed -n '200,240p'

Repository: asgardeo/javascript

Length of output: 1842


🏁 Script executed:

# Search for the function definition
rg -n 'createAuthComponentFromFlow' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx -A 5 -B 5

Repository: asgardeo/javascript

Length of output: 2058


🏁 Script executed:

# Check if there are recursive calls within the function
rg -n 'createAuthComponentFromFlow\s*\(' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx

Repository: asgardeo/javascript

Length of output: 269


🏁 Script executed:

# Find where createAuthComponentFromFlow is called at the top level (not recursively)
rg -n 'renderSignInComponents|renderSignUpComponents|renderInviteUserComponents' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx -B 2 -A 15 | head -50

Repository: asgardeo/javascript

Length of output: 2136


🏁 Script executed:

# Check which React component wraps these render functions
rg -n 'function\s+\w+|const\s+\w+\s*=\s*\(' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx | head -20

Repository: asgardeo/javascript

Length of output: 902


🏁 Script executed:

# Check if these exported render functions are called from React components
rg -n 'renderSignInComponents|renderSignUpComponents|renderInviteUserComponents' packages/react/src/components/presentation/auth/ --type=ts --type=tsx -l

Repository: asgardeo/javascript

Length of output: 90


🏁 Script executed:

# Look at a caller to see how it's invoked
rg -n 'renderSignInComponents|renderSignUpComponents' packages/react/src/components/presentation/auth/ -B 3 -A 3 | head -80

Repository: asgardeo/javascript

Length of output: 7397


🏁 Script executed:

# Check how renderComponents is used inside the useCallback
rg -n 'renderComponents' packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx -B 2 -A 10 | head -60

Repository: asgardeo/javascript

Length of output: 924


Do not call context hooks inside this recursive factory.

createAuthComponentFromFlow runs once per flow component and recursively for blocks/stacks, so adding useContext and useTheme here makes React hook order depend on server-provided JSON shape. Hooks must only be called from React components or custom hooks, not from plain factory functions. Read ComponentRendererContext and theme from a component/custom hook once and pass them into this pure factory.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/src/components/presentation/auth/AuthOptionFactory.tsx` around
lines 217 - 218, The factory createAuthComponentFromFlow must not call hooks
directly; remove useContext(ComponentRendererContext) and useTheme() usage
inside that function and instead make createAuthComponentFromFlow a pure
function that receives theme and customRenderers as parameters (or create a
separate custom hook wrapper, e.g., useAuthComponentFactory, that calls useTheme
and useContext once and passes those values into createAuthComponentFromFlow);
update all callers to obtain theme/customRenderers from the hook/component and
forward them into createAuthComponentFromFlow so hook order no longer depends on
the JSON structure.

Comment on lines +222 to +238
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Demonstrate that plain object bracket lookup can resolve inherited properties.

node - <<'NODE'
const customRenderers = {};
const component = { id: 'toString', type: 'TEXT' };
const customRenderer = customRenderers[component.id] ?? customRenderers[component.type];

console.log({
  isInheritedToString: customRenderer === Object.prototype.toString,
  type: typeof customRenderer,
});
NODE

Repository: asgardeo/javascript

Length of output: 110


🏁 Script executed:

#!/bin/bash
# Locate the file and check if it exists
fd "AuthOptionFactory.tsx" --type f

Repository: asgardeo/javascript

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Read the specific lines to verify the code matches the review
sed -n '222,238p' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx

Repository: asgardeo/javascript

Length of output: 607


🏁 Script executed:

#!/bin/bash
# Read more lines to see how customRenderer is used
sed -n '222,250p' packages/react/src/components/presentation/auth/AuthOptionFactory.tsx

Repository: asgardeo/javascript

Length of output: 1027


🏁 Script executed:

#!/bin/bash
# Find where customRenderers is defined/used in this function
head -n 240 packages/react/src/components/presentation/auth/AuthOptionFactory.tsx | tail -n 50

Repository: asgardeo/javascript

Length of output: 2011


🏁 Script executed:

#!/bin/bash
# Find ComponentRenderer and ComponentRendererMap type definitions
fd "ComponentRenderer" --type f -e ts -e tsx | head -20

Repository: asgardeo/javascript

Length of output: 212


🏁 Script executed:

#!/bin/bash
# Read the ComponentRendererContext to see type definitions
cat packages/react/src/contexts/ComponentRenderer/ComponentRendererContext.ts

Repository: asgardeo/javascript

Length of output: 1763


Guard renderer lookup to own function-valued registrations.

With the default {} renderer map, an id/type like toString or constructor resolves from Object.prototype, so a server-provided component can bypass built-in rendering and invoke a non-renderer. Check own properties and typeof renderer === 'function' before calling.

🛡️ Proposed safer lookup
 import ComponentRendererContext, {
   ComponentRenderContext,
+  ComponentRenderer,
 } from '../../../contexts/ComponentRenderer/ComponentRendererContext';
-  const customRenderer: ComponentRenderer | undefined =
-    customRenderers[component.id] ?? customRenderers[component.type as string];
+  const getCustomRenderer = (rendererKey?: string): ComponentRenderer | undefined => {
+    if (!rendererKey || !Object.prototype.hasOwnProperty.call(customRenderers, rendererKey)) {
+      return undefined;
+    }
+
+    const renderer: ComponentRenderer = customRenderers[rendererKey];
+
+    return typeof renderer === 'function' ? renderer : undefined;
+  };
+
+  const customRenderer = getCustomRenderer(component.id) ?? getCustomRenderer(component.type as string);
   if (customRenderer) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
const getCustomRenderer = (rendererKey?: string): ComponentRenderer | undefined => {
if (!rendererKey || !Object.prototype.hasOwnProperty.call(customRenderers, rendererKey)) {
return undefined;
}
const renderer: ComponentRenderer = customRenderers[rendererKey];
return typeof renderer === 'function' ? renderer : undefined;
};
const customRenderer = getCustomRenderer(component.id) ?? getCustomRenderer(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);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react/src/components/presentation/auth/AuthOptionFactory.tsx` around
lines 222 - 238, The lookup of customRenderer should ensure entries come from
customRenderers' own properties and are functions: instead of directly reading
customRenderers[component.id] or [component.type], check
Object.prototype.hasOwnProperty.call(customRenderers, component.id) or
component.type and that the resolved value is typeof 'function' before assigning
to customRenderer; then only call customRenderer(component, renderCtx) when both
checks pass (references: customRenderers, customRenderer, component.id,
component.type, renderCtx, and the return call).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Extensions] Custom Component Renderers for Flow UI Extensibility

3 participants