From da370c8d819a22d4122299787496a2dc27ecb14b Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 23 Dec 2025 14:49:56 +0530 Subject: [PATCH 1/3] Refactor actionId to action in embedded sign-in and sign-up flow types --- .../javascript/src/models/v2/embedded-signin-flow-v2.ts | 2 +- .../javascript/src/models/v2/embedded-signup-flow-v2.ts | 2 +- .../components/presentation/auth/SignUp/v2/BaseSignUp.tsx | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts b/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts index 700f23e6..f2afe75b 100644 --- a/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts +++ b/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts @@ -294,7 +294,7 @@ export type EmbeddedSignInFlowInitiateRequest = { * // Continue existing flow with user input * const stepRequest: EmbeddedSignInFlowRequest = { * flowId: "flow_12345", - * actionId: "action_001", + * action: "action_001", * inputs: { * username: "user@example.com", * password: "securePassword123" diff --git a/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts index 273cb28f..a8bee6d3 100644 --- a/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts +++ b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts @@ -204,7 +204,7 @@ export type EmbeddedSignUpFlowInitiateRequest = { */ export interface EmbeddedSignUpFlowRequest extends Partial { flowId?: string; - actionId?: string; + action?: string; inputs?: Record; } diff --git a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx index 423b7b3c..128c71ee 100644 --- a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx @@ -461,7 +461,7 @@ const BaseSignUpContent: FC = ({ const payload: EmbeddedFlowExecuteRequestPayload = { ...(currentFlow.flowId && {flowId: currentFlow.flowId}), flowType: (currentFlow as any).flowType || 'REGISTRATION', - ...(component.id && {actionId: component.id}), + ...(component.id && {action: component.id}), inputs: filteredInputs, } as any; @@ -493,7 +493,7 @@ const BaseSignUpContent: FC = ({ /** * Check if the response contains a redirection URL and perform the redirect if necessary. * @param response - The sign-up response - * @param component - The component that triggered the submission (needed for actionId) + * @param component - The component that triggered the submission (needed for action) * @returns true if a redirect was performed, false otherwise */ const handleRedirectionIfNeeded = (response: EmbeddedFlowExecuteResponse, component: any): boolean => { @@ -538,7 +538,7 @@ const BaseSignUpContent: FC = ({ code, state, }, - actionId: '', + action: '', } as any; try { @@ -617,7 +617,7 @@ const BaseSignUpContent: FC = ({ code, state, }, - actionId: '', + action: '', } as any; try { From 679b47a56f274385a2c18d299742fcc75658c2ad Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 23 Dec 2025 16:02:21 +0530 Subject: [PATCH 2/3] Align error handling --- .../src/models/v2/embedded-signin-flow-v2.ts | 6 ++ .../src/models/v2/embedded-signup-flow-v2.ts | 6 ++ .../auth/SignIn/v2/BaseSignIn.tsx | 60 +++++++++++++--- .../presentation/auth/SignIn/v2/SignIn.tsx | 38 ++++------ .../auth/SignUp/v2/BaseSignUp.tsx | 72 +++++++++++++++---- .../react/src/utils/v2/flowTransformer.ts | 7 ++ 6 files changed, 143 insertions(+), 46 deletions(-) diff --git a/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts b/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts index f2afe75b..b30f2b14 100644 --- a/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts +++ b/packages/javascript/src/models/v2/embedded-signin-flow-v2.ts @@ -172,6 +172,12 @@ export interface EmbeddedSignInFlowResponse extends ExtendedEmbeddedSignInFlowRe */ flowId: string; + /** + * Optional reason for flow failure in case of an error. + * Provides additional context when flowStatus is set to ERROR. + */ + failureReason?: string; + /** * Current status of the sign-in flow. * Determines the next action required by the client application. diff --git a/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts index a8bee6d3..d3233fe0 100644 --- a/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts +++ b/packages/javascript/src/models/v2/embedded-signup-flow-v2.ts @@ -146,6 +146,12 @@ export interface EmbeddedSignUpFlowResponse extends ExtendedEmbeddedSignUpFlowRe */ flowId: string; + /** + * Optional reason for flow failure in case of an error. + * Provides additional context when flowStatus is set to ERROR. + */ + failureReason?: string; + /** * Current status of the sign-up flow. * Determines whether more input is needed or the flow is complete. diff --git a/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx index a6d36d92..e7ced69a 100644 --- a/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx @@ -48,9 +48,14 @@ export interface BaseSignInRenderProps { values: Record; /** - * Form errors + * Field validation errors */ - errors: Record; + fieldErrors: Record; + + /** + * API error (if any) + */ + error?: Error | null; /** * Touched fields @@ -85,7 +90,7 @@ export interface BaseSignInRenderProps { /** * Function to validate the form */ - validateForm: () => {isValid: boolean; errors: Record}; + validateForm: () => {isValid: boolean; fieldErrors: Record}; /** * Flow title @@ -127,6 +132,11 @@ export interface BaseSignInProps { */ errorClassName?: string; + /** + * Error object to display + */ + error?: Error | null; + /** * Flag to determine if the component is ready to be rendered. */ @@ -266,6 +276,7 @@ const BaseSignInContent: FC = ({ components = [], onSubmit, onError, + error: externalError, className = '', inputClassName = '', buttonClassName = '', @@ -285,6 +296,7 @@ const BaseSignInContent: FC = ({ const styles = useStyles(theme, theme.vars.colors.text.primary); const [isSubmitting, setIsSubmitting] = useState(false); + const [apiError, setApiError] = useState(null); const isLoading: boolean = externalIsLoading || isSubmitting; @@ -294,7 +306,11 @@ const BaseSignInContent: FC = ({ */ const handleError = useCallback( (error: any) => { - const errorMessage: string = extractErrorMessage(error, t); + // Extract error message from response failureReason or use extractErrorMessage + const errorMessage: string = error?.failureReason || extractErrorMessage(error, t); + + // Set the API error state + setApiError(error instanceof Error ? error : new Error(errorMessage)); // Clear existing messages and add the error message clearMessages(); @@ -315,7 +331,11 @@ const BaseSignInContent: FC = ({ const processComponents = (comps: EmbeddedFlowComponent[]) => { comps.forEach(component => { - if (component.type === 'TEXT_INPUT' || component.type === 'PASSWORD_INPUT') { + if ( + component.type === 'TEXT_INPUT' || + component.type === 'PASSWORD_INPUT' || + component.type === 'EMAIL_INPUT' + ) { const identifier: string = component.ref; fields.push({ name: identifier, @@ -325,6 +345,15 @@ const BaseSignInContent: FC = ({ if (component.required && (!value || value.trim() === '')) { return t('validations.required.field.error'); } + // Add email validation if it's an email field + if ( + (component.type === 'EMAIL_INPUT' || component.variant === 'EMAIL') && + value && + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) + ) { + return t('field.email.invalid'); + } + return null; }, }); @@ -364,13 +393,16 @@ const BaseSignInContent: FC = ({ /** * Handle input value changes. + * Only updates the value without marking as touched. + * Touched state is set on blur to avoid premature validation. */ const handleInputChange = (name: string, value: string): void => { setFormValue(name, value); }; /** - * Handle input blur events (when field loses focus). + * Handle input blur event. + * Marks the field as touched, which triggers validation. */ const handleInputBlur = (name: string): void => { setFormTouched(name, true); @@ -397,6 +429,7 @@ const BaseSignInContent: FC = ({ } setIsSubmitting(true); + setApiError(null); clearMessages(); console.log('Submitting component:', component, 'with data:', data); @@ -502,14 +535,18 @@ const BaseSignInContent: FC = ({ if (children) { const renderProps: BaseSignInRenderProps = { values: formValues, - errors: formErrors, + fieldErrors: formErrors, + error: apiError, touched: touchedFields, isValid: isFormValid, isLoading, components, handleInputChange, handleSubmit, - validateForm, + validateForm: () => { + const result = validateForm(); + return {isValid: result.isValid, fieldErrors: result.errors}; + }, title: flowTitle || t('signin.heading'), subtitle: flowSubtitle || t('signin.subheading'), messages: flowMessages || [], @@ -569,6 +606,13 @@ const BaseSignInContent: FC = ({ )} + {externalError && ( +
+ + {externalError.message} + +
+ )} {flowMessages && flowMessages.length > 0 && (
{flowMessages.map((message, index) => ( diff --git a/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx index 71e639f7..dbaa69bc 100644 --- a/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx @@ -274,7 +274,6 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError * Clears flow state, creates error, and cleans up URL. */ const handleOAuthError = (error: string, errorDescription: string | null): void => { - console.warn('[SignIn] OAuth error detected:', error); clearFlowState(); const errorMessage = errorDescription || `OAuth error: ${error}`; const err = new AsgardeoRuntimeError(errorMessage, 'SIGN_IN_ERROR', 'react'); @@ -408,15 +407,14 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError cleanupFlowUrlParams(); } } catch (error) { - const err = error as Error; + const err = error as any; clearFlowState(); - // Extract error message - const errorMessage = err instanceof Error ? err.message : String(err); + // Extract error message from response or error object + const errorMessage = err?.failureReason || (err instanceof Error ? err.message : String(err)); - // Create error with backend message - const displayError = new AsgardeoRuntimeError(errorMessage, 'SIGN_IN_ERROR', 'react'); - setError(displayError); + // Set error with the extracted message + setError(new Error(errorMessage)); initializationAttemptedRef.current = false; return; } @@ -430,10 +428,6 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError const effectiveFlowId = payload.flowId || currentFlowId; if (!effectiveFlowId) { - console.error('[SignIn] handleSubmit - ERROR: No flowId available', { - payloadFlowId: payload.flowId, - currentFlowId, - }); throw new Error('No active flow ID'); } @@ -450,21 +444,21 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError return; } - const {flowId, components} = normalizeFlowResponse(response, t, { + const {flowId, components, ...rest} = normalizeFlowResponse(response, t, { resolveTranslations: !children, }); // Handle Error flow status - flow has failed and is invalidated if (response.flowStatus === EmbeddedSignInFlowStatusV2.Error) { - console.error('[SignIn] Flow returned Error status, clearing flow state'); clearFlowState(); // Extract failureReason from response if available const failureReason = (response as any)?.failureReason; const errorMessage = failureReason || 'Authentication flow failed. Please try again.'; - const err = new AsgardeoRuntimeError(errorMessage, 'SIGN_IN_ERROR', 'react'); + const err = new Error(errorMessage); setError(err); cleanupFlowUrlParams(); - return; + // Throw the error so it's caught by the catch block and propagated to BaseSignIn + throw err; } if (response.flowStatus === EmbeddedSignInFlowStatusV2.Complete) { @@ -492,8 +486,6 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError if (finalRedirectUrl && window?.location) { window.location.href = finalRedirectUrl; - } else { - console.warn('[SignIn] Flow completed but no redirect URL available'); } return; @@ -509,14 +501,13 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError cleanupFlowUrlParams(); } } catch (error) { - const err = error as Error; + const err = error as any; clearFlowState(); - // Extract error message - const errorMessage = err instanceof Error ? err.message : String(err); + // Extract error message from response or error object + const errorMessage = err?.failureReason || (err instanceof Error ? err.message : String(err)); - const displayError = new AsgardeoRuntimeError(errorMessage, 'SIGN_IN_ERROR', 'react'); - setError(displayError); + setError(new Error(errorMessage)); return; } finally { setIsSubmitting(false); @@ -527,7 +518,6 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError * Handle authentication errors. */ const handleError = (error: Error): void => { - console.error('Authentication error:', error); setError(error); }; @@ -569,7 +559,6 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError }; handleSubmit(submitPayload).catch(error => { - console.error('[SignIn] OAuth callback submission failed:', error); cleanupOAuthUrlParams(true); }); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -594,6 +583,7 @@ const SignIn: FC = ({className, size = 'medium', onSuccess, onError isLoading={isLoading || !isInitialized || !isFlowInitialized} onSubmit={handleSubmit} onError={handleError} + error={flowError} className={className} size={size} variant={variant} diff --git a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx index 128c71ee..f3c33932 100644 --- a/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx @@ -52,9 +52,14 @@ export interface BaseSignUpRenderProps { values: Record; /** - * Form errors + * Field validation errors */ - errors: Record; + fieldErrors: Record; + + /** + * API error (if any) + */ + error?: Error | null; /** * Touched fields @@ -89,7 +94,7 @@ export interface BaseSignUpRenderProps { /** * Function to validate the form */ - validateForm: () => {isValid: boolean; errors: Record}; + validateForm: () => {isValid: boolean; fieldErrors: Record}; /** * Flow title @@ -131,6 +136,11 @@ export interface BaseSignUpProps { */ errorClassName?: string; + /** + * Error object to display + */ + error?: Error | null; + /** * Custom CSS class name for form inputs. */ @@ -243,6 +253,7 @@ const BaseSignUpContent: FC = ({ onError, onFlowChange, onComplete, + error: externalError, className = '', inputClassName = '', buttonClassName = '', @@ -267,7 +278,11 @@ const BaseSignUpContent: FC = ({ */ const handleError = useCallback( (error: any) => { - const errorMessage: string = extractErrorMessage(error, t); + // Extract error message from response failureReason or use extractErrorMessage + const errorMessage: string = error?.failureReason || extractErrorMessage(error, t); + + // Set the API error state + setApiError(error instanceof Error ? error : new Error(errorMessage)); // Clear existing messages and add the error message clearMessages(); @@ -282,7 +297,7 @@ const BaseSignUpContent: FC = ({ const [isLoading, setIsLoading] = useState(false); const [isFlowInitialized, setIsFlowInitialized] = useState(false); const [currentFlow, setCurrentFlow] = useState(null); - const [formData, setFormData] = useState>({}); + const [apiError, setApiError] = useState(null); const initializationAttemptedRef = useRef(false); @@ -333,7 +348,11 @@ const BaseSignUpContent: FC = ({ const processComponents = (comps: any[]) => { comps.forEach(component => { - if (component.type === EmbeddedFlowComponentType.TextInput) { + if ( + component.type === EmbeddedFlowComponentType.TextInput || + component.type === EmbeddedFlowComponentType.PasswordInput || + component.type === EmbeddedFlowComponentType.EmailInput + ) { // Use component.ref (mapped identifier) as the field name instead of component.id // This ensures form field names match what the input components use const fieldName = component.ref || component.id; @@ -347,13 +366,14 @@ const BaseSignUpContent: FC = ({ return t('validations.required.field.error'); } // Add email validation if it's an email field - if (component.variant === 'EMAIL' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { + if ( + (component.type === EmbeddedFlowComponentType.EmailInput || component.variant === 'EMAIL') && + value && + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) + ) { return t('field.email.invalid'); } - // Add password strength validation if it's a password field - if (component.type === 'PASSWORD_INPUT' && value && value.length < 8) { - return t('field.password.weak'); - } + return null; }, }); @@ -377,7 +397,7 @@ const BaseSignUpContent: FC = ({ initialValues: {}, fields: formFields, validateOnBlur: true, - validateOnChange: true, + validateOnChange: false, requiredMessage: t('validations.required.field.error'), }); @@ -418,9 +438,18 @@ const BaseSignUpContent: FC = ({ /** * Handle input value changes. + * Only updates the value without marking as touched. + * Touched state is set on blur to avoid premature validation. */ const handleInputChange = (name: string, value: string) => { setFormValue(name, value); + }; + + /** + * Handle input blur event. + * Marks the field as touched, which triggers validation. + */ + const handleInputBlur = (name: string) => { setFormTouched(name, true); }; @@ -445,6 +474,7 @@ const BaseSignUpContent: FC = ({ } setIsLoading(true); + setApiError(null); clearMessages(); try { @@ -701,6 +731,7 @@ const BaseSignUpContent: FC = ({ { buttonClassName: buttonClasses, inputClassName: inputClasses, + onInputBlur: handleInputBlur, onSubmit: handleSubmit, size, variant, @@ -717,6 +748,7 @@ const BaseSignUpContent: FC = ({ inputClasses, buttonClasses, handleSubmit, + handleInputBlur, ], ); @@ -745,6 +777,7 @@ const BaseSignUpContent: FC = ({ (async () => { setIsLoading(true); + setApiError(null); clearMessages(); try { @@ -788,14 +821,18 @@ const BaseSignUpContent: FC = ({ if (children) { const renderProps: BaseSignUpRenderProps = { values: formValues, - errors: formErrors, + fieldErrors: formErrors, + error: apiError, touched: touchedFields, isValid: isFormValid, isLoading, components: currentFlow?.data?.components || [], handleInputChange, handleSubmit, - validateForm, + validateForm: () => { + const result = validateForm(); + return {isValid: result.isValid, fieldErrors: result.errors}; + }, title: flowTitle || t('signup.heading'), subtitle: flowSubtitle || t('signup.subheading'), messages: flowMessages || [], @@ -856,6 +893,13 @@ const BaseSignUpContent: FC = ({ )} + {externalError && ( +
+ + {externalError.message} + +
+ )} {flowMessages && flowMessages.length > 0 && (
{flowMessages.map((message: any, index: number) => ( diff --git a/packages/react/src/utils/v2/flowTransformer.ts b/packages/react/src/utils/v2/flowTransformer.ts index c860fbe2..e698c8a9 100644 --- a/packages/react/src/utils/v2/flowTransformer.ts +++ b/packages/react/src/utils/v2/flowTransformer.ts @@ -208,6 +208,7 @@ export const transformComponents = ( /** * Extract error message from flow error response. * Supports any flow error response that follows the standard structure. + * Prioritizes failureReason if present, otherwise falls back to translated generic message. * * @param error - The error response object * @param t - Translation function for fallback messages @@ -219,10 +220,16 @@ export const extractErrorMessage = ( t: UseTranslation['t'], defaultErrorKey: string = 'errors.flow.generic', ): string => { + // Check for failureReason in the error object if (error && typeof error === 'object' && error.failureReason) { return error.failureReason; } + // Check if error is a standard Error object with a message + if (error instanceof Error && error.message) { + return error.message; + } + // Fallback to a generic error message return t(defaultErrorKey); }; From ca71b3b783ece9ff46ea8d1519e1a69547bfe210 Mon Sep 17 00:00:00 2001 From: Brion Date: Tue, 23 Dec 2025 16:02:48 +0530 Subject: [PATCH 3/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/fifty-dragons-drive.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fifty-dragons-drive.md diff --git a/.changeset/fifty-dragons-drive.md b/.changeset/fifty-dragons-drive.md new file mode 100644 index 00000000..1db896f4 --- /dev/null +++ b/.changeset/fifty-dragons-drive.md @@ -0,0 +1,6 @@ +--- +'@asgardeo/javascript': patch +'@asgardeo/react': patch +--- + +Align error handling