Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/javascript/src/utils/getBaseLanguage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
* Copyright 2026 The ThunderID Authors
* SPDX-License-Identifier: Apache-2.0
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand Down
3 changes: 2 additions & 1 deletion packages/javascript/src/utils/normalizeLocaleTag.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
* Copyright 2026 The ThunderID Authors
* SPDX-License-Identifier: Apache-2.0
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,20 +189,21 @@ const BaseRecoveryContent: FC<BaseRecoveryProps> = ({
if (component.required && (!value || value.trim() === '')) {
return t('validations.required.field.error');
}
if (
(component.type === EmbeddedFlowComponentType.EmailInput || component.variant === 'EMAIL') &&
value &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
) {
return t('field.email.invalid');
}
// Evaluate declarative validation rules from meta.components[].validation.
if (ruleValidator && value) {
const ruleMessage = ruleValidator(value);
if (ruleMessage) {
return t(ruleMessage);
}
}
// Add email validation if it's an email field
if (
(component.type === EmbeddedFlowComponentType.EmailInput || component.variant === 'EMAIL') &&
value &&
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
) {
return t('field.email.invalid');
}
return null;
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
const {meta, vendor} = useThunderID();
const {theme} = useTheme();
const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext);
const {t} = useTranslation();
const {t, currentLanguage} = useTranslation();
const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages} = useFlow();
const styles: any = useStyles(theme, theme.vars.colors.text.primary);

Expand Down Expand Up @@ -319,6 +319,13 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
if (component.required && (!value || value.trim() === '')) {
return t('validations.required.field.error');
}
// Run declarative rules from meta.components[].validation.
if (ruleValidator && value) {
const ruleMessage = ruleValidator(value);
if (ruleMessage) {
return t(ruleMessage);
}
}
// Add email validation if it's an email field
if (

@ThaminduDilshan ThaminduDilshan Aug 18, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will get rid of the frontend validations we have by default for the inputs with type EMAIL_INPUT.
Can't we keep this as a fallback validation for EMAIL_INPUT type inputs when the flow validation rules are not defined?

I prefer not to remove this rule at this point as we still haven't fully incorporated flow validation rules to the thunderid console.

@brionmario WDYT?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1
Lets keep the default validation.
@coderabbitai Create an issue in https://github.com/thunder-id/thunderid to centralize and maintain a default set of validations in the @thunderid/browser or @thunderid/javascript that can be reused.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo="thunder-id/thunderid"
body="$(cat <<'EOF'
## Summary

Centralize the SDK default client-side validation rules in a reusable API from `@thunderid/browser` or `@thunderid/javascript`.

## Rationale

The React authentication forms must retain default validation for inputs such as `EMAIL_INPUT` while ThunderID Console flow validation rules are not yet fully available. The current validation logic is duplicated in UI components. Centralizing the rules will provide consistent behavior across SDKs and avoid hardcoded validation logic in individual flows.

## Affected areas

- `@thunderid/browser`
- `@thunderid/javascript`
- `@thunderid/react` authentication flows, including sign-in, sign-up, and recovery
- Flow component types such as `EMAIL_INPUT`

## Required changes

1. Define and export a maintained default-validation API from `@thunderid/browser` or `@thunderid/javascript`.
2. Include an email-format fallback for `EMAIL_INPUT` when the flow component has no applicable declarative validation rule.
3. Define precedence between declarative flow rules and default validation rules.
4. Update React authentication forms to consume the shared API instead of defining email validation inline.
5. Ensure validation messages use the existing translation mechanism and do not expose raw translation keys.

## Acceptance criteria

- A consumer can apply the shared default validation based on an embedded flow component type.
- `EMAIL_INPUT` has client-side email-format validation when no applicable flow validation rule is present.
- Declarative flow validation behavior remains supported and has documented precedence over fallback validation.
- Sign-in, sign-up, and recovery use the shared validation API.
- Validation errors are translated through the SDK translation function.
- Unit tests cover valid and invalid email values, absent flow rules, and declarative-rule precedence.

## Backlinks

- Pull request: https://github.com/thunder-id/javascript-sdks/pull/73
- Review discussion: https://github.com/thunder-id/javascript-sdks/pull/73#discussion_r3803557915
- Requested by: `@brionmario`
EOF
)"

gh issue create \
  --repo "$repo" \
  --title "Centralize reusable default client-side validation rules for SDK flow inputs" \
  --body "$body" \
  --assignee "brionmario"

Length of output: 2509


@brionmario, created thunder-id/thunderid#5069: Centralize reusable default client-side validation rules for SDK flow inputs.

The issue is assigned to you and links to this pull request discussion.


✏️ Learnings added
Learnt from: brionmario
Repo: thunder-id/javascript-sdks PR: 73
File: packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx:323-323
Timestamp: 2026-08-18T11:26:16.803Z
Learning: ThunderID Console has not yet fully incorporated flow validation rules. React authentication forms must retain default client-side validation, including `EMAIL_INPUT` fallback validation, until declarative flow rules are available.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, shall we have this if condition check for EMAIL_TYPE after the ruleValidator check? @brionmario @ThaminduDilshan . That way the check will still exist for EMAIL_TYPE and for the cases where flow validation rule is incorporated, that check will happen first and proper validation message will be rendered.

(component.type === 'EMAIL_INPUT' || component.variant === 'EMAIL') &&
Expand All @@ -327,13 +334,6 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
) {
return t('field.email.invalid');
}
// Run declarative rules from meta.components[].validation.
if (ruleValidator && value) {
const ruleMessage = ruleValidator(value);
if (ruleMessage) {
return t(ruleMessage);
}
}

return null;
},
Expand Down Expand Up @@ -378,6 +378,7 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
validateForm,
touchAllFields,
reset: resetForm,
revalidateTouchedFields,
} = form;

// Project server-side fieldErrors into form state. `setTouchedFields` is used instead
Expand All @@ -400,6 +401,12 @@ const BaseSignInContent: FC<BaseSignInProps> = ({
setFormErrors(errors);
}, [serverFieldErrors, setFormErrors, setTouchedFields, clearFormErrors]);

// Re-translate displayed validation errors when the UI language changes.
// revalidateTouchedFields is stable, so this effect fires only on language change.
useEffect(() => {
revalidateTouchedFields();
}, [currentLanguage, revalidateTouchedFields]);

/**
* Handle input value changes.
* Only updates the value without marking as touched.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,13 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
if (component.required && (!value || value.trim() === '')) {
return t('validations.required.field.error');
}
// Evaluate declarative validation rules from meta.components[].validation.
if (ruleValidator && value) {
const ruleMessage = ruleValidator(value);
if (ruleMessage) {
return t(ruleMessage);
}
}
// Add email validation if it's an email field
if (
(component.type === EmbeddedFlowComponentType.EmailInput || component.variant === 'EMAIL') &&
Expand All @@ -435,13 +442,6 @@ const BaseSignUpContent: FC<BaseSignUpProps> = ({
) {
return t('field.email.invalid');
}
// Evaluate declarative validation rules from meta.components[].validation.
if (ruleValidator && value) {
const ruleMessage = ruleValidator(value);
if (ruleMessage) {
return t(ruleMessage);
}
}

return null;
},
Expand Down
52 changes: 51 additions & 1 deletion packages/react/src/hooks/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2025 The ThunderID Authors
// SPDX-License-Identifier: Apache-2.0

import {useState, useCallback, FormEvent} from 'react';
import {useState, useCallback, useRef, FormEvent} from 'react';

/**
* Generic form field configuration
Expand Down Expand Up @@ -163,6 +163,11 @@ export interface UseFormReturn<T extends Record<string, string>> {
* Validate all fields
*/
validateForm: () => ValidationResult;
/**
* Re-run validation for all touched fields that have a client-side config,
* refreshing stored error strings to the current language.
*/
revalidateTouchedFields: () => void;
/**
* Current form values
*/
Expand Down Expand Up @@ -229,6 +234,9 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
const [errors, setFormErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>);
const [isSubmitted, setIsSubmitted] = useState(false);

// Ref to track which fields have client-side validation rules. Errors injected via
// serErrors are not added here, so revalidateTouchedFields preserves server-side errors.
const clientErrorFieldRef = useRef(new Set<keyof T>());
// Get field configuration by name
const getFieldConfig: (name: keyof T) => FormField | undefined = useCallback(
(name: keyof T): FormField | undefined => fields.find((field: FormField) => field.name === name),
Expand All @@ -246,6 +254,40 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
[values, getFieldConfig, requiredMessage],
);

// "Latest value" ref so revalidateTouchedFields can read fresh validators without
// appearing in any effect dep array (avoids exhaustive-deps violations at call sites).
const validateFieldRef = useRef(validateField);
validateFieldRef.current = validateField;

// Re-translate all, client-validated error strings on language change.
// Stable identity ([] deps): uses only refs and the stable state setter.
const revalidateTouchedFields: () => void = useCallback((): void => {
setFormErrors((prevErrors: Record<keyof T, string>) => {
if (Object.keys(prevErrors).length === 0) return prevErrors;

const newErrors: Record<keyof T, string> = {...prevErrors};
let changed = false;

(Object.keys(prevErrors) as Array<keyof T>).forEach((name: keyof T) => {
// Skip errors that were not produced by client-side validation - preserve server messages.
if (!clientErrorFieldRef.current.has(name)) return;

const freshError: string | null = validateFieldRef.current(name);
if (freshError === prevErrors[name]) return;

changed = true;
if (freshError) {
newErrors[name] = freshError;
} else {
delete newErrors[name];
clientErrorFieldRef.current.delete(name);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

return changed ? newErrors : prevErrors;
});
}, []);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Validate the entire form
const validateForm: () => ValidationResult = useCallback((): ValidationResult => {
const newErrors: Record<keyof T, string> = {} as Record<keyof T, string>;
Expand Down Expand Up @@ -307,8 +349,10 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
const newErrors: Record<keyof T, string> = {...prev};
if (error) {
newErrors[name] = error;
clientErrorFieldRef.current.add(name);
} else {
delete newErrors[name];
clientErrorFieldRef.current.delete(name);
}
return newErrors;
});
Expand Down Expand Up @@ -339,8 +383,10 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
const newErrors: Record<keyof T, string> = {...prev};
if (error) {
newErrors[name] = error;
clientErrorFieldRef.current.add(name);
} else {
delete newErrors[name];
clientErrorFieldRef.current.delete(name);
}
return newErrors;
});
Expand Down Expand Up @@ -375,6 +421,7 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
// Validate all fields
const validation: ValidationResult = validateForm();
setFormErrors(validation.errors as Record<keyof T, string>);
clientErrorFieldRef.current = new Set(Object.keys(validation.errors) as Array<keyof T>);
}, [fields, validateForm]);

// Set a field error
Expand All @@ -399,6 +446,7 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
// Clear all errors
const clearErrors: () => void = useCallback((): void => {
setFormErrors({} as Record<keyof T, string>);
clientErrorFieldRef.current.clear();
}, []);

// Reset form to initial state
Expand All @@ -407,6 +455,7 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
setFormTouched({} as Record<keyof T, boolean>);
setFormErrors({} as Record<keyof T, string>);
setIsSubmitted(false);
clientErrorFieldRef.current.clear();
}, [initialValues]);

// Handle form submission
Expand Down Expand Up @@ -455,6 +504,7 @@ export const useForm = <T extends Record<string, string>>(config: UseFormConfig<
isSubmitted,
isValid,
reset,
revalidateTouchedFields,
setError,
setErrors,
setTouched,
Expand Down
Loading