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
6 changes: 6 additions & 0 deletions .changeset/fifty-dragons-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@asgardeo/javascript': patch
'@asgardeo/react': patch
---

Align error handling
8 changes: 7 additions & 1 deletion packages/javascript/src/models/v2/embedded-signin-flow-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -294,7 +300,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"
Expand Down
8 changes: 7 additions & 1 deletion packages/javascript/src/models/v2/embedded-signup-flow-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -204,7 +210,7 @@ export type EmbeddedSignUpFlowInitiateRequest = {
*/
export interface EmbeddedSignUpFlowRequest extends Partial<EmbeddedSignUpFlowInitiateRequest> {
flowId?: string;
actionId?: string;
action?: string;
inputs?: Record<string, any>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,14 @@ export interface BaseSignInRenderProps {
values: Record<string, string>;

/**
* Form errors
* Field validation errors
*/
errors: Record<string, string>;
fieldErrors: Record<string, string>;

/**
* API error (if any)
*/
error?: Error | null;

/**
* Touched fields
Expand Down Expand Up @@ -85,7 +90,7 @@ export interface BaseSignInRenderProps {
/**
* Function to validate the form
*/
validateForm: () => {isValid: boolean; errors: Record<string, string>};
validateForm: () => {isValid: boolean; fieldErrors: Record<string, string>};

/**
* Flow title
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -266,6 +276,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
components = [],
onSubmit,
onError,
error: externalError,
className = '',
inputClassName = '',
buttonClassName = '',
Expand All @@ -285,6 +296,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
const styles = useStyles(theme, theme.vars.colors.text.primary);

const [isSubmitting, setIsSubmitting] = useState(false);
const [apiError, setApiError] = useState<Error | null>(null);

const isLoading: boolean = externalIsLoading || isSubmitting;

Expand All @@ -294,7 +306,11 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
*/
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();
Expand All @@ -315,7 +331,11 @@ const BaseSignInContent: FC<BaseSignInProps> = ({

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,
Expand All @@ -325,6 +345,15 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
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;
},
});
Expand Down Expand Up @@ -364,13 +393,16 @@ const BaseSignInContent: FC<BaseSignInProps> = ({

/**
* 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);
Expand All @@ -397,6 +429,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
}

setIsSubmitting(true);
setApiError(null);
clearMessages();
console.log('Submitting component:', component, 'with data:', data);

Expand Down Expand Up @@ -502,14 +535,18 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
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 || [],
Expand Down Expand Up @@ -569,6 +606,13 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
</Card.Header>
)}
<Card.Content>
{externalError && (
<div className={styles.flowMessagesContainer}>
<Alert variant="error" className={cx(styles.flowMessageItem, messageClasses)}>
<Alert.Description>{externalError.message}</Alert.Description>
</Alert>
</div>
)}
{flowMessages && flowMessages.length > 0 && (
<div className={styles.flowMessagesContainer}>
{flowMessages.map((message, index) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ const SignIn: FC<SignInProps> = ({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');
Expand Down Expand Up @@ -408,15 +407,14 @@ const SignIn: FC<SignInProps> = ({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;
}
Expand All @@ -430,10 +428,6 @@ const SignIn: FC<SignInProps> = ({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');
}

Expand All @@ -450,21 +444,21 @@ const SignIn: FC<SignInProps> = ({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) {
Expand Down Expand Up @@ -492,8 +486,6 @@ const SignIn: FC<SignInProps> = ({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;
Expand All @@ -509,14 +501,13 @@ const SignIn: FC<SignInProps> = ({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);
Expand All @@ -527,7 +518,6 @@ const SignIn: FC<SignInProps> = ({className, size = 'medium', onSuccess, onError
* Handle authentication errors.
*/
const handleError = (error: Error): void => {
console.error('Authentication error:', error);
setError(error);
};

Expand Down Expand Up @@ -569,7 +559,6 @@ const SignIn: FC<SignInProps> = ({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
Expand All @@ -594,6 +583,7 @@ const SignIn: FC<SignInProps> = ({className, size = 'medium', onSuccess, onError
isLoading={isLoading || !isInitialized || !isFlowInitialized}
onSubmit={handleSubmit}
onError={handleError}
error={flowError}
className={className}
size={size}
variant={variant}
Expand Down
Loading
Loading