@adyen/kyc-components
Version:
This guide assumes that you have already an account with Adyen. A legalEntity needs to be created, and you need to have a `legalEntityId` to instatiate a Component.
755 lines (754 loc) • 25.8 kB
JavaScript
try {
let e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {}, n = new e.Error().stack;
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "f8849619-94da-4804-a0a9-0600fa78485c", e._sentryDebugIdIdentifier = "sentry-dbid-f8849619-94da-4804-a0a9-0600fa78485c");
} catch (e) {}
import { o as createLogger } from "./translation-BFxyJ1c5.js";
import { c as objectsDeepEqual } from "./useAnalyticsContext-BVFDMrVE.js";
import { y as isEmpty } from "./validatorUtils-DRapRJ6z.js";
import { t as ValidationResult } from "./validationResult-D3sPVzMw.js";
import { t as isNotEmptyValidator } from "./commonValidators-DCdet-gH.js";
import { useCallback, useEffect, useMemo, useReducer } from "preact/hooks";
//#region src/hooks/useMultiForm/utils.ts
var omitKeys = (obj, omit) => Object.keys(obj).filter((k) => !omit.includes(k)).reduce((a, c) => {
a[c] = obj[c];
return a;
}, {});
var addKeys = (obj, add, initialValue, defaultData, pendingData) => add.reduce((a, c) => ({
...a,
[c]: a[c] ?? pendingData?.[c] ?? defaultData?.[c] ?? initialValue
}), obj);
//#endregion
//#region src/hooks/useMultiForm/reducer.ts
var hasValueChanged = (newData, prevData) => {
if (typeof newData === "object") return !objectsDeepEqual(newData, prevData);
return newData !== prevData;
};
/** Formats and validates a field */
var processField = ({ key, value, formName, mode, defaultData, data, fieldContext, formatters, staticValidate }) => {
const formatterFn = formatters?.[key]?.formatter ? formatters[key]?.formatter : formatters?.[key];
const formattedValue = formatterFn && typeof formatterFn === "function" ? formatterFn(value, fieldContext) : value;
const fieldHasDefaultValue = key in (defaultData ?? {});
const fieldHasChanged = defaultData?.[key] !== value;
const validationResult = (fieldContext.state.obscuredFields?.includes(key) ? fieldHasChanged : fieldHasChanged || fieldHasDefaultValue) ? staticValidate({
key,
value: formattedValue,
mode,
formName
}, data, fieldContext) : new ValidationResult([{
isValid: true,
hasError: false
}]);
const asyncValidationResult = fieldContext.state?.asyncErrors?.[key];
/** Add any existing async validation result to validation result */
if (asyncValidationResult) validationResult.addError(asyncValidationResult);
return [formattedValue, validationResult];
};
/**
* The function takes in the below params and spits out an initial state, by running the validations
* across all forms and fields. It makes sure all validations are done and only sets the validity
* instead of the errors, this is to avoid users seeing error immaturely on initial load.
*
* @param requiredFields
* @param staticValidate
* @param asyncValidate
* @param defaultData
* @param obscuredFields
* @param optionalFields
* @param trustedFields
* @param fieldProblems
* @param formatters
* @param data
*/
function init({ requiredFields, staticValidate, defaultData, obscuredFields, optionalFields, trustedFields, fieldProblems, formatters, data, asyncErrors }) {
/**
* Fetches the validation result of a field in a form
* @param fieldKey
* @param formName
*/
const getProcessedState = (fieldKey, formName) => {
const [formattedValue, validationResult] = processField({
key: fieldKey,
value: data?.[formName]?.[fieldKey] ?? defaultData?.[formName]?.[fieldKey] ?? null,
mode: "blur",
formName,
formatters: formatters?.[formName],
defaultData: defaultData?.[formName],
data,
fieldContext: { state: {
data: data?.[formName],
obscuredFields: obscuredFields?.[formName],
optionalFields: optionalFields?.[formName],
trustedFields: trustedFields?.[formName],
requiredFields: requiredFields?.[formName],
asyncErrors: asyncErrors?.[formName]
} },
staticValidate
});
return {
valid: validationResult.isValid && !fieldProblems?.[formName]?.[fieldKey] || false,
errors: null,
data: formattedValue,
fieldProblems: fieldProblems?.[formName]?.[fieldKey]
};
};
/**
* requiredFields can be undefined if the configuration api call is not yet completed
* returned default state in this case
*/
if (!requiredFields) return {
data: defaultData,
errors: {},
fieldProblems: {},
valid: {},
requiredFields: {},
asyncErrors: {},
defaultData
};
/**
* Make sure forms containing only optional fields are also taken into account
*/
const formsForOptionalFields = Object.keys(optionalFields ?? {});
const formsForRequiredFields = Object.keys(requiredFields);
/**
* Visits every field and builds a state with the validity
*/
return Array.from(new Set([...formsForOptionalFields, ...formsForRequiredFields])).reduce((acc, formName) => {
const form = formName;
const optionalsFormFields = optionalFields?.[form] ?? [];
const requiredFormFields = requiredFields?.[form] ?? [];
const formState = [...optionalsFormFields, ...requiredFormFields]?.reduce((acc, field) => {
const { valid, errors, data, fieldProblems: processedFieldProblems } = getProcessedState(field, form);
return {
...acc,
data: {
...acc.data,
[field]: data
},
valid: {
...acc.valid,
[field]: valid
},
errors: {
...acc.errors,
[field]: errors
},
fieldProblems: {
...acc.fieldProblems,
...processedFieldProblems ? { [field]: processedFieldProblems } : {}
}
};
}, {});
return {
requiredFields,
data: {
...acc.data,
[formName]: formState.data
},
valid: {
...acc.valid,
[formName]: formState.valid
},
errors: {
...acc.errors,
[formName]: formState.errors
},
fieldProblems: {
...acc.fieldProblems,
[formName]: formState.fieldProblems
},
asyncErrors: { ...acc.asyncErrors },
obscuredFields,
optionalFields,
trustedFields,
defaultData
};
}, {
requiredFields,
data: {},
valid: {},
errors: {},
fieldProblems: {},
obscuredFields,
optionalFields,
trustedFields,
asyncErrors: {}
});
}
function reducer({ staticValidate, obscuredFields, optionalFields, trustedFields, formatters, defaultData }) {
return (state, { type, key, mode, value, formData, formName, selectedSchema, innerFormFields, requiredFields, optionalFields: newOptionalFields, asyncValidationResults }) => {
const clonedState = structuredClone(state);
switch (type) {
case "setData":
if (!key || !formName) return state;
return {
...clonedState,
data: {
...clonedState.data,
[formName]: {
...clonedState.data?.[formName],
[key]: value
}
}
};
case "setFormData": {
if (!formName || !formData) return state;
const formRequiredFields = clonedState.requiredFields?.[formName] ?? [];
const formOptionalFields = clonedState.optionalFields?.[formName] ?? [];
const allowedFields = [...formRequiredFields, ...formOptionalFields];
const filteredUpdates = Object.keys(formData).filter((key) => allowedFields.includes(key)).reduce((obj, key) => {
obj[key] = formData[key];
return obj;
}, {});
const updatedData = {
...clonedState.data,
[formName]: filteredUpdates
};
const formValidity = allowedFields.reduce((acc, field) => {
const [, validation] = processField({
key: field,
value: filteredUpdates[field],
mode: "blur",
formName,
defaultData: defaultData?.[formName],
data: updatedData,
formatters: formatters?.[formName],
fieldContext: { state: {
data: clonedState?.data?.[formName],
obscuredFields: obscuredFields?.[formName],
optionalFields: optionalFields?.[formName],
trustedFields: trustedFields?.[formName],
errors: clonedState.errors?.[formName],
valid: clonedState.valid?.[formName],
fieldProblems: clonedState.fieldProblems?.[formName],
asyncErrors: clonedState.asyncErrors?.[formName]
} },
staticValidate
});
return {
...acc,
[field]: validation.isValid
};
}, {});
return {
...clonedState,
data: {
...clonedState.data,
[formName]: { ...filteredUpdates }
},
valid: {
...clonedState.valid,
[formName]: formValidity
}
};
}
case "setValid":
if (!key || !formName) return state;
return clonedState?.valid?.[formName]?.[key] === value ? state : {
...clonedState,
valid: {
...clonedState.valid,
[formName]: {
...clonedState.valid?.[formName],
[key]: value
}
}
};
case "setErrors":
if (!key || !formName) return state;
return {
...clonedState,
errors: {
...clonedState.errors,
[formName]: {
...clonedState.errors?.[formName],
[key]: value
}
}
};
case "setRequiredFields": {
/**
* If defaultData changes reset the data in the form to new defaultData
* Fetching documents are bit delayed, this accomodates that delay
*/
const hasDefaultDataChanged = !objectsDeepEqual(defaultData, state.defaultData);
if (objectsDeepEqual(requiredFields, clonedState.requiredFields) && objectsDeepEqual(newOptionalFields, clonedState.optionalFields) && !hasDefaultDataChanged) return state;
return init({
requiredFields,
data: hasDefaultDataChanged ? defaultData : clonedState.data,
fieldProblems: clonedState.fieldProblems,
obscuredFields,
optionalFields: newOptionalFields,
trustedFields,
staticValidate,
defaultData
});
}
case "setInnerFormRequiredFields": {
/**
* This sets the requiredFields in a single form.
* Ideally this should not be used anywhere, as this goes against the idea of having all
* the rules of calculating requiredFields in a single place. keeping this in case we have
* any exceptional scenarios where this would be required. Can be removed once all forms
* migrated to useMultiform
*/
if (!innerFormFields || !formName) return state;
const requiredFields = {
...clonedState.requiredFields,
[formName]: innerFormFields
};
const defaultState = init({
requiredFields,
defaultData,
fieldProblems: clonedState.fieldProblems,
obscuredFields,
optionalFields,
trustedFields,
staticValidate,
data: defaultData
});
const removedFields = clonedState.optionalFields?.[formName].length ? [...(clonedState.requiredFields?.[formName] ?? []).filter((field) => !innerFormFields.includes(field)), ...clonedState.optionalFields[formName].filter((field) => !optionalFields?.[formName].includes(field))] : clonedState.requiredFields?.[formName].filter((field) => !innerFormFields.includes(field));
const newFields = optionalFields?.[formName].length ? [...innerFormFields.filter((field) => !clonedState.requiredFields?.[formName].includes(field)), ...(optionalFields[formName] ?? []).filter((field) => !clonedState.optionalFields?.[formName].includes(field))] : innerFormFields.filter((field) => !clonedState.requiredFields?.[formName].includes(field));
const formLocal = {
data: omitKeys(clonedState.data?.[formName], newFields),
errors: omitKeys(clonedState.errors?.[formName], newFields),
valid: omitKeys(clonedState.valid?.[formName], newFields),
fieldProblems: omitKeys(clonedState.fieldProblems?.[formName], newFields)
};
const local = {
...clonedState.local,
[formName]: formLocal
};
const updatedFormData = addKeys(omitKeys(clonedState.data?.[formName], removedFields), newFields, null, defaultState.data?.[formName], clonedState.local?.[formName].data);
const updatedData = {
...clonedState.data,
[formName]: updatedFormData
};
const updatedFormValidity = addKeys(omitKeys(clonedState.valid?.[formName], removedFields), newFields, false, defaultState.valid?.[formName], clonedState.local?.[formName].valid);
const updatedValidity = {
...clonedState.valid,
[formName]: updatedFormValidity
};
const updatedFormErrors = addKeys(omitKeys(clonedState.errors?.[formName], removedFields), newFields, null, defaultState.errors?.[formName], clonedState.local?.[formName].errors);
const updatedErrors = {
...clonedState.errors,
[formName]: updatedFormErrors
};
const updatedFormFieldProblems = addKeys(omitKeys(clonedState.fieldProblems?.[formName], removedFields), newFields, false, defaultState.fieldProblems?.[formName], clonedState.local?.[formName].fieldProblems);
return {
requiredFields,
data: updatedData,
valid: updatedValidity,
errors: updatedErrors,
fieldProblems: {
...clonedState.fieldProblems,
[formName]: updatedFormFieldProblems
},
asyncErrors: clonedState.asyncErrors,
optionalFields,
trustedFields,
local,
defaultData
};
}
case "updateField": {
/**
* Called when a field value changes in form
* Validates the field and updates data, validity, error in the state for the field
*/
if (!key || !mode || !formName) return state;
const updatedData = {
...clonedState.data,
[formName]: {
...clonedState.data?.[formName],
[key]: value
}
};
/**
* Wipe async errors for the field if value has been changed
*/
const updatedAsyncErrors = hasValueChanged(value, clonedState.data?.[formName]?.[key]) ? {
...clonedState.asyncErrors,
[formName]: {
...clonedState.asyncErrors?.[formName],
[key]: null
}
} : clonedState.asyncErrors;
const [formattedValue, validation] = processField({
key,
value,
mode,
formName,
defaultData: defaultData?.[formName],
data: updatedData,
formatters: formatters?.[formName],
fieldContext: { state: {
data: clonedState.data?.[formName],
obscuredFields: obscuredFields?.[formName],
optionalFields: optionalFields?.[formName],
trustedFields: trustedFields?.[formName],
errors: clonedState.errors[formName],
valid: clonedState.valid[formName],
fieldProblems: clonedState.fieldProblems[formName],
asyncErrors: updatedAsyncErrors[formName]
} },
staticValidate
});
const clearFieldProblems = (clonedState.data?.[formName])?.[key] !== formattedValue;
return {
...clonedState,
data: {
...clonedState.data,
[formName]: {
...clonedState.data?.[formName],
[key]: formattedValue
}
},
asyncErrors: updatedAsyncErrors,
errors: {
...clonedState.errors,
[formName]: {
...clonedState.errors[formName],
[key]: validation.getError() ?? null
}
},
valid: {
...clonedState.valid,
[formName]: {
...clonedState?.valid?.[formName],
[key]: (validation.isValid && !clonedState.fieldProblems?.[formName]?.[key]) ?? false
}
},
fieldProblems: {
...clonedState.fieldProblems,
[formName]: {
...clonedState.fieldProblems[formName],
...!clearFieldProblems && clonedState.fieldProblems?.[formName]?.[key] ? { [key]: true } : {}
}
}
};
}
case "validateForm": {
/**
* Called when triggerValidation('formName') is called, validates all the fields in the form
* and populates the errors in the state
*/
if (!formName) return state;
const formFields = clonedState.requiredFields?.[formName];
if (!formFields) return state;
const formState = (selectedSchema || formFields).reduce((acc, field) => {
const formData = clonedState.data?.[formName];
const [formattedValue, validation] = processField({
key: field,
mode: "blur",
value: formData?.[field] ?? null,
formName,
defaultData: defaultData?.[formName],
data: clonedState.data,
formatters: formatters?.[formName],
fieldContext: { state: {
data: clonedState.data?.[formName],
obscuredFields: obscuredFields?.[formName],
optionalFields: optionalFields?.[formName],
trustedFields: trustedFields?.[formName],
errors: clonedState.errors[formName],
valid: clonedState.valid[formName],
fieldProblems: clonedState.fieldProblems[formName],
asyncErrors: clonedState.asyncErrors[formName]
} },
staticValidate
});
const clearFieldProblems = (clonedState?.data?.[formName])?.[field] !== formattedValue;
const fieldAsyncValidationResult = asyncValidationResults?.[field];
return {
data: formData,
asyncErrors: {
...acc.asyncErrors,
[field]: fieldAsyncValidationResult?.getError() ?? null
},
errors: {
...acc.errors,
[field]: validation.getError() ?? fieldAsyncValidationResult?.getError() ?? null
},
valid: {
...acc.valid,
[field]: validation.isValid && (fieldAsyncValidationResult?.isValid ?? true) && !clonedState.fieldProblems[formName][field]
},
fieldProblems: {
...acc.fieldProblems,
...!clearFieldProblems && acc.fieldProblems?.[field] ? { [field]: true } : {}
}
};
}, {});
return {
...clonedState,
errors: {
...clonedState.errors,
[formName]: formState.errors
},
asyncErrors: {
...clonedState.asyncErrors,
[formName]: formState.asyncErrors
},
valid: {
...clonedState.valid,
[formName]: formState.valid
},
fieldProblems: {
...clonedState.fieldProblems,
[formName]: formState.fieldProblems
}
};
}
default: throw new Error("Undefined useForm action");
}
};
}
//#endregion
//#region src/hooks/useMultiForm/useMultiFormAsyncValidator.ts
var logger = createLogger();
/**
* Returns
* - triggerAsyncValidation function which can execute the async validation for a field and populate the sync validation results
* - asyncValidationResult state which keeps track of async validations of all fields.
* @param asyncRules
*/
var useMultiFormAsyncValidator = (asyncRules) => {
const validResult = {
isValid: true,
hasError: false
};
const triggerAsyncValidation = useCallback(async (field, formName, data) => {
if (!asyncRules) return new ValidationResult([validResult]);
const rule = typeof asyncRules === "function" ? asyncRules(data)[formName]?.[field] : asyncRules[formName][field];
const value = data?.[formName]?.[field];
if (!rule || !value) return new ValidationResult([validResult]);
const isValid = await rule.asyncValidate(value).catch(logger.error);
if (isValid === false) return new ValidationResult([{
isValid,
errorMessage: typeof rule.errorMessage === "function" ? rule.errorMessage(value) : rule.errorMessage,
hasError: !isValid
}]);
return new ValidationResult([validResult]);
}, [asyncRules]);
const triggerAsyncFormValidation = async (formName, state, selectedSchema) => {
const fields = selectedSchema ?? state.requiredFields?.[formName];
if (!fields?.length) return { isValid: true };
const asyncValidationResults = await fields.reduce(async (accPromise, fieldName) => {
const acc = await accPromise;
const asyncValidationResult = await triggerAsyncValidation(fieldName, formName, state.data);
return {
...acc,
[fieldName]: asyncValidationResult
};
}, Promise.resolve({}));
const isValid = Object.values(asyncValidationResults).every((validationResult) => validationResult.isValid);
return {
...asyncValidationResults,
isValid
};
};
return { triggerAsyncFormValidation };
};
//#endregion
//#region src/hooks/useMultiForm/useMultiFormStaticValidator.ts
var useMultiFormStaticValidator = (rules) => {
const getRulesForField = useCallback((field, form, data) => {
const fallbackRule = {
validate: () => true,
modes: ["blur", "input"]
};
if (!form) return [fallbackRule];
const fieldRules = typeof rules === "function" ? rules(data)?.[form]?.[field] ?? [fallbackRule] : rules?.[form]?.[field] ?? [fallbackRule];
return Array.isArray(fieldRules) ? fieldRules : [fieldRules];
}, [rules]);
return { triggerStaticValidation: useCallback(({ key, value, mode = "blur", formName }, data, context) => {
const fieldRules = getRulesForField(key, formName, data);
const isFieldOptional = !!context?.state.optionalFields?.includes(key);
const isFieldObscured = !!context?.state.obscuredFields?.includes(key);
if (!isFieldOptional) fieldRules.unshift(isNotEmptyValidator);
return new ValidationResult(fieldRules.map((rule) => {
const shouldValidate = rule.modes.includes(mode) && !isFieldObscured;
const isValid = isFieldOptional && isEmpty(value) ? true : rule.validate(value, context);
return {
isValid,
errorMessage: typeof rule.errorMessage === "function" ? rule.errorMessage(value, context) : rule.errorMessage,
hasError: shouldValidate && !isValid
};
}));
}, [getRulesForField]) };
};
//#endregion
//#region src/hooks/useMultiForm/useMultiForm.ts
function useMultiForm({ defaultData, rules, asyncRules, fieldProblems, allFields, requiredFields, obscuredFields, optionalFields, trustedFields, formatters }) {
const { triggerStaticValidation } = useMultiFormStaticValidator(rules);
const { triggerAsyncFormValidation } = useMultiFormAsyncValidator(asyncRules);
const getRequiredFields = useMemo(() => typeof requiredFields === "function" ? requiredFields : () => requiredFields, [requiredFields]);
const getSchema = useMemo(() => getRequiredFields(defaultData), [defaultData, getRequiredFields]);
const getReducer = useCallback(() => reducer({
staticValidate: triggerStaticValidation,
defaultData,
obscuredFields,
optionalFields,
trustedFields,
formatters
}), [
triggerStaticValidation,
obscuredFields,
optionalFields,
trustedFields,
formatters,
defaultData
]);
const initialData = useMemo(() => ({
requiredFields: getSchema,
defaultData,
fieldProblems,
obscuredFields,
optionalFields,
trustedFields,
formatters,
staticValidate: triggerStaticValidation,
data: defaultData
}), [
getSchema,
defaultData,
fieldProblems,
obscuredFields,
optionalFields,
trustedFields,
formatters,
triggerStaticValidation
]);
const [state, dispatch] = useReducer(getReducer(), initialData, init);
const isValid = useMemo(() => {
const formsForOptionalFields = Object.keys(state.optionalFields ?? {});
const formsForRequiredFields = Object.keys(state.requiredFields ?? {});
return Array.from(new Set([...formsForOptionalFields, ...formsForRequiredFields])).reduce((acc, formName) => {
const form = formName;
const optionalsFormFields = state.optionalFields?.[form] ?? [];
const requiredFormFields = state.requiredFields?.[form] ?? [];
const formFields = [...optionalsFormFields, ...requiredFormFields];
const formValidity = state.valid[formName] ?? {};
return {
...acc,
[formName]: Object.entries(formValidity)?.every(([field, valid]) => formFields.includes(field) ? valid : true)
};
}, {});
}, [state.requiredFields, state.valid]);
const getTargetValue = useCallback((key, formName, e) => {
if (!e?.target) return e;
if (e.target.type === "checkbox") return !state.data?.[formName][key];
return e.target.value;
}, [state.data]);
const setErrors = useCallback((key, value, formName) => {
dispatch({
type: "setErrors",
key,
value,
formName
});
}, []);
const setValid = useCallback((key, value, formName) => {
dispatch({
type: "setValid",
key,
value,
formName
});
}, []);
const setData = useCallback((key, value, formName) => {
dispatch({
type: "setData",
key,
value,
formName
});
}, []);
const setFormData = useCallback((formName, formData) => {
dispatch({
type: "setFormData",
formName,
formData
});
}, []);
const handleChangeFor = useCallback((key, formName, mode = "blur") => (e) => {
dispatch({
type: "updateField",
key,
value: getTargetValue(key, formName, e),
mode,
formName
});
}, [getTargetValue]);
const triggerValidation = useCallback(async (formName, selectedSchema) => {
const asyncValidationResults = await triggerAsyncFormValidation(formName, state, selectedSchema);
dispatch({
type: "validateForm",
formName,
selectedSchema,
asyncValidationResults
});
return asyncValidationResults.isValid;
}, [state, triggerAsyncFormValidation]);
const resetToDefaultData = useCallback(() => {
if (!getSchema) return;
Object.entries(getSchema).forEach(([formName]) => {
const form = formName;
const formData = defaultData?.[form];
getSchema[form].forEach((field) => {
if (!formData || typeof formData[field] !== "undefined") {
handleChangeFor(field, form)(formData);
return;
}
setValid(field, false, form);
setErrors(field, null, form);
setData(field, formData[field], form);
});
});
}, [
defaultData,
getSchema,
handleChangeFor,
setData,
setErrors,
setValid
]);
useEffect(() => {
const requiredFields = getRequiredFields(state.data);
if (!requiredFields) return;
dispatch({
type: "setRequiredFields",
requiredFields,
defaultData,
optionalFields
});
}, [
getRequiredFields,
state.data,
defaultData,
optionalFields
]);
return {
setData,
setFormData,
setValid,
setErrors,
handleChangeFor,
triggerValidation,
resetToDefaultData,
isValid,
allFields,
requiredFields: state.requiredFields,
optionalFields: state.optionalFields,
trustedFields: state.trustedFields,
obscuredFields: state.obscuredFields,
valid: state.valid,
errors: state.errors,
data: state.data,
fieldProblems: state.fieldProblems,
defaultData,
asyncErrors: state.asyncErrors
};
}
//#endregion
export { useMultiForm as t };