diff --git a/.changeset/brave-waves-stick.md b/.changeset/brave-waves-stick.md new file mode 100644 index 00000000..743d1877 --- /dev/null +++ b/.changeset/brave-waves-stick.md @@ -0,0 +1,6 @@ +--- +'@asgardeo/javascript': minor +'@asgardeo/react': minor +--- + +Add ⚡️ Thunder support for `SignUp` component. diff --git a/packages/javascript/src/api/v2/executeEmbeddedSignUpFlowV2.ts b/packages/javascript/src/api/v2/executeEmbeddedSignUpFlowV2.ts new file mode 100644 index 00000000..acb8dfde --- /dev/null +++ b/packages/javascript/src/api/v2/executeEmbeddedSignUpFlowV2.ts @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2025, 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 { + EmbeddedFlowExecuteRequestConfigV2, + EmbeddedSignUpFlowResponseV2, + EmbeddedSignUpFlowStatusV2, +} from '../../models/v2/embedded-signup-flow-v2'; +import AsgardeoAPIError from '../../errors/AsgardeoAPIError'; + +const executeEmbeddedSignUpFlowV2 = async ({ + url, + baseUrl, + payload, + sessionDataKey, + ...requestConfig +}: EmbeddedFlowExecuteRequestConfigV2): Promise => { + if (!payload) { + throw new AsgardeoAPIError( + 'Registration payload is required', + 'executeEmbeddedSignUpFlow-ValidationError-002', + 'javascript', + 400, + 'If a registration payload is not provided, the request cannot be constructed correctly.', + ); + } + + let endpoint: string = url ?? `${baseUrl}/flow/execute`; + + const response: Response = await fetch(endpoint, { + ...requestConfig, + method: requestConfig.method || 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const errorText = await response.text(); + + throw new AsgardeoAPIError( + `Registration request failed: ${errorText}`, + 'executeEmbeddedSignUpFlow-ResponseError-001', + 'javascript', + response.status, + response.statusText, + ); + } + + const flowResponse: EmbeddedSignUpFlowResponseV2 = await response.json(); + + // IMPORTANT: Only applicable for Asgardeo V2 platform. + // Check if the flow is complete and has an assertion and sessionDataKey is provided, then call OAuth2 authorize. + if ( + flowResponse.flowStatus === EmbeddedSignUpFlowStatusV2.Complete && + (flowResponse as any).assertion && + sessionDataKey + ) { + try { + const oauth2Response: Response = await fetch(`${baseUrl}/oauth2/authorize`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...requestConfig.headers, + }, + body: JSON.stringify({ + assertion: (flowResponse as any).assertion, + sessionDataKey, + }), + credentials: 'include', + }); + + if (!oauth2Response.ok) { + const oauth2ErrorText: string = await oauth2Response.text(); + + throw new AsgardeoAPIError( + `OAuth2 authorization failed: ${oauth2ErrorText}`, + 'executeEmbeddedSignUpFlow-OAuth2Error-002', + 'javascript', + oauth2Response.status, + oauth2Response.statusText, + ); + } + + const oauth2Result = await oauth2Response.json(); + + return { + flowStatus: flowResponse.flowStatus, + redirectUrl: oauth2Result.redirect_uri, + } as any; + } catch (authError) { + throw new AsgardeoAPIError( + `OAuth2 authorization failed: ${authError instanceof Error ? authError.message : 'Unknown error'}`, + 'executeEmbeddedSignUpFlow-OAuth2Error-001', + 'javascript', + 500, + 'Failed to complete OAuth2 authorization after successful embedded sign-up flow.', + ); + } + } + + return flowResponse; +}; + +export default executeEmbeddedSignUpFlowV2; diff --git a/packages/javascript/src/index.ts b/packages/javascript/src/index.ts index 7e0717c1..e92a4b48 100644 --- a/packages/javascript/src/index.ts +++ b/packages/javascript/src/index.ts @@ -46,6 +46,7 @@ export {default as updateOrganization, createPatchOperations, UpdateOrganization export {default as updateMeProfile, UpdateMeProfileConfig} from './api/updateMeProfile'; export {default as getBrandingPreference, GetBrandingPreferenceConfig} from './api/getBrandingPreference'; export {default as executeEmbeddedSignInFlowV2} from './api/v2/executeEmbeddedSignInFlowV2'; +export {default as executeEmbeddedSignUpFlowV2} from './api/v2/executeEmbeddedSignUpFlowV2'; export {default as ApplicationNativeAuthenticationConstants} from './constants/ApplicationNativeAuthenticationConstants'; export {default as TokenConstants} from './constants/TokenConstants'; @@ -90,6 +91,16 @@ export { EmbeddedFlowExecuteRequestPayload, EmbeddedFlowExecuteRequestConfig, } from './models/embedded-flow'; +export { + EmbeddedSignUpFlowStatusV2, + EmbeddedSignUpFlowInitiateRequestV2, + EmbeddedSignUpFlowRequestV2, + EmbeddedSignUpFlowCompleteResponse, + EmbeddedSignUpFlowResponseV2, + ExtendedEmbeddedSignUpFlowResponseV2, + EmbeddedSignUpFlowTypeV2, + EmbeddedFlowExecuteRequestConfigV2, +} from './models/v2/embedded-signup-flow-v2'; export {FlowMode} from './models/flow'; export {AsgardeoClient} from './models/client'; export { diff --git a/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts new file mode 100644 index 00000000..b8df4a8e --- /dev/null +++ b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2025, 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 {EmbeddedFlowExecuteRequestConfig, EmbeddedFlowResponseType, EmbeddedFlowType} from '../embedded-flow'; + +export enum EmbeddedSignUpFlowStatusV2 { + Complete = 'COMPLETE', + Incomplete = 'INCOMPLETE', + Error = 'ERROR', +} + +export enum EmbeddedSignUpFlowTypeV2 { + Redirection = 'REDIRECTION', + View = 'VIEW', +} + +/** + * Extended response structure for the embedded sign-up flow V2. + * @remarks This response is only done from the SDK level. + * @experimental + */ +export interface ExtendedEmbeddedSignUpFlowResponseV2 { + /** + * The URL to redirect the user after completing the sign-up flow. + */ + redirectUrl?: string; +} + +/** + * Response structure for the new Asgardeo V2 embedded sign-up flow. + * @experimental + */ +export interface EmbeddedSignUpFlowResponseV2 extends ExtendedEmbeddedSignUpFlowResponseV2 { + flowId: string; + flowStatus: EmbeddedSignUpFlowStatusV2; + type: EmbeddedSignUpFlowTypeV2; + data: { + actions?: { + type: EmbeddedFlowResponseType; + id: string; + }[]; + inputs?: { + name: string; + type: string; + required: boolean; + }[]; + }; +} + +/** + * Response structure for the new Asgardeo V2 embedded sign-up flow when the flow is complete. + * @experimental + */ +export interface EmbeddedSignUpFlowCompleteResponse { + redirect_uri: string; +} + +/** + * Request payload for initiating the new Asgardeo V2 embedded sign-up flow. + * @experimental + */ +export type EmbeddedSignUpFlowInitiateRequestV2 = { + applicationId: string; + flowType: EmbeddedFlowType; +}; + +/** + * Request payload for executing steps in the new Asgardeo V2 embedded sign-up flow. + * @experimental + */ +export interface EmbeddedSignUpFlowRequestV2 extends Partial { + flowId?: string; + actionId?: string; + inputs?: Record; +} + +/** + * Request config for executing the new Asgardeo V2 embedded sign-up flow. + * @experimental + */ +export interface EmbeddedFlowExecuteRequestConfigV2 extends EmbeddedFlowExecuteRequestConfig { + sessionDataKey?: string; +} diff --git a/packages/react/src/AsgardeoReactClient.ts b/packages/react/src/AsgardeoReactClient.ts index 0901ad30..cc5a6d13 100644 --- a/packages/react/src/AsgardeoReactClient.ts +++ b/packages/react/src/AsgardeoReactClient.ts @@ -50,6 +50,7 @@ import { Platform, isEmpty, EmbeddedSignInFlowResponseV2, + executeEmbeddedSignUpFlowV2, } from '@asgardeo/browser'; import AuthAPI from './__temp__/api'; import getMeOrganizations from './api/getMeOrganizations'; @@ -399,19 +400,25 @@ class AsgardeoReactClient e override async signUp(options?: SignUpOptions): Promise; override async signUp(payload: EmbeddedFlowExecuteRequestPayload): Promise; override async signUp(...args: any[]): Promise { - const configData = await this.asgardeo.getConfigData(); + const config: AsgardeoReactConfig = (await this.asgardeo.getConfigData()) as AsgardeoReactConfig; const firstArg = args[0]; + const baseUrl: string = config?.baseUrl; - if (typeof firstArg === 'object' && 'flowType' in firstArg) { - const baseUrl: string = configData?.baseUrl; + if (config.platform === Platform.AsgardeoV2) { + return executeEmbeddedSignUpFlowV2({ + baseUrl, + payload: firstArg as EmbeddedFlowExecuteRequestPayload, + }) as any; + } + if (typeof firstArg === 'object' && 'flowType' in firstArg) { return executeEmbeddedSignUpFlow({ baseUrl, payload: firstArg as EmbeddedFlowExecuteRequestPayload, }); } - navigate(getRedirectBasedSignUpUrl(configData as Config)); + navigate(getRedirectBasedSignUpUrl(config as Config)); } async request(requestConfig?: HttpRequestConfig): Promise> { diff --git a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx index f911d844..467fd151 100644 --- a/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/BaseSignUp.tsx @@ -26,8 +26,9 @@ import { AsgardeoAPIError, } from '@asgardeo/browser'; import {cx} from '@emotion/css'; -import {FC, ReactElement, useEffect, useState, useCallback, useRef} from 'react'; +import {FC, ReactElement, ReactNode, useEffect, useState, useCallback, useRef} from 'react'; import {renderSignUpComponents} from './SignUpOptionFactory'; +import {transformSimpleToComponentDriven} from './transformer'; import FlowProvider from '../../../contexts/Flow/FlowProvider'; import useFlow from '../../../contexts/Flow/useFlow'; import {useForm, FormField} from '../../../hooks/useForm'; @@ -39,6 +40,76 @@ import Logo from '../../primitives/Logo/Logo'; import Spinner from '../../primitives/Spinner/Spinner'; import useStyles from './BaseSignUp.styles'; +/** + * Render props for custom UI rendering + */ +export interface BaseSignUpRenderProps { + /** + * Form values + */ + values: Record; + + /** + * Form errors + */ + errors: Record; + + /** + * Touched fields + */ + touched: Record; + + /** + * Whether the form is valid + */ + isValid: boolean; + + /** + * Loading state + */ + isLoading: boolean; + + /** + * Current error message + */ + error: string | null; + + /** + * Flow components + */ + components: any[]; + + /** + * Function to handle input changes + */ + handleInputChange: (name: string, value: string) => void; + + /** + * Function to handle form submission + */ + handleSubmit: (component: any, data?: Record) => Promise; + + /** + * Function to validate the form + */ + validateForm: () => {isValid: boolean; errors: Record}; + + /** + * Flow title + */ + title: string; + + /** + * Flow subtitle + */ + subtitle: string; + + /** + * Flow messages + */ + messages: Array<{message: string; type: string}>; +} + /** * Props for the BaseSignUp component. */ @@ -120,6 +191,11 @@ export interface BaseSignUpProps { * Whether to redirect after sign-up. */ shouldRedirectAfterSignUp?: boolean; + + /** + * Render props function for custom UI + */ + children?: (props: BaseSignUpRenderProps) => ReactNode; } /** @@ -128,6 +204,7 @@ export interface BaseSignUpProps { * It accepts API functions as props to maintain framework independence. * * @example + * // Default UI * ```tsx * import { BaseSignUp } from '@asgardeo/react'; * @@ -142,20 +219,41 @@ export interface BaseSignUpProps { * // Your API call to handle sign-up * return await handleSignUp(payload); * }} - * onSuccess={(response) => { - * console.log('Success:', response); - * }} * onError={(error) => { + * onError={(error) => { * console.error('Error:', error); * }} - * onComplete={(redirectUrl) => { + * onComplete={(response) => { * // Platform-specific redirect handling (e.g., Next.js router.push) - * router.push(redirectUrl); // or window.location.href = redirectUrl + * router.push(response); // or window.location.href = redirectUrl * }} * className="max-w-md mx-auto" * /> * ); * }; * ``` + * + * @example + * // Custom UI with render props + * ```tsx + * + * {({values, errors, handleInputChange, handleSubmit, isLoading, components}) => ( + *
+ * handleInputChange('username', e.target.value)} + * /> + * {errors.username && {errors.username}} + * + *
+ * )} + *
+ * ``` */ const BaseSignUp: FC = props => { const {theme, colorScheme} = useTheme(); @@ -191,6 +289,7 @@ const BaseSignUpContent: FC = ({ size = 'medium', variant = 'outlined', isInitialized, + children, }) => { const {theme, colorScheme} = useTheme(); const {t} = useTranslation(); @@ -205,6 +304,35 @@ const BaseSignUpContent: FC = ({ const initializationAttemptedRef = useRef(false); + /** + * Normalize flow response to ensure component-driven format + */ + const normalizeFlowResponse = useCallback( + (response: EmbeddedFlowExecuteResponse): EmbeddedFlowExecuteResponse => { + // If response already has components, return as-is (Asgardeo/IS format) + if (response?.data?.components && Array.isArray(response.data.components)) { + return response; + } + + // If response has simple inputs/actions (Thunder format), transform to component-driven + if (response?.data && ((response.data as any).inputs || (response.data as any).actions)) { + const transformedComponents = transformSimpleToComponentDriven(response, t); + + return { + ...response, + data: { + ...response.data, + components: transformedComponents, + }, + }; + } + + // Return as-is if no transformation needed + return response; + }, + [t], + ); + /** * Extract form fields from flow components */ @@ -331,7 +459,8 @@ const BaseSignUpContent: FC = ({ actionId: component.id, } as any; - const response = await onSubmit(payload); + const rawResponse = await onSubmit(payload); + const response = normalizeFlowResponse(rawResponse); onFlowChange?.(response); if (response.flowStatus === EmbeddedFlowStatus.Complete) { @@ -600,7 +729,8 @@ const BaseSignUpContent: FC = ({ setError(null); try { - const response = await onInitialize(); + const rawResponse = await onInitialize(); + const response = normalizeFlowResponse(rawResponse); setCurrentFlow(response); setIsFlowInitialized(true); @@ -632,10 +762,32 @@ const BaseSignUpContent: FC = ({ onError, onFlowChange, setupFormFields, + normalizeFlowResponse, afterSignUpUrl, t, ]); + // If render props are provided, use them + if (children) { + const renderProps: BaseSignUpRenderProps = { + values: formValues, + errors: formErrors, + touched: touchedFields, + isValid: isFormValid, + isLoading, + error, + components: currentFlow?.data?.components || [], + handleInputChange, + handleSubmit, + validateForm, + title: flowTitle || t('signup.title'), + subtitle: flowSubtitle || t('signup.subtitle'), + messages: flowMessages || [], + }; + + return
{children(renderProps)}
; + } + if (!isFlowInitialized && isLoading) { return ( diff --git a/packages/react/src/components/presentation/SignUp/SignUp.tsx b/packages/react/src/components/presentation/SignUp/SignUp.tsx index 6cab4b59..587e6f3a 100644 --- a/packages/react/src/components/presentation/SignUp/SignUp.tsx +++ b/packages/react/src/components/presentation/SignUp/SignUp.tsx @@ -21,21 +21,33 @@ import { EmbeddedFlowExecuteResponse, EmbeddedFlowResponseType, EmbeddedFlowType, + Platform, } from '@asgardeo/browser'; -import {FC} from 'react'; -import BaseSignUp, {BaseSignUpProps} from './BaseSignUp'; +import {FC, ReactNode} from 'react'; +import BaseSignUp, {BaseSignUpProps, BaseSignUpRenderProps} from './BaseSignUp'; import useAsgardeo from '../../../contexts/Asgardeo/useAsgardeo'; +/** + * Render props function parameters (re-exported from BaseSignUp for convenience) + */ +export type SignUpRenderProps = BaseSignUpRenderProps; + /** * Props for the SignUp component. */ -export type SignUpProps = BaseSignUpProps; +export type SignUpProps = BaseSignUpProps & { + /** + * Render props function for custom UI + */ + children?: (props: SignUpRenderProps) => ReactNode; +}; /** * A styled SignUp component that provides embedded sign-up flow with pre-built styling. * This component handles the API calls for sign-up and delegates UI logic to BaseSignUp. * * @example + * // Default UI * ```tsx * import { SignUp } from '@asgardeo/react'; * @@ -60,6 +72,45 @@ export type SignUpProps = BaseSignUpProps; * ); * }; * ``` + * + * @example + * // Custom UI with render props + * ```tsx + * import { SignUp } from '@asgardeo/react'; + * + * const App = () => { + * return ( + * console.error('Error:', error)} + * onComplete={(response) => console.log('Success:', response)} + * > + * {({values, errors, handleInputChange, handleSubmit, isLoading, components}) => ( + *
+ *

Custom Sign Up

+ * {isLoading ? ( + *

Loading...

+ * ) : ( + *
{ + * e.preventDefault(); + * handleSubmit(components[0], values); + * }}> + * handleInputChange('username', e.target.value)} + * /> + * {errors.username && {errors.username}} + * + *
+ * )} + *
+ * )} + *
+ * ); + * }; + * ``` */ const SignUp: FC = ({ className, @@ -68,25 +119,35 @@ const SignUp: FC = ({ onError, onComplete, shouldRedirectAfterSignUp = true, + children, ...rest }) => { - const {signUp, isInitialized} = useAsgardeo(); + const {signUp, isInitialized, applicationId, platform} = useAsgardeo(); /** * Initialize the sign-up flow. */ - const handleInitialize = async (payload?: EmbeddedFlowExecuteRequestPayload): Promise => - await signUp( - payload || { - flowType: EmbeddedFlowType.Registration, - }, - ) as EmbeddedFlowExecuteResponse; + const handleInitialize = async ( + payload?: EmbeddedFlowExecuteRequestPayload, + ): Promise => { + // For Thunder/AsgardeoV2 platform, it uses the same API but might return different response format + // The transformation will be handled by BaseSignUp's normalizeFlowResponse function + + // If no payload provided, create initial payload + // For Thunder (Platform.AsgardeoV2), include applicationId for proper initialization + const initialPayload = payload || { + flowType: EmbeddedFlowType.Registration, + ...(platform === Platform.AsgardeoV2 && applicationId && {applicationId}), + }; + + return (await signUp(initialPayload)) as EmbeddedFlowExecuteResponse; + }; /** * Handle sign-up steps. */ const handleOnSubmit = async (payload: EmbeddedFlowExecuteRequestPayload): Promise => - await signUp(payload) as EmbeddedFlowExecuteResponse; + (await signUp(payload)) as EmbeddedFlowExecuteResponse; /** * Handle successful sign-up and redirect. @@ -122,6 +183,7 @@ const SignUp: FC = ({ className={className} size={size} isInitialized={isInitialized} + children={children} {...rest} /> ); diff --git a/packages/react/src/components/presentation/SignUp/transformer.ts b/packages/react/src/components/presentation/SignUp/transformer.ts new file mode 100644 index 00000000..1be17025 --- /dev/null +++ b/packages/react/src/components/presentation/SignUp/transformer.ts @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2025, 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 {EmbeddedFlowComponent, EmbeddedFlowComponentType} from '@asgardeo/browser'; +import useTranslation, {UseTranslation} from '../../../hooks/useTranslation'; + +/** + * Generate a unique ID for components + */ +const generateId = (prefix: string): string => { + const suffix: string = Math.random().toString(36).substring(2, 6); + + return `${prefix}_${suffix}`; +}; + +/** + * Convert simple input type to component variant + */ +const getInputVariant = (type: string): 'TEXT' | 'EMAIL' | 'PASSWORD' => { + switch (type.toLowerCase()) { + case 'email': + return 'EMAIL'; + case 'password': + return 'PASSWORD'; + default: + return 'TEXT'; + } +}; + +/** + * Get appropriate label for input based on name and type + */ +const getInputLabel = (name: string, type: string, t: UseTranslation['t']): string => { + const i18nKey: string = `elements.fields.${name}`; + const label: string = t(i18nKey); + + if (label === i18nKey || !label) { + return name.charAt(0).toUpperCase() + name.slice(1); + } + + return label; +}; + +/** + * Get appropriate placeholder for input based on name and type. + * + * @param name - The input field name + * @param type - The input field type + * @param t - Translation function + * @returns Localized or fallback placeholder for the input + */ +const getInputPlaceholder = (name: string, type: string, t: UseTranslation['t']): string => { + const label: string = getInputLabel(name, type, t); + const placeholder: string = t('elements.fields.placeholder', {field: label}); + + // If translation not found, fallback + if (!placeholder || placeholder === 'elements.fields.placeholder') { + return `Enter your ${label}`; + } + + return placeholder; +}; + +/** + * Convert simple input to component-driven input component + */ +const convertSimpleInputToComponent = ( + input: { + name: string; + type: string; + required: boolean; + }, + t: UseTranslation['t'], +): EmbeddedFlowComponent => { + const variant: 'TEXT' | 'EMAIL' | 'PASSWORD' = getInputVariant(input.type); + const label: string = getInputLabel(input.name, input.type, t); + const placeholder: string = getInputPlaceholder(input.name, input.type, t); + + return { + id: generateId('input'), + type: EmbeddedFlowComponentType.Input, + variant, + config: { + type: input.type === 'string' ? 'text' : input.type, + label, + placeholder, + required: input.required as boolean, + identifier: input.name, + hint: '', + }, + components: [], + }; +}; + +/** + * Convert action to component-driven button component + */ +const convertActionToComponent = ( + action: {type: string; id: string}, + t: UseTranslation['t'], +): EmbeddedFlowComponent => { + const i18nKey: string = `elements.buttons.${action.id}`; + let text: string = t(i18nKey); + + if (!text || text === i18nKey) { + text = action.id.replace(/_/g, ' '); + text = text.charAt(0).toUpperCase() + text.slice(1); + } + + return { + id: generateId('action'), + type: EmbeddedFlowComponentType.Button, + variant: 'SECONDARY', + config: { + type: 'button', + text, + actionId: action.id, + actionType: action.type, + }, + components: [], + }; +}; + +/** + * Transform simple flow response to component-driven format + */ +export const transformSimpleToComponentDriven = (response: any, t: UseTranslation['t']): EmbeddedFlowComponent[] => { + // Create input components if present + const inputComponents: EmbeddedFlowComponent[] = + response?.data?.inputs?.map((input: any) => convertSimpleInputToComponent(input, t)) || []; + + // Create action buttons if present + const actionComponents: EmbeddedFlowComponent[] = + response?.data?.actions?.map((action: any) => convertActionToComponent(action, t)) || []; + + // Add a submit button if there are inputs + const submitButton: EmbeddedFlowComponent | null = + inputComponents.length > 0 + ? { + id: generateId('button'), + type: EmbeddedFlowComponentType.Button, + variant: 'PRIMARY', + config: { + type: 'submit', + text: t('elements.buttons.signUp'), + }, + components: [], + } + : null; + + // Compose form components (inputs + submit only) + const formComponents: EmbeddedFlowComponent[] = []; + if (inputComponents.length > 0) { + formComponents.push(...inputComponents); + if (submitButton) formComponents.push(submitButton); + } + + const result: EmbeddedFlowComponent[] = []; + // Add form if there are input fields + if (formComponents.length > 0) { + result.push({ + id: generateId('form'), + type: EmbeddedFlowComponentType.Form, + config: {}, + components: formComponents, + }); + } + + // Add actions outside the form + if (actionComponents.length > 0) { + result.push(...actionComponents); + } + + return result; +}; + +/** + * Generic transformer that handles both simple and component-driven responses + */ +export const normalizeFlowResponse = ( + response: any, + t: UseTranslation['t'], +): { + flowId: string; + components: EmbeddedFlowComponent[]; +} => { + return { + flowId: response.flowId, + components: transformSimpleToComponentDriven(response, t), + }; +};