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
19 changes: 2 additions & 17 deletions packages/javascript/src/utils/getBaseLanguage.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Copyright 2026 The ThunderID Authors
// SPDX-License-Identifier: Apache-2.0

/**
* Resolves a BCP 47 locale tag to its base (primary) language subtag, so that
Expand Down
19 changes: 2 additions & 17 deletions packages/javascript/src/utils/normalizeLocaleTag.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
Comment thread
SajidMannikeri17 marked this conversation as resolved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Copyright 2026 The ThunderID Authors
// SPDX-License-Identifier: Apache-2.0

/**
* Lowercases the language subtag and uppercases a 2-letter region subtag (the common
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 (
Comment thread
SajidMannikeri17 marked this conversation as resolved.
(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