@thunderid/react 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/cjs/components/presentation/auth/Recovery/BaseRecovery.cjs +1 -1
  2. package/dist/cjs/components/presentation/auth/Recovery/BaseRecovery.cjs.map +1 -1
  3. package/dist/cjs/components/presentation/auth/SignIn/BaseSignIn.cjs +6 -3
  4. package/dist/cjs/components/presentation/auth/SignIn/BaseSignIn.cjs.map +1 -1
  5. package/dist/cjs/components/presentation/auth/SignUp/BaseSignUp.cjs +1 -1
  6. package/dist/cjs/components/presentation/auth/SignUp/BaseSignUp.cjs.map +1 -1
  7. package/dist/cjs/contexts/FlowMeta/FlowMetaProvider.cjs +11 -7
  8. package/dist/cjs/contexts/FlowMeta/FlowMetaProvider.cjs.map +1 -1
  9. package/dist/cjs/contexts/ThunderID/ThunderIDProvider.cjs +2 -1
  10. package/dist/cjs/contexts/ThunderID/ThunderIDProvider.cjs.map +1 -1
  11. package/dist/cjs/hooks/useForm.cjs +42 -5
  12. package/dist/cjs/hooks/useForm.cjs.map +1 -1
  13. package/dist/components/presentation/auth/Recovery/BaseRecovery.d.ts.map +1 -1
  14. package/dist/components/presentation/auth/Recovery/BaseRecovery.js +1 -1
  15. package/dist/components/presentation/auth/Recovery/BaseRecovery.js.map +1 -1
  16. package/dist/components/presentation/auth/SignIn/BaseSignIn.d.ts.map +1 -1
  17. package/dist/components/presentation/auth/SignIn/BaseSignIn.js +6 -3
  18. package/dist/components/presentation/auth/SignIn/BaseSignIn.js.map +1 -1
  19. package/dist/components/presentation/auth/SignUp/BaseSignUp.js +1 -1
  20. package/dist/components/presentation/auth/SignUp/BaseSignUp.js.map +1 -1
  21. package/dist/contexts/FlowMeta/FlowMetaProvider.d.ts +2 -0
  22. package/dist/contexts/FlowMeta/FlowMetaProvider.d.ts.map +1 -1
  23. package/dist/contexts/FlowMeta/FlowMetaProvider.js +11 -7
  24. package/dist/contexts/FlowMeta/FlowMetaProvider.js.map +1 -1
  25. package/dist/contexts/ThunderID/ThunderIDProvider.d.ts.map +1 -1
  26. package/dist/contexts/ThunderID/ThunderIDProvider.js +2 -1
  27. package/dist/contexts/ThunderID/ThunderIDProvider.js.map +1 -1
  28. package/dist/hooks/useForm.d.ts +5 -0
  29. package/dist/hooks/useForm.d.ts.map +1 -1
  30. package/dist/hooks/useForm.js +43 -6
  31. package/dist/hooks/useForm.js.map +1 -1
  32. package/dist/models/config.d.ts +2 -0
  33. package/dist/models/config.d.ts.map +1 -1
  34. package/package.json +2 -2
@@ -136,6 +136,11 @@ export interface UseFormReturn<T extends Record<string, string>> {
136
136
  * Validate all fields
137
137
  */
138
138
  validateForm: () => ValidationResult;
139
+ /**
140
+ * Re-run validation for all touched fields that have a client-side config,
141
+ * refreshing stored error strings to the current language.
142
+ */
143
+ revalidateTouchedFields: () => void;
139
144
  /**
140
145
  * Current form values
141
146
  */
@@ -1 +1 @@
1
- {"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useForm.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from "react";
1
+ import { useCallback, useRef, useState } from "react";
2
2
 
3
3
  //#region src/hooks/useForm.ts
4
4
  /**
@@ -63,6 +63,7 @@ const useForm = (config = {}) => {
63
63
  const [touched, setFormTouched] = useState({});
64
64
  const [errors, setFormErrors] = useState({});
65
65
  const [isSubmitted, setIsSubmitted] = useState(false);
66
+ const clientErrorFieldRef = useRef(/* @__PURE__ */ new Set());
66
67
  const getFieldConfig = useCallback((name) => fields.find((field) => field.name === name), [fields]);
67
68
  const validateField = useCallback((name) => {
68
69
  return computeFieldError(values[name] || "", getFieldConfig(name), requiredMessage);
@@ -71,6 +72,27 @@ const useForm = (config = {}) => {
71
72
  getFieldConfig,
72
73
  requiredMessage
73
74
  ]);
75
+ const validateFieldRef = useRef(validateField);
76
+ validateFieldRef.current = validateField;
77
+ const revalidateTouchedFields = useCallback(() => {
78
+ setFormErrors((prevErrors) => {
79
+ if (Object.keys(prevErrors).length === 0) return prevErrors;
80
+ const newErrors = { ...prevErrors };
81
+ let changed = false;
82
+ Object.keys(prevErrors).forEach((name) => {
83
+ if (!clientErrorFieldRef.current.has(name)) return;
84
+ const freshError = validateFieldRef.current(name);
85
+ if (freshError === prevErrors[name]) return;
86
+ changed = true;
87
+ if (freshError) newErrors[name] = freshError;
88
+ else {
89
+ delete newErrors[name];
90
+ clientErrorFieldRef.current.delete(name);
91
+ }
92
+ });
93
+ return changed ? newErrors : prevErrors;
94
+ });
95
+ }, []);
74
96
  const validateForm = useCallback(() => {
75
97
  const newErrors = {};
76
98
  fields.forEach((field) => {
@@ -103,8 +125,13 @@ const useForm = (config = {}) => {
103
125
  const error = computeFieldError(value, getFieldConfig(name), requiredMessage);
104
126
  setFormErrors((prev) => {
105
127
  const newErrors = { ...prev };
106
- if (error) newErrors[name] = error;
107
- else delete newErrors[name];
128
+ if (error) {
129
+ newErrors[name] = error;
130
+ clientErrorFieldRef.current.add(name);
131
+ } else {
132
+ delete newErrors[name];
133
+ clientErrorFieldRef.current.delete(name);
134
+ }
108
135
  return newErrors;
109
136
  });
110
137
  }, [
@@ -129,8 +156,13 @@ const useForm = (config = {}) => {
129
156
  const error = validateField(name);
130
157
  setFormErrors((prev) => {
131
158
  const newErrors = { ...prev };
132
- if (error) newErrors[name] = error;
133
- else delete newErrors[name];
159
+ if (error) {
160
+ newErrors[name] = error;
161
+ clientErrorFieldRef.current.add(name);
162
+ } else {
163
+ delete newErrors[name];
164
+ clientErrorFieldRef.current.delete(name);
165
+ }
134
166
  return newErrors;
135
167
  });
136
168
  }
@@ -146,7 +178,9 @@ const useForm = (config = {}) => {
146
178
  acc[field.name] = true;
147
179
  return acc;
148
180
  }, {}));
149
- setFormErrors(validateForm().errors);
181
+ const validation = validateForm();
182
+ setFormErrors(validation.errors);
183
+ clientErrorFieldRef.current = new Set(Object.keys(validation.errors));
150
184
  }, [fields, validateForm]);
151
185
  const setError = useCallback((name, error) => {
152
186
  setFormErrors((prev) => ({
@@ -162,12 +196,14 @@ const useForm = (config = {}) => {
162
196
  }, []);
163
197
  const clearErrors = useCallback(() => {
164
198
  setFormErrors({});
199
+ clientErrorFieldRef.current.clear();
165
200
  }, []);
166
201
  const reset = useCallback(() => {
167
202
  setFormValues({ ...initialValues });
168
203
  setFormTouched({});
169
204
  setFormErrors({});
170
205
  setIsSubmitted(false);
206
+ clientErrorFieldRef.current.clear();
171
207
  }, [initialValues]);
172
208
  const handleSubmit = useCallback((onSubmit) => async (e) => {
173
209
  if (e) e.preventDefault();
@@ -205,6 +241,7 @@ const useForm = (config = {}) => {
205
241
  isSubmitted,
206
242
  isValid,
207
243
  reset,
244
+ revalidateTouchedFields,
208
245
  setError,
209
246
  setErrors,
210
247
  setTouched,
@@ -1 +1 @@
1
- {"version":3,"file":"useForm.js","names":["fieldError: string | null","getFieldConfig: (name: keyof T) => FormField | undefined","validateField: (name: keyof T) => string | null","validateForm: () => ValidationResult","newErrors: Record<keyof T, string>","error: string | null","globalErrors: Record<string, string>","isValid: boolean","setValue: (name: keyof T, value: string) => void","setValues: (newValues: Partial<T>) => void","setTouched: (name: keyof T, isTouched?: boolean) => void","setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void","touchAllFields: () => void","setError: (name: keyof T, error: string) => void","setErrors: (newErrors: Partial<Record<keyof T, string>>) => void","clearErrors: () => void","reset: () => void","handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>","fieldConfig: FormField | undefined"],"sources":["../../src/hooks/useForm.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {useState, useCallback, FormEvent} from 'react';\n\n/**\n * Generic form field configuration\n */\nexport interface FormField {\n initialValue?: string;\n name: string;\n required?: boolean;\n validator?: (value: string) => string | null;\n}\n\n/**\n * Form validation result\n */\nexport interface ValidationResult {\n errors: Record<string, string>;\n isValid: boolean;\n}\n\n/**\n * Configuration for the useForm hook\n */\nexport interface UseFormConfig<T extends Record<string, string>> {\n /**\n * Form field definitions\n */\n fields?: FormField[];\n /**\n * Initial form values\n */\n initialValues?: Partial<T>;\n /**\n * Custom required field validation message\n */\n requiredMessage?: string;\n /**\n * When a field has been touched (blurred at least once), re-run validation on every\n * subsequent change so a rendered error clears the moment the value becomes valid.\n *\n * Independent of `validateOnChange`: this only affects fields that have already been\n * blurred, so users don't see errors while initially typing. Default `false` to\n * preserve prior behavior; enable per-form for a \"correct-as-you-type\" UX.\n */\n revalidateOnChangeAfterBlur?: boolean;\n /**\n * Whether to validate on blur (default: true)\n */\n validateOnBlur?: boolean;\n /**\n * Whether to validate on change (default: false). When true, every keystroke\n * triggers validation — including the first, which surfaces errors before the user\n * has had a chance to finish typing. Prefer `revalidateOnChangeAfterBlur` for the\n * common \"clear the error as the user corrects\" UX.\n */\n validateOnChange?: boolean;\n /**\n * Global form validator function\n */\n validator?: (values: T) => Record<string, string>;\n}\n\n/**\n * Pure per-field validator. Kept outside `useForm` so `setValue` can validate against\n * the value it just wrote — inline validation via the closure-based `validateField`\n * inside the hook would read stale `values` (state is committed asynchronously).\n */\nconst computeFieldError = (\n value: string,\n fieldConfig: FormField | undefined,\n requiredMessage: string,\n): string | null => {\n if (fieldConfig?.required && (!value || value.trim() === '')) {\n return requiredMessage;\n }\n if (fieldConfig?.validator) {\n const fieldError: string | null = fieldConfig.validator(value);\n if (fieldError) return fieldError;\n }\n return null;\n};\n\n/**\n * Return type for the useForm hook\n */\nexport interface UseFormReturn<T extends Record<string, string>> {\n /**\n * Clear all errors\n */\n clearErrors: () => void;\n /**\n * Current validation errors\n */\n errors: Record<keyof T, string>;\n /**\n * Get field props for easy integration with form components\n */\n getFieldProps: (name: keyof T) => {\n error: string | undefined;\n name: keyof T;\n onBlur: () => void;\n onChange: (value: string) => void;\n required: boolean;\n touched: boolean;\n value: string;\n };\n /**\n * Handle form submission\n */\n handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>;\n /**\n * Whether the form has been submitted\n */\n isSubmitted: boolean;\n /**\n * Whether the form is currently valid\n */\n isValid: boolean;\n /**\n * Reset the form to initial values\n */\n reset: () => void;\n /**\n * Set a field error\n */\n setError: (name: keyof T, error: string) => void;\n /**\n * Set multiple field errors\n */\n setErrors: (errors: Partial<Record<keyof T, string>>) => void;\n /**\n * Mark a field as touched\n */\n setTouched: (name: keyof T, touched?: boolean) => void;\n /**\n * Mark multiple fields as touched\n */\n setTouchedFields: (touched: Partial<Record<keyof T, boolean>>) => void;\n /**\n * Set a single field value\n */\n setValue: (name: keyof T, value: string) => void;\n /**\n * Set multiple field values\n */\n setValues: (values: Partial<T>) => void;\n /**\n * Mark all fields as touched\n */\n touchAllFields: () => void;\n /**\n * Fields that have been touched by the user\n */\n touched: Record<keyof T, boolean>;\n /**\n * Validate a single field\n */\n validateField: (name: keyof T) => string | null;\n /**\n * Validate all fields\n */\n validateForm: () => ValidationResult;\n /**\n * Current form values\n */\n values: T;\n}\n\n/**\n * Generic form hook that provides comprehensive form state management and validation.\n *\n * @template T - The type of form values (must extend Record<string, string>)\n * @param config - Configuration options for the form\n * @returns Form state and methods\n *\n * @example\n * ```tsx\n * interface LoginForm {\n * username: string;\n * password: string;\n * }\n *\n * const {\n * values,\n * touched,\n * errors,\n * isValid,\n * setValue,\n * handleSubmit,\n * getFieldProps\n * } = useForm<LoginForm>({\n * initialValues: { username: '', password: '' },\n * fields: [\n * { name: 'username', required: true },\n * { name: 'password', required: true }\n * ]\n * });\n *\n * const onSubmit = handleSubmit((values) => {\n * console.log('Form submitted:', values);\n * });\n *\n * return (\n * <form onSubmit={onSubmit}>\n * <input {...getFieldProps('username')} />\n * <input {...getFieldProps('password')} type=\"password\" />\n * <button type=\"submit\" disabled={!isValid}>Submit</button>\n * </form>\n * );\n * ```\n */\nexport const useForm = <T extends Record<string, string>>(config: UseFormConfig<T> = {}): UseFormReturn<T> => {\n const {\n initialValues = {} as T,\n fields = [],\n validator,\n validateOnChange = false,\n validateOnBlur = true,\n revalidateOnChangeAfterBlur = false,\n requiredMessage = 'This field is required',\n } = config;\n\n // Initialize form state\n const [values, setFormValues] = useState<T>({...initialValues} as T);\n const [touched, setFormTouched] = useState<Record<keyof T, boolean>>({} as Record<keyof T, boolean>);\n const [errors, setFormErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>);\n const [isSubmitted, setIsSubmitted] = useState(false);\n\n // Get field configuration by name\n const getFieldConfig: (name: keyof T) => FormField | undefined = useCallback(\n (name: keyof T): FormField | undefined => fields.find((field: FormField) => field.name === name),\n [fields],\n );\n\n // Validate a single field against currently-committed state. Used by the blur path\n // (where `values` is already up-to-date) and by `validateForm`.\n const validateField: (name: keyof T) => string | null = useCallback(\n (name: keyof T): string | null => {\n const value: string = values[name] || '';\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n return computeFieldError(value, fieldConfig, requiredMessage);\n },\n [values, getFieldConfig, requiredMessage],\n );\n\n // Validate the entire form\n const validateForm: () => ValidationResult = useCallback((): ValidationResult => {\n const newErrors: Record<keyof T, string> = {} as Record<keyof T, string>;\n\n // Validate individual fields\n fields.forEach((field: FormField) => {\n const error: string | null = validateField(field.name as keyof T);\n if (error) {\n newErrors[field.name as keyof T] = error;\n }\n });\n\n // Run global validator if provided\n if (validator) {\n const globalErrors: Record<string, string> = validator(values);\n Object.keys(globalErrors).forEach((key: string) => {\n if (globalErrors[key]) {\n newErrors[key as keyof T] = globalErrors[key];\n }\n });\n }\n\n return {\n errors: newErrors,\n isValid: Object.keys(newErrors).length === 0,\n };\n }, [fields, validateField, validator, values]);\n\n // Check if form is currently valid\n const isValid: boolean = Object.keys(errors).length === 0;\n\n // Set a single field value.\n //\n // Validation policy:\n // - `validateOnChange: true` → validate on every keystroke.\n // - `revalidateOnChangeAfterBlur: true` → validate only if the field has\n // already been touched (blurred once),\n // so errors clear as the user corrects\n // without appearing while first typing.\n // - both false → no on-change validation.\n //\n // Validation runs against the NEXT value (the string being written) rather than\n // going through `validateField` which reads the stale closed-over `values`. This\n // prevents the one-keystroke-lag bug where the error clears one character late.\n const setValue: (name: keyof T, value: string) => void = useCallback(\n (name: keyof T, value: string): void => {\n setFormValues((prev: T) => ({\n ...prev,\n [name]: value,\n }));\n\n const shouldValidate: boolean = validateOnChange || (revalidateOnChangeAfterBlur && touched[name] === true);\n if (!shouldValidate) {\n return;\n }\n\n const error: string | null = computeFieldError(value, getFieldConfig(name), requiredMessage);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n } else {\n delete newErrors[name];\n }\n return newErrors;\n });\n },\n [validateOnChange, revalidateOnChangeAfterBlur, touched, getFieldConfig, requiredMessage],\n );\n\n // Set multiple field values\n const setValues: (newValues: Partial<T>) => void = useCallback((newValues: Partial<T>): void => {\n setFormValues((prev: T) => ({\n ...prev,\n ...newValues,\n }));\n }, []);\n\n // Mark a field as touched\n const setTouched: (name: keyof T, isTouched?: boolean) => void = useCallback(\n (name: keyof T, isTouched = true): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n [name]: isTouched,\n }));\n\n // Validate on blur if enabled and field is touched\n if (validateOnBlur && isTouched) {\n const error: string | null = validateField(name);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n } else {\n delete newErrors[name];\n }\n return newErrors;\n });\n }\n },\n [validateField, validateOnBlur],\n );\n\n // Set multiple touched fields\n const setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void = useCallback(\n (touchedFields: Partial<Record<keyof T, boolean>>): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n ...touchedFields,\n }));\n },\n [],\n );\n\n // Mark all fields as touched\n const touchAllFields: () => void = useCallback((): void => {\n const allTouched: Record<keyof T, boolean> = fields.reduce(\n (acc: Record<keyof T, boolean>, field: FormField) => {\n acc[field.name as keyof T] = true;\n return acc;\n },\n {} as Record<keyof T, boolean>,\n );\n\n setFormTouched(allTouched);\n\n // Validate all fields\n const validation: ValidationResult = validateForm();\n setFormErrors(validation.errors as Record<keyof T, string>);\n }, [fields, validateForm]);\n\n // Set a field error\n const setError: (name: keyof T, error: string) => void = useCallback((name: keyof T, error: string): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n [name]: error,\n }));\n }, []);\n\n // Set multiple field errors\n const setErrors: (newErrors: Partial<Record<keyof T, string>>) => void = useCallback(\n (newErrors: Partial<Record<keyof T, string>>): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n ...newErrors,\n }));\n },\n [],\n );\n\n // Clear all errors\n const clearErrors: () => void = useCallback((): void => {\n setFormErrors({} as Record<keyof T, string>);\n }, []);\n\n // Reset form to initial state\n const reset: () => void = useCallback((): void => {\n setFormValues({...initialValues} as T);\n setFormTouched({} as Record<keyof T, boolean>);\n setFormErrors({} as Record<keyof T, string>);\n setIsSubmitted(false);\n }, [initialValues]);\n\n // Handle form submission\n const handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void> = useCallback(\n (onSubmit: (values: T) => void | Promise<void>) =>\n async (e?: FormEvent): Promise<void> => {\n if (e) {\n e.preventDefault();\n }\n\n setIsSubmitted(true);\n touchAllFields();\n\n const validation: ValidationResult = validateForm();\n\n if (validation.isValid) {\n await onSubmit(values);\n }\n },\n [values, touchAllFields, validateForm],\n );\n\n // Get field props for easy integration\n const getFieldProps: (name: keyof T) => any = useCallback(\n (name: keyof T) => {\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n\n return {\n error: touched[name] ? errors[name] : undefined,\n name,\n onBlur: (): void => setTouched(name, true),\n onChange: (value: string): void => setValue(name, value),\n required: fieldConfig?.required || false,\n touched: touched[name] || false,\n value: values[name] || '',\n };\n },\n [values, errors, touched, setValue, setTouched, getFieldConfig],\n );\n\n return {\n clearErrors,\n errors,\n getFieldProps,\n handleSubmit,\n isSubmitted,\n isValid,\n reset,\n setError,\n setErrors,\n setTouched,\n setTouchedFields,\n setValue,\n setValues,\n touchAllFields,\n touched,\n validateField,\n validateForm,\n values,\n };\n};\n\nexport default useForm;\n"],"mappings":";;;;;;;;AAsEA,MAAM,qBACJ,OACA,aACA,oBACkB;AAClB,KAAI,aAAa,aAAa,CAAC,SAAS,MAAM,MAAM,KAAK,IACvD,QAAO;AAET,KAAI,aAAa,WAAW;EAC1B,MAAMA,aAA4B,YAAY,UAAU,MAAM;AAC9D,MAAI,WAAY,QAAO;;AAEzB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoIT,MAAa,WAA6C,SAA2B,EAAE,KAAuB;CAC5G,MAAM,EACJ,gBAAgB,EAAE,EAClB,SAAS,EAAE,EACX,WACA,mBAAmB,OACnB,iBAAiB,MACjB,8BAA8B,OAC9B,kBAAkB,6BAChB;CAGJ,MAAM,CAAC,QAAQ,iBAAiB,SAAY,EAAC,GAAG,eAAc,CAAM;CACpE,MAAM,CAAC,SAAS,kBAAkB,SAAmC,EAAE,CAA6B;CACpG,MAAM,CAAC,QAAQ,iBAAiB,SAAkC,EAAE,CAA4B;CAChG,MAAM,CAAC,aAAa,kBAAkB,SAAS,MAAM;CAGrD,MAAMC,iBAA2D,aAC9D,SAAyC,OAAO,MAAM,UAAqB,MAAM,SAAS,KAAK,EAChG,CAAC,OAAO,CACT;CAID,MAAMC,gBAAkD,aACrD,SAAiC;AAGhC,SAAO,kBAFe,OAAO,SAAS,IACK,eAAe,KAAK,EAClB,gBAAgB;IAE/D;EAAC;EAAQ;EAAgB;EAAgB,CAC1C;CAGD,MAAMC,eAAuC,kBAAoC;EAC/E,MAAMC,YAAqC,EAAE;AAG7C,SAAO,SAAS,UAAqB;GACnC,MAAMC,QAAuB,cAAc,MAAM,KAAgB;AACjE,OAAI,MACF,WAAU,MAAM,QAAmB;IAErC;AAGF,MAAI,WAAW;GACb,MAAMC,eAAuC,UAAU,OAAO;AAC9D,UAAO,KAAK,aAAa,CAAC,SAAS,QAAgB;AACjD,QAAI,aAAa,KACf,WAAU,OAAkB,aAAa;KAE3C;;AAGJ,SAAO;GACL,QAAQ;GACR,SAAS,OAAO,KAAK,UAAU,CAAC,WAAW;GAC5C;IACA;EAAC;EAAQ;EAAe;EAAW;EAAO,CAAC;CAG9C,MAAMC,UAAmB,OAAO,KAAK,OAAO,CAAC,WAAW;CAexD,MAAMC,WAAmD,aACtD,MAAe,UAAwB;AACtC,iBAAe,UAAa;GAC1B,GAAG;IACF,OAAO;GACT,EAAE;AAGH,MAAI,EAD4B,oBAAqB,+BAA+B,QAAQ,UAAU,MAEpG;EAGF,MAAMH,QAAuB,kBAAkB,OAAO,eAAe,KAAK,EAAE,gBAAgB;AAC5F,iBAAe,SAAkC;GAC/C,MAAMD,YAAqC,EAAC,GAAG,MAAK;AACpD,OAAI,MACF,WAAU,QAAQ;OAElB,QAAO,UAAU;AAEnB,UAAO;IACP;IAEJ;EAAC;EAAkB;EAA6B;EAAS;EAAgB;EAAgB,CAC1F;CAGD,MAAMK,YAA6C,aAAa,cAAgC;AAC9F,iBAAe,UAAa;GAC1B,GAAG;GACH,GAAG;GACJ,EAAE;IACF,EAAE,CAAC;CAGN,MAAMC,aAA2D,aAC9D,MAAe,YAAY,SAAe;AACzC,kBAAgB,UAAoC;GAClD,GAAG;IACF,OAAO;GACT,EAAE;AAGH,MAAI,kBAAkB,WAAW;GAC/B,MAAML,QAAuB,cAAc,KAAK;AAChD,kBAAe,SAAkC;IAC/C,MAAMD,YAAqC,EAAC,GAAG,MAAK;AACpD,QAAI,MACF,WAAU,QAAQ;QAElB,QAAO,UAAU;AAEnB,WAAO;KACP;;IAGN,CAAC,eAAe,eAAe,CAChC;CAGD,MAAMO,mBAA+E,aAClF,kBAA2D;AAC1D,kBAAgB,UAAoC;GAClD,GAAG;GACH,GAAG;GACJ,EAAE;IAEL,EAAE,CACH;CAGD,MAAMC,iBAA6B,kBAAwB;AASzD,iBAR6C,OAAO,QACjD,KAA+B,UAAqB;AACnD,OAAI,MAAM,QAAmB;AAC7B,UAAO;KAET,EAAE,CACH,CAEyB;AAI1B,gBADqC,cAAc,CAC1B,OAAkC;IAC1D,CAAC,QAAQ,aAAa,CAAC;CAG1B,MAAMC,WAAmD,aAAa,MAAe,UAAwB;AAC3G,iBAAe,UAAmC;GAChD,GAAG;IACF,OAAO;GACT,EAAE;IACF,EAAE,CAAC;CAGN,MAAMC,YAAmE,aACtE,cAAsD;AACrD,iBAAe,UAAmC;GAChD,GAAG;GACH,GAAG;GACJ,EAAE;IAEL,EAAE,CACH;CAGD,MAAMC,cAA0B,kBAAwB;AACtD,gBAAc,EAAE,CAA4B;IAC3C,EAAE,CAAC;CAGN,MAAMC,QAAoB,kBAAwB;AAChD,gBAAc,EAAC,GAAG,eAAc,CAAM;AACtC,iBAAe,EAAE,CAA6B;AAC9C,gBAAc,EAAE,CAA4B;AAC5C,iBAAe,MAAM;IACpB,CAAC,cAAc,CAAC;CAGnB,MAAMC,eAAoG,aACvG,aACC,OAAO,MAAiC;AACtC,MAAI,EACF,GAAE,gBAAgB;AAGpB,iBAAe,KAAK;AACpB,kBAAgB;AAIhB,MAFqC,cAAc,CAEpC,QACb,OAAM,SAAS,OAAO;IAG5B;EAAC;EAAQ;EAAgB;EAAa,CACvC;AAoBD,QAAO;EACL;EACA;EACA,eApB4C,aAC3C,SAAkB;GACjB,MAAMC,cAAqC,eAAe,KAAK;AAE/D,UAAO;IACL,OAAO,QAAQ,QAAQ,OAAO,QAAQ;IACtC;IACA,cAAoB,WAAW,MAAM,KAAK;IAC1C,WAAW,UAAwB,SAAS,MAAM,MAAM;IACxD,UAAU,aAAa,YAAY;IACnC,SAAS,QAAQ,SAAS;IAC1B,OAAO,OAAO,SAAS;IACxB;KAEH;GAAC;GAAQ;GAAQ;GAAS;GAAU;GAAY;GAAe,CAChE;EAMC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD;;AAGH,sBAAe"}
1
+ {"version":3,"file":"useForm.js","names":["fieldError: string | null","getFieldConfig: (name: keyof T) => FormField | undefined","validateField: (name: keyof T) => string | null","revalidateTouchedFields: () => void","newErrors: Record<keyof T, string>","freshError: string | null","validateForm: () => ValidationResult","error: string | null","globalErrors: Record<string, string>","isValid: boolean","setValue: (name: keyof T, value: string) => void","setValues: (newValues: Partial<T>) => void","setTouched: (name: keyof T, isTouched?: boolean) => void","setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void","touchAllFields: () => void","validation: ValidationResult","setError: (name: keyof T, error: string) => void","setErrors: (newErrors: Partial<Record<keyof T, string>>) => void","clearErrors: () => void","reset: () => void","handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>","fieldConfig: FormField | undefined"],"sources":["../../src/hooks/useForm.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {useState, useCallback, useRef, FormEvent} from 'react';\n\n/**\n * Generic form field configuration\n */\nexport interface FormField {\n initialValue?: string;\n name: string;\n required?: boolean;\n validator?: (value: string) => string | null;\n}\n\n/**\n * Form validation result\n */\nexport interface ValidationResult {\n errors: Record<string, string>;\n isValid: boolean;\n}\n\n/**\n * Configuration for the useForm hook\n */\nexport interface UseFormConfig<T extends Record<string, string>> {\n /**\n * Form field definitions\n */\n fields?: FormField[];\n /**\n * Initial form values\n */\n initialValues?: Partial<T>;\n /**\n * Custom required field validation message\n */\n requiredMessage?: string;\n /**\n * When a field has been touched (blurred at least once), re-run validation on every\n * subsequent change so a rendered error clears the moment the value becomes valid.\n *\n * Independent of `validateOnChange`: this only affects fields that have already been\n * blurred, so users don't see errors while initially typing. Default `false` to\n * preserve prior behavior; enable per-form for a \"correct-as-you-type\" UX.\n */\n revalidateOnChangeAfterBlur?: boolean;\n /**\n * Whether to validate on blur (default: true)\n */\n validateOnBlur?: boolean;\n /**\n * Whether to validate on change (default: false). When true, every keystroke\n * triggers validation — including the first, which surfaces errors before the user\n * has had a chance to finish typing. Prefer `revalidateOnChangeAfterBlur` for the\n * common \"clear the error as the user corrects\" UX.\n */\n validateOnChange?: boolean;\n /**\n * Global form validator function\n */\n validator?: (values: T) => Record<string, string>;\n}\n\n/**\n * Pure per-field validator. Kept outside `useForm` so `setValue` can validate against\n * the value it just wrote — inline validation via the closure-based `validateField`\n * inside the hook would read stale `values` (state is committed asynchronously).\n */\nconst computeFieldError = (\n value: string,\n fieldConfig: FormField | undefined,\n requiredMessage: string,\n): string | null => {\n if (fieldConfig?.required && (!value || value.trim() === '')) {\n return requiredMessage;\n }\n if (fieldConfig?.validator) {\n const fieldError: string | null = fieldConfig.validator(value);\n if (fieldError) return fieldError;\n }\n return null;\n};\n\n/**\n * Return type for the useForm hook\n */\nexport interface UseFormReturn<T extends Record<string, string>> {\n /**\n * Clear all errors\n */\n clearErrors: () => void;\n /**\n * Current validation errors\n */\n errors: Record<keyof T, string>;\n /**\n * Get field props for easy integration with form components\n */\n getFieldProps: (name: keyof T) => {\n error: string | undefined;\n name: keyof T;\n onBlur: () => void;\n onChange: (value: string) => void;\n required: boolean;\n touched: boolean;\n value: string;\n };\n /**\n * Handle form submission\n */\n handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void>;\n /**\n * Whether the form has been submitted\n */\n isSubmitted: boolean;\n /**\n * Whether the form is currently valid\n */\n isValid: boolean;\n /**\n * Reset the form to initial values\n */\n reset: () => void;\n /**\n * Set a field error\n */\n setError: (name: keyof T, error: string) => void;\n /**\n * Set multiple field errors\n */\n setErrors: (errors: Partial<Record<keyof T, string>>) => void;\n /**\n * Mark a field as touched\n */\n setTouched: (name: keyof T, touched?: boolean) => void;\n /**\n * Mark multiple fields as touched\n */\n setTouchedFields: (touched: Partial<Record<keyof T, boolean>>) => void;\n /**\n * Set a single field value\n */\n setValue: (name: keyof T, value: string) => void;\n /**\n * Set multiple field values\n */\n setValues: (values: Partial<T>) => void;\n /**\n * Mark all fields as touched\n */\n touchAllFields: () => void;\n /**\n * Fields that have been touched by the user\n */\n touched: Record<keyof T, boolean>;\n /**\n * Validate a single field\n */\n validateField: (name: keyof T) => string | null;\n /**\n * Validate all fields\n */\n validateForm: () => ValidationResult;\n /**\n * Re-run validation for all touched fields that have a client-side config,\n * refreshing stored error strings to the current language.\n */\n revalidateTouchedFields: () => void;\n /**\n * Current form values\n */\n values: T;\n}\n\n/**\n * Generic form hook that provides comprehensive form state management and validation.\n *\n * @template T - The type of form values (must extend Record<string, string>)\n * @param config - Configuration options for the form\n * @returns Form state and methods\n *\n * @example\n * ```tsx\n * interface LoginForm {\n * username: string;\n * password: string;\n * }\n *\n * const {\n * values,\n * touched,\n * errors,\n * isValid,\n * setValue,\n * handleSubmit,\n * getFieldProps\n * } = useForm<LoginForm>({\n * initialValues: { username: '', password: '' },\n * fields: [\n * { name: 'username', required: true },\n * { name: 'password', required: true }\n * ]\n * });\n *\n * const onSubmit = handleSubmit((values) => {\n * console.log('Form submitted:', values);\n * });\n *\n * return (\n * <form onSubmit={onSubmit}>\n * <input {...getFieldProps('username')} />\n * <input {...getFieldProps('password')} type=\"password\" />\n * <button type=\"submit\" disabled={!isValid}>Submit</button>\n * </form>\n * );\n * ```\n */\nexport const useForm = <T extends Record<string, string>>(config: UseFormConfig<T> = {}): UseFormReturn<T> => {\n const {\n initialValues = {} as T,\n fields = [],\n validator,\n validateOnChange = false,\n validateOnBlur = true,\n revalidateOnChangeAfterBlur = false,\n requiredMessage = 'This field is required',\n } = config;\n\n // Initialize form state\n const [values, setFormValues] = useState<T>({...initialValues} as T);\n const [touched, setFormTouched] = useState<Record<keyof T, boolean>>({} as Record<keyof T, boolean>);\n const [errors, setFormErrors] = useState<Record<keyof T, string>>({} as Record<keyof T, string>);\n const [isSubmitted, setIsSubmitted] = useState(false);\n\n // Ref to track which fields have client-side validation rules. Errors injected via\n // serErrors are not added here, so revalidateTouchedFields preserves server-side errors.\n const clientErrorFieldRef = useRef(new Set<keyof T>());\n // Get field configuration by name\n const getFieldConfig: (name: keyof T) => FormField | undefined = useCallback(\n (name: keyof T): FormField | undefined => fields.find((field: FormField) => field.name === name),\n [fields],\n );\n\n // Validate a single field against currently-committed state. Used by the blur path\n // (where `values` is already up-to-date) and by `validateForm`.\n const validateField: (name: keyof T) => string | null = useCallback(\n (name: keyof T): string | null => {\n const value: string = values[name] || '';\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n return computeFieldError(value, fieldConfig, requiredMessage);\n },\n [values, getFieldConfig, requiredMessage],\n );\n\n // \"Latest value\" ref so revalidateTouchedFields can read fresh validators without\n // appearing in any effect dep array (avoids exhaustive-deps violations at call sites).\n const validateFieldRef = useRef(validateField);\n validateFieldRef.current = validateField;\n\n // Re-translate all, client-validated error strings on language change.\n // Stable identity ([] deps): uses only refs and the stable state setter.\n const revalidateTouchedFields: () => void = useCallback((): void => {\n setFormErrors((prevErrors: Record<keyof T, string>) => {\n if (Object.keys(prevErrors).length === 0) return prevErrors;\n\n const newErrors: Record<keyof T, string> = {...prevErrors};\n let changed = false;\n\n (Object.keys(prevErrors) as Array<keyof T>).forEach((name: keyof T) => {\n // Skip errors that were not produced by client-side validation - preserve server messages.\n if (!clientErrorFieldRef.current.has(name)) return;\n\n const freshError: string | null = validateFieldRef.current(name);\n if (freshError === prevErrors[name]) return;\n\n changed = true;\n if (freshError) {\n newErrors[name] = freshError;\n } else {\n delete newErrors[name];\n clientErrorFieldRef.current.delete(name);\n }\n });\n\n return changed ? newErrors : prevErrors;\n });\n }, []);\n\n // Validate the entire form\n const validateForm: () => ValidationResult = useCallback((): ValidationResult => {\n const newErrors: Record<keyof T, string> = {} as Record<keyof T, string>;\n\n // Validate individual fields\n fields.forEach((field: FormField) => {\n const error: string | null = validateField(field.name as keyof T);\n if (error) {\n newErrors[field.name as keyof T] = error;\n }\n });\n\n // Run global validator if provided\n if (validator) {\n const globalErrors: Record<string, string> = validator(values);\n Object.keys(globalErrors).forEach((key: string) => {\n if (globalErrors[key]) {\n newErrors[key as keyof T] = globalErrors[key];\n }\n });\n }\n\n return {\n errors: newErrors,\n isValid: Object.keys(newErrors).length === 0,\n };\n }, [fields, validateField, validator, values]);\n\n // Check if form is currently valid\n const isValid: boolean = Object.keys(errors).length === 0;\n\n // Set a single field value.\n //\n // Validation policy:\n // - `validateOnChange: true` → validate on every keystroke.\n // - `revalidateOnChangeAfterBlur: true` → validate only if the field has\n // already been touched (blurred once),\n // so errors clear as the user corrects\n // without appearing while first typing.\n // - both false → no on-change validation.\n //\n // Validation runs against the NEXT value (the string being written) rather than\n // going through `validateField` which reads the stale closed-over `values`. This\n // prevents the one-keystroke-lag bug where the error clears one character late.\n const setValue: (name: keyof T, value: string) => void = useCallback(\n (name: keyof T, value: string): void => {\n setFormValues((prev: T) => ({\n ...prev,\n [name]: value,\n }));\n\n const shouldValidate: boolean = validateOnChange || (revalidateOnChangeAfterBlur && touched[name] === true);\n if (!shouldValidate) {\n return;\n }\n\n const error: string | null = computeFieldError(value, getFieldConfig(name), requiredMessage);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n clientErrorFieldRef.current.add(name);\n } else {\n delete newErrors[name];\n clientErrorFieldRef.current.delete(name);\n }\n return newErrors;\n });\n },\n [validateOnChange, revalidateOnChangeAfterBlur, touched, getFieldConfig, requiredMessage],\n );\n\n // Set multiple field values\n const setValues: (newValues: Partial<T>) => void = useCallback((newValues: Partial<T>): void => {\n setFormValues((prev: T) => ({\n ...prev,\n ...newValues,\n }));\n }, []);\n\n // Mark a field as touched\n const setTouched: (name: keyof T, isTouched?: boolean) => void = useCallback(\n (name: keyof T, isTouched = true): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n [name]: isTouched,\n }));\n\n // Validate on blur if enabled and field is touched\n if (validateOnBlur && isTouched) {\n const error: string | null = validateField(name);\n setFormErrors((prev: Record<keyof T, string>) => {\n const newErrors: Record<keyof T, string> = {...prev};\n if (error) {\n newErrors[name] = error;\n clientErrorFieldRef.current.add(name);\n } else {\n delete newErrors[name];\n clientErrorFieldRef.current.delete(name);\n }\n return newErrors;\n });\n }\n },\n [validateField, validateOnBlur],\n );\n\n // Set multiple touched fields\n const setTouchedFields: (touchedFields: Partial<Record<keyof T, boolean>>) => void = useCallback(\n (touchedFields: Partial<Record<keyof T, boolean>>): void => {\n setFormTouched((prev: Record<keyof T, boolean>) => ({\n ...prev,\n ...touchedFields,\n }));\n },\n [],\n );\n\n // Mark all fields as touched\n const touchAllFields: () => void = useCallback((): void => {\n const allTouched: Record<keyof T, boolean> = fields.reduce(\n (acc: Record<keyof T, boolean>, field: FormField) => {\n acc[field.name as keyof T] = true;\n return acc;\n },\n {} as Record<keyof T, boolean>,\n );\n\n setFormTouched(allTouched);\n\n // Validate all fields\n const validation: ValidationResult = validateForm();\n setFormErrors(validation.errors as Record<keyof T, string>);\n clientErrorFieldRef.current = new Set(Object.keys(validation.errors) as Array<keyof T>);\n }, [fields, validateForm]);\n\n // Set a field error\n const setError: (name: keyof T, error: string) => void = useCallback((name: keyof T, error: string): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n [name]: error,\n }));\n }, []);\n\n // Set multiple field errors\n const setErrors: (newErrors: Partial<Record<keyof T, string>>) => void = useCallback(\n (newErrors: Partial<Record<keyof T, string>>): void => {\n setFormErrors((prev: Record<keyof T, string>) => ({\n ...prev,\n ...newErrors,\n }));\n },\n [],\n );\n\n // Clear all errors\n const clearErrors: () => void = useCallback((): void => {\n setFormErrors({} as Record<keyof T, string>);\n clientErrorFieldRef.current.clear();\n }, []);\n\n // Reset form to initial state\n const reset: () => void = useCallback((): void => {\n setFormValues({...initialValues} as T);\n setFormTouched({} as Record<keyof T, boolean>);\n setFormErrors({} as Record<keyof T, string>);\n setIsSubmitted(false);\n clientErrorFieldRef.current.clear();\n }, [initialValues]);\n\n // Handle form submission\n const handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: FormEvent) => Promise<void> = useCallback(\n (onSubmit: (values: T) => void | Promise<void>) =>\n async (e?: FormEvent): Promise<void> => {\n if (e) {\n e.preventDefault();\n }\n\n setIsSubmitted(true);\n touchAllFields();\n\n const validation: ValidationResult = validateForm();\n\n if (validation.isValid) {\n await onSubmit(values);\n }\n },\n [values, touchAllFields, validateForm],\n );\n\n // Get field props for easy integration\n const getFieldProps: (name: keyof T) => any = useCallback(\n (name: keyof T) => {\n const fieldConfig: FormField | undefined = getFieldConfig(name);\n\n return {\n error: touched[name] ? errors[name] : undefined,\n name,\n onBlur: (): void => setTouched(name, true),\n onChange: (value: string): void => setValue(name, value),\n required: fieldConfig?.required || false,\n touched: touched[name] || false,\n value: values[name] || '',\n };\n },\n [values, errors, touched, setValue, setTouched, getFieldConfig],\n );\n\n return {\n clearErrors,\n errors,\n getFieldProps,\n handleSubmit,\n isSubmitted,\n isValid,\n reset,\n revalidateTouchedFields,\n setError,\n setErrors,\n setTouched,\n setTouchedFields,\n setValue,\n setValues,\n touchAllFields,\n touched,\n validateField,\n validateForm,\n values,\n };\n};\n\nexport default useForm;\n"],"mappings":"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"}
@@ -8,5 +8,7 @@ export type ThunderIDReactConfig = ThunderIDBrowserConfig & {
8
8
  * own CSP header/meta tag issues for the current request.
9
9
  */
10
10
  cspNonce?: string;
11
+ /** Forwarded to `FlowMetaProvider`'s `namespace` prop. See its doc for details. */
12
+ namespace?: string;
11
13
  };
12
14
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/models/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,sBAAsB,EAAC,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG;IAC1D;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/models/config.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,sBAAsB,EAAC,MAAM,oBAAoB,CAAC;AAE1D,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GAAG;IAC1D;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,mFAAmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thunderid/react",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "React SDK for ThunderID",
5
5
  "keywords": [
6
6
  "thunderid",
@@ -62,7 +62,7 @@
62
62
  "@floating-ui/react": "0.27.12",
63
63
  "dompurify": "3.4.13",
64
64
  "tslib": "2.8.1",
65
- "@thunderid/browser": "^1.0.5"
65
+ "@thunderid/browser": "^1.0.6"
66
66
  },
67
67
  "publishConfig": {
68
68
  "access": "public"