Skip to content

Add input validation support to flow components#518

Open
SajidMannikeri17 wants to merge 1 commit into
asgardeo:mainfrom
Infosys:feat/input-validation
Open

Add input validation support to flow components#518
SajidMannikeri17 wants to merge 1 commit into
asgardeo:mainfrom
Infosys:feat/input-validation

Conversation

@SajidMannikeri17
Copy link
Copy Markdown

@SajidMannikeri17 SajidMannikeri17 commented May 13, 2026

Purpose

SDK support for declarative input validation rules on flow prompts, paired
with the backend feature in asgardeo/thunder#2410.

Today, a flow's prompt inputs can carry validation rules in the flow definition
and the server returns data.fieldErrors when those rules fail. This PR makes
that feature usable from @asgardeo/react consumers — flow authors put rules on
input components, the SDK runs them client-side for instant feedback, and the SDK
surfaces server-side validation errors into the same render state used for
required-field errors.

Approach

Three layers, mirroring the design in the discussion:

  1. @asgardeo/javascript — new framework-agnostic primitives:

    • ValidationRule, ValidationRuleType, FieldError types.
    • evaluateValidationRule and buildValidatorFromRules utilities.
    • EmbeddedFlowComponent.validation and EmbeddedFlowResponseData.fieldErrors extensions on the v2 flow models.
  2. @asgardeo/react — wires the primitives into the auth components:

    • BaseSignIn, BaseSignUp, BaseAcceptInvite, BaseInviteUser, BaseRecovery — client-side rule evaluation via useForm, server-side fieldErrors projection via useEffect.
    • SignInRenderProps.fieldErrors — exposes server-side validation errors to render-prop consumers (custom UI flows).
  3. @asgardeo/i18n — default validation message keys (validation.pattern.invalid, validation.minLength.invalid, validation.maxLength.invalid) added to all locale bundles.

Backward compatibility

All new fields are optional. Existing consumers without validation rules behave
exactly as before. No package version bumps required at the consumer level; a
consumer pinned to the previous SDK version can upgrade without code changes.

Tests

  • 18 new unit tests for the validation utilities (13 for evaluateValidationRule,
    5 for buildValidatorFromRules).
  • All 482 existing tests across the 4 modified packages still pass.
  • Typecheck clean across all 4 packages.

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 declarative input validation rules supporting pattern matching and length constraints with built-in client-side evaluation.
    • Integrated server-side field-level error handling and display across sign-in, sign-up, and account recovery flows.
    • Added multilingual validation error messages across 9 supported languages.
    • Exposed field validation errors to render-prop consumers for flexible custom UI implementations.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

This PR introduces end-to-end declarative input validation for embedded flows. It adds ValidationRule and FieldError types to flow models, provides client-side rule evaluation utilities with i18n fallback messages, extends nine language translation bundles, and wires validation error handling into five React flow components and render-props.

Changes

Input validation flow

Layer / File(s) Summary
Validation model and data contracts
packages/javascript/src/models/v2/embedded-flow-v2.ts
EmbeddedFlowComponent gains optional validation?: ValidationRule[], and EmbeddedFlowResponseData includes optional fieldErrors?: FieldError[]. New exported types define ValidationRuleType ('regex'|'minLength'|'maxLength'), ValidationRule (type, value, optional message), and FieldError (identifier, message).
Client-side validation utilities and tests
packages/javascript/src/utils/v2/evaluateValidationRule.ts, packages/javascript/src/utils/v2/buildValidatorFromRules.ts, packages/javascript/src/utils/__tests__/*
evaluateValidationRule evaluates a single rule against a value, returning the rule's message or a fallback key, with lenient handling of invalid patterns and non-conforming types. buildValidatorFromRules composes rule arrays into a form-field validator returning the first failing message or null. Tests verify pass/fail logic, message fallback behavior, and forward compatibility.
i18n validation messages
packages/i18n/src/models/i18n.ts, packages/i18n/src/translations/*.ts
I18nTranslations interface adds three new keys: validation.pattern.invalid, validation.minLength.invalid, validation.maxLength.invalid. All nine language bundles (en-US, fr-FR, hi-IN, ja-JP, pt-BR, pt-PT, si-LK, ta-IN, te-IN) include matching translations.
Public API exports
packages/javascript/src/index.ts, .changeset/input-validation-flow-inputs.md
ValidationRuleV2, ValidationRuleTypeV2, FieldErrorV2, evaluateValidationRule, DEFAULT_VALIDATION_MESSAGE_KEYS, and buildValidatorFromRules are exported from the main SDK entry point. Changeset documents minor version bumps and new exports across @asgardeo/javascript, @asgardeo/react, @asgardeo/i18n.
React component validation wiring
packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx, packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx, packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx, packages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsx, packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx, packages/react/src/components/presentation/auth/Recovery/v2/BaseRecovery.tsx
Five base flow components evaluate component.validation rules during form field extraction and apply i18n translation to rule failure messages. All components use useEffect to project server-provided fieldErrors from currentFlow.data into form state, marking affected fields touched. SignIn introduces serverFieldErrors state, exposes fieldErrors via render-props, and adds serverFieldErrors prop to BaseSignIn for the default UI path.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • Support Input Validation for Flow Inputs thunder-id/thunderid#2619: Directly related; both introduce the same ValidationRule discriminated type (regex/minLength/maxLength), FieldError and data.fieldErrors additions to flow response models, component-level validation via EmbeddedFlowComponent.validation, and default i18n fallback keys.

Possibly related PRs

  • asgardeo/javascript#493: Extends recovery flow React components (BaseRecovery) to evaluate component.validation and surface server fieldErrors, directly related to the retrieved PR that introduces new embedded recovery flow components.

Suggested labels

Type/Improvement

Suggested reviewers

  • brionmario
  • thiva-k

Poem

🐰 Validation rules hop through the fields,
Client and server, both now yield!
With i18n fallbacks, messages gleam,
Flow components wired, a validation dream.
Errors caught early, touched with care,
Input so clean, validation is fair! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the primary change: adding input validation support to flow components, which matches the main objective of introducing declarative validation rules.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description follows the template structure with all required sections completed: Purpose, Related Issues, Related PRs, Checklist, and Security checks. The description is comprehensive and well-documented.

✏️ 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.

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: 3

🧹 Nitpick comments (3)
packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx (1)

64-70: 💤 Low value

JSDoc comment could clarify the server error lifecycle.

The inline documentation states that fieldErrors reflects both client-side and server-side errors, but it doesn't explain when server errors are cleared. Consider adding a note that server errors persist until the next submission or until the field is edited (depending on implementation).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx`
around lines 64 - 70, Update the JSDoc for the fieldErrors block in BaseSignIn
to explicitly document the server error lifecycle: state that fieldErrors
combines client-side rule evaluation and server-side failures (from
data.fieldErrors), that full FieldError[] is on the raw response and mirrored to
the serverFieldErrors prop, and add a sentence stating when server-side errors
are cleared (e.g., they persist until the next form submission or until the user
edits the associated field depending on the existing implementation of
BaseSignIn's submit/field-change handlers). Reference the fieldErrors identifier
and the serverFieldErrors prop so readers can locate the related state/prop
handling.
packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx (1)

435-452: 💤 Low value

Complex nested validation logic can be simplified.

Lines 435-452 have nested conditionals: if required & empty → error, else → email validation, then rule validation (if no error yet). The if (value && !errors[comp.ref]) check at line 443 suggests defensive coding, but errors[comp.ref] won't be set yet in this context since we're building the errors object.

Consider restructuring for clarity:

♻️ Optional refactor for clarity
            const value: any = formValues[comp.ref];
            if (comp.required && (!value || value.trim() === '')) {
              errors[comp.ref] = `${comp.label || comp.ref} is required`;
+             return; // Skip further validation for empty required fields
            } else {
              // Email validation
              if (comp.type === 'EMAIL_INPUT' && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
                errors[comp.ref] = 'Please enter a valid email address';
+               return; // Skip rule validation if email format is invalid
              }
              // Evaluate declarative validation rules from meta.components[].validation.
-             if (value && !errors[comp.ref]) {
+             if (value) {
                const ruleValidator = buildValidatorFromRules(comp.validation);
                if (ruleValidator) {
                  const message = ruleValidator(value);
                  if (message) {
                    errors[comp.ref] = message;
                  }
                }
              }
            }

This makes the validation precedence explicit: required → email format → rules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx`
around lines 435 - 452, The validation block in BaseInviteUser.tsx is overly
nested and uses a redundant guard (checking value && !errors[comp.ref]) which is
unnecessary because errors is being built here; refactor the logic in the loop
that inspects each comp so it enforces precedence explicitly: first check
required and set errors[comp.ref] if missing, then if comp.type ===
'EMAIL_INPUT' validate the email format and set errors[comp.ref] if invalid, and
finally if value exists and no error yet call
buildValidatorFromRules(comp.validation) and apply the returned ruleValidator to
set errors[comp.ref] if it returns a message; keep references to comp.ref,
comp.label, comp.type, buildValidatorFromRules and the errors object to locate
and update the code.
packages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsx (1)

491-502: 💤 Low value

Validation logic flow may allow both required and rule errors to be set.

Lines 491-502 check comp.required first, then evaluate rules. However, if comp.required && !value, the function returns early, but the else if (value) block at line 493 means rules are only evaluated when there's a value. The structure is correct, but the code would be clearer with an explicit early return after setting the required error.

♻️ Optional refactor for clarity
            const value: any = formValues[comp.ref];
            if (comp.required && (!value || value.trim() === '')) {
              errors[comp.ref] = t('validations.required.field.error');
-           } else if (value) {
+             return; // Skip rule evaluation for empty required fields
+           }
+           if (value) {
              // Evaluate declarative validation rules from meta.components[].validation.
              const ruleValidator = buildValidatorFromRules(comp.validation);
              if (ruleValidator) {
                const message = ruleValidator(value);
                if (message) {
                  errors[comp.ref] = t(message);
                }
              }
            }

This makes it explicit that we don't evaluate rules for empty required fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsx`
around lines 491 - 502, The required-field branch may be ambiguous; after
setting errors[comp.ref] when comp.required && (!value || value.trim() === ''),
short-circuit so we don't run buildValidatorFromRules/ ruleValidator for empty
values — add an explicit early exit (e.g., continue the loop or return from the
enclosing function) immediately after setting the required error so rules are
not evaluated for empty required fields and t(...) is only called once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsx`:
- Around line 293-308: The useEffect that reads server FieldError entries from
currentFlow (FieldError) currently merges them into state via setFormErrors(prev
=> ({...prev,...errors})) which leaves stale server errors; change setFormErrors
and setTouchedFields to replace the server error state when responseFieldErrors
exist (e.g., call setFormErrors(errors) and setTouchedFields(touched)) so
server-provided errors do not persist across responses; if you need to preserve
client-side validation errors, track them separately or merge only client-side
errors back into state after replacing server errors.

In
`@packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx`:
- Around line 284-299: The useEffect that processes server FieldError objects
(referencing currentFlow, FieldError, and useEffect) merges the new server
errors into existing form state via setFormErrors((prev) => ({...prev,
...errors})) and similarly for setTouchedFields, which leaves stale errors;
change the update to replace the server-provided error state instead of merging
(e.g., setFormErrors to set only the new errors and setTouchedFields to set only
the new touched map), ensuring you still preserve non-server local form state
elsewhere if needed.

In `@packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx`:
- Around line 385-397: The effect that applies serverFieldErrors should also
clear injected errors when serverFieldErrors becomes null/empty: update the
useEffect around serverFieldErrors to, when serverFieldErrors is falsy or length
=== 0, call setFormErrors({}) and clear touched flags for any previously-set
server error fields by calling setFormTouched(key, false) for each key (you can
read previous keys from current formErrors or track them in a ref) so stale
messages/touched state are removed; keep the existing logic for populating
errors when serverFieldErrors exists and adjust the effect dependencies to
include formErrors or the ref used to track previous keys.

---

Nitpick comments:
In
`@packages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsx`:
- Around line 491-502: The required-field branch may be ambiguous; after setting
errors[comp.ref] when comp.required && (!value || value.trim() === ''),
short-circuit so we don't run buildValidatorFromRules/ ruleValidator for empty
values — add an explicit early exit (e.g., continue the loop or return from the
enclosing function) immediately after setting the required error so rules are
not evaluated for empty required fields and t(...) is only called once.

In
`@packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx`:
- Around line 435-452: The validation block in BaseInviteUser.tsx is overly
nested and uses a redundant guard (checking value && !errors[comp.ref]) which is
unnecessary because errors is being built here; refactor the logic in the loop
that inspects each comp so it enforces precedence explicitly: first check
required and set errors[comp.ref] if missing, then if comp.type ===
'EMAIL_INPUT' validate the email format and set errors[comp.ref] if invalid, and
finally if value exists and no error yet call
buildValidatorFromRules(comp.validation) and apply the returned ruleValidator to
set errors[comp.ref] if it returns a message; keep references to comp.ref,
comp.label, comp.type, buildValidatorFromRules and the errors object to locate
and update the code.

In `@packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx`:
- Around line 64-70: Update the JSDoc for the fieldErrors block in BaseSignIn to
explicitly document the server error lifecycle: state that fieldErrors combines
client-side rule evaluation and server-side failures (from data.fieldErrors),
that full FieldError[] is on the raw response and mirrored to the
serverFieldErrors prop, and add a sentence stating when server-side errors are
cleared (e.g., they persist until the next form submission or until the user
edits the associated field depending on the existing implementation of
BaseSignIn's submit/field-change handlers). Reference the fieldErrors identifier
and the serverFieldErrors prop so readers can locate the related state/prop
handling.
🪄 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: 021eeeb6-b26e-4ba3-9e3a-4e980b34fc2e

📥 Commits

Reviewing files that changed from the base of the PR and between 22ee26d and 207a59f.

📒 Files selected for processing (23)
  • .changeset/input-validation-flow-inputs.md
  • packages/i18n/src/models/i18n.ts
  • packages/i18n/src/translations/en-US.ts
  • packages/i18n/src/translations/fr-FR.ts
  • packages/i18n/src/translations/hi-IN.ts
  • packages/i18n/src/translations/ja-JP.ts
  • packages/i18n/src/translations/pt-BR.ts
  • packages/i18n/src/translations/pt-PT.ts
  • packages/i18n/src/translations/si-LK.ts
  • packages/i18n/src/translations/ta-IN.ts
  • packages/i18n/src/translations/te-IN.ts
  • packages/javascript/src/index.ts
  • packages/javascript/src/models/v2/embedded-flow-v2.ts
  • packages/javascript/src/utils/__tests__/buildValidatorFromRules.test.ts
  • packages/javascript/src/utils/__tests__/evaluateValidationRule.test.ts
  • packages/javascript/src/utils/v2/buildValidatorFromRules.ts
  • packages/javascript/src/utils/v2/evaluateValidationRule.ts
  • packages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsx
  • packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx
  • packages/react/src/components/presentation/auth/Recovery/v2/BaseRecovery.tsx
  • packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx
  • packages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsx
  • packages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx

Comment on lines +293 to +308
useEffect(() => {
const responseFieldErrors: FieldError[] | undefined = (currentFlow?.data as any)?.fieldErrors;
if (!responseFieldErrors || responseFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
const touched: Record<string, boolean> = {};
for (const fe of responseFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
touched[fe.identifier] = true;
}
}
setFormErrors((prev: Record<string, string>) => ({...prev, ...errors}));
setTouchedFields((prev: Record<string, boolean>) => ({...prev, ...touched}));
}, [currentFlow]);
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 | 🟡 Minor | ⚡ Quick win

Server errors are merged instead of replaced, potentially leaving stale errors.

Line 306 merges new server errors into the existing formErrors state using spread: {...prev, ...errors}. This means if a field had a server error in response N but is no longer present in response N+1's fieldErrors, the old error will persist in state.

Consider replacing instead of merging when server errors are present:

🔄 Proposed fix to replace server errors
    }
-   setFormErrors((prev: Record<string, string>) => ({...prev, ...errors}));
+   // Replace server errors entirely; client-side errors are re-computed on validation
+   setFormErrors(errors);
    setTouchedFields((prev: Record<string, boolean>) => ({...prev, ...touched}));
  }, [currentFlow]);

Note: If you need to preserve client-side validation errors alongside server errors, you'll need to track their sources separately or ensure client validation re-runs after server errors arrive.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsx`
around lines 293 - 308, The useEffect that reads server FieldError entries from
currentFlow (FieldError) currently merges them into state via setFormErrors(prev
=> ({...prev,...errors})) which leaves stale server errors; change setFormErrors
and setTouchedFields to replace the server error state when responseFieldErrors
exist (e.g., call setFormErrors(errors) and setTouchedFields(touched)) so
server-provided errors do not persist across responses; if you need to preserve
client-side validation errors, track them separately or merge only client-side
errors back into state after replacing server errors.

Comment on lines +284 to +299
useEffect(() => {
const responseFieldErrors: FieldError[] | undefined = (currentFlow?.data as any)?.fieldErrors;
if (!responseFieldErrors || responseFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
const touched: Record<string, boolean> = {};
for (const fe of responseFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
touched[fe.identifier] = true;
}
}
setFormErrors((prev: Record<string, string>) => ({...prev, ...errors}));
setTouchedFields((prev: Record<string, boolean>) => ({...prev, ...touched}));
}, [currentFlow]);
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 | 🟡 Minor | ⚡ Quick win

Server errors are merged instead of replaced, same issue as BaseAcceptInvite.

Line 297 uses {...prev, ...errors} to merge server errors into existing form errors. This can leave stale errors from a previous server response if a field is no longer in the new fieldErrors array.

🔄 Proposed fix to replace server errors
    }
-   setFormErrors((prev: Record<string, string>) => ({...prev, ...errors}));
+   setFormErrors(errors);
    setTouchedFields((prev: Record<string, boolean>) => ({...prev, ...touched}));
  }, [currentFlow]);
📝 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
useEffect(() => {
const responseFieldErrors: FieldError[] | undefined = (currentFlow?.data as any)?.fieldErrors;
if (!responseFieldErrors || responseFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
const touched: Record<string, boolean> = {};
for (const fe of responseFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
touched[fe.identifier] = true;
}
}
setFormErrors((prev: Record<string, string>) => ({...prev, ...errors}));
setTouchedFields((prev: Record<string, boolean>) => ({...prev, ...touched}));
}, [currentFlow]);
useEffect(() => {
const responseFieldErrors: FieldError[] | undefined = (currentFlow?.data as any)?.fieldErrors;
if (!responseFieldErrors || responseFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
const touched: Record<string, boolean> = {};
for (const fe of responseFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
touched[fe.identifier] = true;
}
}
setFormErrors(errors);
setTouchedFields((prev: Record<string, boolean>) => ({...prev, ...touched}));
}, [currentFlow]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsx`
around lines 284 - 299, The useEffect that processes server FieldError objects
(referencing currentFlow, FieldError, and useEffect) merges the new server
errors into existing form state via setFormErrors((prev) => ({...prev,
...errors})) and similarly for setTouchedFields, which leaves stale errors;
change the update to replace the server-provided error state instead of merging
(e.g., setFormErrors to set only the new errors and setTouchedFields to set only
the new touched map), ensuring you still preserve non-server local form state
elsewhere if needed.

Comment on lines +385 to +397
useEffect(() => {
if (!serverFieldErrors || serverFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
for (const fe of serverFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
}
}
setFormErrors(errors);
Object.keys(errors).forEach((field: string) => setFormTouched(field, true));
}, [serverFieldErrors, setFormErrors, setFormTouched]);
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 | 🟡 Minor | ⚡ Quick win

Potential stale error state when serverFieldErrors is cleared.

The useEffect only runs when serverFieldErrors changes, but it doesn't clear the form errors when serverFieldErrors becomes null or empty. If the server returns validation errors in response N, then the user corrects the input and the server returns no errors in response N+1 (serverFieldErrors: null), the old errors will remain in formErrors until the user triggers client-side validation by blurring a field.

Consider clearing server-injected errors when serverFieldErrors becomes null or empty:

🔄 Proposed fix to clear stale server errors
  useEffect(() => {
-   if (!serverFieldErrors || serverFieldErrors.length === 0) {
-     return;
-   }
+   if (!serverFieldErrors || serverFieldErrors.length === 0) {
+     // Clear any previously-set server errors when the server no longer returns them
+     setFormErrors({});
+     return;
+   }
    const errors: Record<string, string> = {};
    for (const fe of serverFieldErrors) {
      if (!(fe.identifier in errors)) {
        errors[fe.identifier] = fe.message;
      }
    }
    setFormErrors(errors);
    Object.keys(errors).forEach((field: string) => setFormTouched(field, true));
  }, [serverFieldErrors, setFormErrors, setFormTouched]);
📝 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
useEffect(() => {
if (!serverFieldErrors || serverFieldErrors.length === 0) {
return;
}
const errors: Record<string, string> = {};
for (const fe of serverFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
}
}
setFormErrors(errors);
Object.keys(errors).forEach((field: string) => setFormTouched(field, true));
}, [serverFieldErrors, setFormErrors, setFormTouched]);
useEffect(() => {
if (!serverFieldErrors || serverFieldErrors.length === 0) {
// Clear any previously-set server errors when the server no longer returns them
setFormErrors({});
return;
}
const errors: Record<string, string> = {};
for (const fe of serverFieldErrors) {
if (!(fe.identifier in errors)) {
errors[fe.identifier] = fe.message;
}
}
setFormErrors(errors);
Object.keys(errors).forEach((field: string) => setFormTouched(field, true));
}, [serverFieldErrors, setFormErrors, setFormTouched]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx`
around lines 385 - 397, The effect that applies serverFieldErrors should also
clear injected errors when serverFieldErrors becomes null/empty: update the
useEffect around serverFieldErrors to, when serverFieldErrors is falsy or length
=== 0, call setFormErrors({}) and clear touched flags for any previously-set
server error fields by calling setFormTouched(key, false) for each key (you can
read previous keys from current formErrors or track them in a ref) so stale
messages/touched state are removed; keep the existing logic for populating
errors when serverFieldErrors exists and adjust the effect dependencies to
include formErrors or the ref used to track previous keys.

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.

1 participant