UNPKG

@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
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 };