Add input validation support to flow components#518
Conversation
Signed-off-by: Sajid Mannikeri <[email protected]>
📝 WalkthroughWalkthroughThis 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. ChangesInput validation flow
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
packages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsx (1)
64-70: 💤 Low valueJSDoc comment could clarify the server error lifecycle.
The inline documentation states that
fieldErrorsreflects 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 valueComplex 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, buterrors[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 valueValidation logic flow may allow both required and rule errors to be set.
Lines 491-502 check
comp.requiredfirst, then evaluate rules. However, ifcomp.required && !value, the function returns early, but theelse 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
📒 Files selected for processing (23)
.changeset/input-validation-flow-inputs.mdpackages/i18n/src/models/i18n.tspackages/i18n/src/translations/en-US.tspackages/i18n/src/translations/fr-FR.tspackages/i18n/src/translations/hi-IN.tspackages/i18n/src/translations/ja-JP.tspackages/i18n/src/translations/pt-BR.tspackages/i18n/src/translations/pt-PT.tspackages/i18n/src/translations/si-LK.tspackages/i18n/src/translations/ta-IN.tspackages/i18n/src/translations/te-IN.tspackages/javascript/src/index.tspackages/javascript/src/models/v2/embedded-flow-v2.tspackages/javascript/src/utils/__tests__/buildValidatorFromRules.test.tspackages/javascript/src/utils/__tests__/evaluateValidationRule.test.tspackages/javascript/src/utils/v2/buildValidatorFromRules.tspackages/javascript/src/utils/v2/evaluateValidationRule.tspackages/react/src/components/presentation/auth/AcceptInvite/v2/BaseAcceptInvite.tsxpackages/react/src/components/presentation/auth/InviteUser/v2/BaseInviteUser.tsxpackages/react/src/components/presentation/auth/Recovery/v2/BaseRecovery.tsxpackages/react/src/components/presentation/auth/SignIn/v2/BaseSignIn.tsxpackages/react/src/components/presentation/auth/SignIn/v2/SignIn.tsxpackages/react/src/components/presentation/auth/SignUp/v2/BaseSignUp.tsx
| 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]); |
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
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.
| 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.
| 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]); |
There was a problem hiding this comment.
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.
| 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.
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
validationrules in the flow definitionand the server returns
data.fieldErrorswhen those rules fail. This PR makesthat feature usable from
@asgardeo/reactconsumers — flow authors put rules oninput 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:
@asgardeo/javascript— new framework-agnostic primitives:ValidationRule,ValidationRuleType,FieldErrortypes.evaluateValidationRuleandbuildValidatorFromRulesutilities.EmbeddedFlowComponent.validationandEmbeddedFlowResponseData.fieldErrorsextensions on the v2 flow models.@asgardeo/react— wires the primitives into the auth components:BaseSignIn,BaseSignUp,BaseAcceptInvite,BaseInviteUser,BaseRecovery— client-side rule evaluation viauseForm, server-sidefieldErrorsprojection viauseEffect.SignInRenderProps.fieldErrors— exposes server-side validation errors to render-prop consumers (custom UI flows).@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
validationrules behaveexactly 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
evaluateValidationRule,5 for
buildValidatorFromRules).Related Issues
Related PRs
Checklist
Security checks
Summary by CodeRabbit