Implementation: Issue #510 - Add Granular Form Field Validation Messages for Deposit and Withdraw Forms
The deposit and withdraw forms in YieldVault-RWA now have granular per-field validation feedback, providing users with actionable guidance when they submit invalid inputs. This implementation adds schema-driven validation rules, touched-state message display, submit guards, and server-error mapping to both forms so that users receive precise, per-field error messages before submission and clear feedback when the server rejects a request.
Library: Custom useForm hook with built-in validation support (not React Hook Form or Formik)
Integration API: ValidationSchema type with custom validation functions via custom property
Mode: onBlur touched-state tracking with real-time re-validation after first submit
Library: Custom lightweight validation system in frontend/src/forms/validate.ts
- ✅ Zod is available as a dependency but is used only for API schemas, not forms
- ✅ Custom validation system is lightweight and requires no additional dependencies
- ✅ Follows existing form validation patterns in the codebase
Pattern: Existing FormField component with error prop
- ✅ Error messages render below the input with
.form-errorCSS class - ✅ Component handles
aria-describedbyfor accessibility - ✅ Helper text displays when no error exists
- ✅ No new components created
- Amount field (required)
- Must be present: "Amount is required."
- Must be valid number: "Enter a valid number."
- Must be greater than 0: "Amount must be greater than 0."
- Must be ≥ MIN_DEPOSIT (1 USDC): "Minimum deposit is 1.00 USDC."
- Must not exceed wallet balance: "Deposit amount cannot exceed your available USDC balance of [X]."
- Vault not at capacity: "Deposits are temporarily disabled because the vault is at capacity."
- Sufficient XLM for network fees: "Insufficient XLM balance for network fees. You need [X] XLM."
- Amount field (required)
- Must be present: "Amount is required."
- Must be valid number: "Enter a valid number."
- Must be greater than 0: "Amount must be greater than 0."
- Must not exceed vault balance: "Withdrawal amount cannot exceed your available vault balance of [X]."
Expected Server Response Format:
{
code?: string;
message: string;
details?: {
field?: string;
[key: string]: unknown;
};
}Mapping Strategy:
- Parse
details.fieldto identify field-level errors - Map
details.field→ form field name - Sanitize
messageto remove stack traces, database constraint names, and internal field references - Set field error via
setFieldError('fieldName', sanitizedMessage) - For non-field errors, display as general error toast
Mode: onBlur - errors shown only after field has been touched (blurred)
Configuration: Already implemented in useForm hook
Real-time Update: After first submit attempt, errors update in real-time as user types
Convention: Disabled-until-valid initially, then enable/disable based on form validity Current Implementation: Button disabled when:
- No wallet connected
- Transaction processing
- Form has validation errors
- Amount field empty
- (Deposit only) Vault capacity reached
- ✅
frontend/src/forms/schemas/depositFormSchema.ts- Deposit validation schema factory - ✅
frontend/src/forms/schemas/depositFormSchema.test.ts- Deposit schema tests - ✅
frontend/src/forms/schemas/withdrawFormSchema.ts- Withdraw validation schema factory - ✅
frontend/src/forms/schemas/withdrawFormSchema.test.ts- Withdraw schema tests - ✅
frontend/src/lib/errorMappers.ts- Server error mapping and sanitization - ✅
frontend/src/lib/errorMappers.test.ts- Error mapping tests
- ✅
frontend/src/components/VaultDashboard.tsx- Integrated new schemas, improved error handling - ✅
frontend/src/forms/index.ts- Exported new schema factories
| Field | Rule | Error Message |
|---|---|---|
| Amount (Deposit) | Required | "Amount is required." |
| Amount (Deposit) | Valid number | "Enter a valid number." |
| Amount (Deposit) | > 0 | "Amount must be greater than 0." |
| Amount (Deposit) | ≥ 1 USDC | "Minimum deposit is 1.00 USDC." |
| Amount (Deposit) | ≤ wallet balance | "Deposit amount cannot exceed your available USDC balance of [X]." |
| Amount (Deposit) | Vault not full | "Deposits are temporarily disabled because the vault is at capacity." |
| Amount (Deposit) | Sufficient XLM | "Insufficient XLM balance for network fees. You need [X] XLM." |
| Amount (Withdraw) | Required | "Amount is required." |
| Amount (Withdraw) | Valid number | "Enter a valid number." |
| Amount (Withdraw) | > 0 | "Amount must be greater than 0." |
| Amount (Withdraw) | ≤ vault balance | "Withdrawal amount cannot exceed your available vault balance of [X]." |
| Server Field Name | Form Field Name | Sanitization |
|---|---|---|
amount |
amount |
Remove stack traces, constraint names, internal references |
balance |
amount |
Remove stack traces, constraint names, internal references |
| Any field | Display as general error | Truncate to 200 chars, remove sensitive info |
- Mode:
onBlur(errors shown after blur event) - Re-validation: After first submit, errors update in real-time on input changes
- Initial State: No errors displayed before user interaction
- ✅ Empty amount shows "Amount is required."
- ✅ Non-numeric input shows "Enter a valid number."
- ✅ Zero amount shows "Amount must be greater than 0."
- ✅ Amount below minimum shows "Minimum deposit is 1.00 USDC."
- ✅ Amount exceeding wallet balance shows balance-specific error
- ✅ Vault at capacity shows capacity error
- ✅ Insufficient XLM shows fee error
- ✅ Valid amount shows no error
- ✅ Empty amount shows "Amount is required."
- ✅ Non-numeric input shows "Enter a valid number."
- ✅ Zero amount shows "Amount must be greater than 0."
- ✅ Amount exceeding vault balance shows vault-specific error
- ✅ Valid amount shows no error
- ✅ Field-level errors mapped correctly
- ✅ General errors mapped correctly
- ✅ Stack traces sanitized before display
- ✅ Database constraint names removed
- ✅ Long messages truncated to 200 chars
- ✅ Error-free fallback message provided
- ✅ Submit handler not invoked with validation errors
- ✅ Submit handler invoked exactly once with valid form
- ✅ Form can't be submitted twice simultaneously
- ✅ Server response with field
amounterror sets amount field error - ✅ Server response with general error sets form-level error message
- ✅ Multiple field errors handled (if applicable)
tsc --noEmit
# ✅ Zero errorsnpm run lint
# ✅ Zero errorsnpm run test:run
# ✅ All tests passnpm run build
# ✅ Completes without error- ✅ frontend-lint-test: Passes
- ✅ frontend-build: Passes
- Input Validation: Client-side schema validation is UX enhancement only - server-side validation remains unchanged
- Error Sanitization: All server error messages sanitized before display:
- Stack traces removed
- Database constraint names removed
- Internal field references removed
- Messages truncated to 200 chars
- XSS Prevention: Error messages from server validated before rendering in DOM
- No Type Coercion: Amount fields reject non-numeric input - no silent coercion
- ✅ JSDoc comments added for all validation schema factories
- ✅ Server error response shape documented in
errorMappers.ts - ✅ Validation rule descriptions in schema files
- ✅ Future maintainers know expected error response format
- ✅ Branch created from latest main:
feature/granular-form-field-validation - ✅ Only frontend changes - no backend modifications
- ✅ Coordinate with #511 and #512: All three modify forms but in different ways
- #510 (this PR): Validation and error messages
- #511: Different feature implementation
- #512: Different feature implementation
- ✅ No shared component file conflicts detected
- ✅ Closes #510
- ✅ PR description includes all required information:
- Validation rules for every field in both forms
- Server error mapping table
- Touched-state mode configured
- Test output summary
- Pipeline parity confirmation
- ✅ All CI checks passing
- ✅ Ready for code review