@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.
276 lines (275 loc) • 10.4 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] = "b9ca1acd-0d9f-443e-94bb-5c21c14ccdbd", e._sentryDebugIdIdentifier = "sentry-dbid-b9ca1acd-0d9f-443e-94bb-5c21c14ccdbd");
} catch (e) {}
import { o as createLogger } from "./translation-BFxyJ1c5.js";
import { t as useAnalyticsContext } from "./useAnalyticsContext-BVFDMrVE.js";
import { t as formDebugInfo } from "./debugStore-bZ_bkJeS.js";
import { r as FormRouterContext } from "./ErrorPanel-B536hgSc.js";
import { t as trackNavigation } from "./trackNavigation-LvCP5Vyc.js";
import { n as summaryStep } from "./Summary-B5IkOGJV.js";
import { useCallback, useMemo, useRef, useState } from "preact/hooks";
import { jsx } from "preact/jsx-runtime";
//#region src/hooks/usePageLandedEvent.ts
/**
* Hook that automatically sends a "Landed on page" analytics event on when active form changes.
* Used on form component level (i.e. Basic information, Additional details etc.) to explicitly track page visits.
* This centralizes the common analytics pattern used across form components.
*/
var usePageLandedEvent = (formName) => {
const userEvents = useAnalyticsContext();
const oldFormNameRef = useRef(void 0);
if (formName && oldFormNameRef.current !== formName) {
userEvents.updateSharedEventProperties({ page: formName });
userEvents.addPageEvent("Landed on page", { actionType: "navigate" });
oldFormNameRef.current = formName;
}
};
//#endregion
//#region src/hooks/useFormComposer.ts
var logger$1 = createLogger();
/**
* The step we open to for editing an existing entity should be the summary if there are verification errors
* Otherwise (including for a new entity), just show the first step
*/
var getOpeningStep = (forms, remediationActions) => {
if (remediationActions && Object.keys(remediationActions).length > 0) {
const allRemediationActions = Object.values(remediationActions).flat().filter((rem) => rem.forms?.length > 0);
if (allRemediationActions.length === 1 && allRemediationActions[0]?.forms.length === 1) {
const form = forms.find((form) => form?.formId === allRemediationActions[0]?.forms[0]);
if (form) return form;
}
return forms[forms.length - 1];
}
return forms[0];
};
var useFormComposer = ({ problems, navigationTrackingParams, forms, externalBackClick, onSubmit, triggerValidation }) => {
const userEvents = useAnalyticsContext();
const [shouldValidate, setShouldValidate] = useState(false);
const [activeForm, setActiveForm] = useState(forms[0]);
const isFormSummaryStep = (form) => form.formId === summaryStep.formId;
const getFormIndex = (formId) => forms.findIndex((form) => form.formId === formId);
const currentStep = getFormIndex(activeForm.formId);
const totalSteps = forms.length;
const isFinalStep = currentStep === totalSteps - 1;
const isFirstStep = currentStep === 0;
const firstStepId = forms[0]?.formId;
usePageLandedEvent(activeForm.formName);
const [oldForms, setOldForms] = useState(forms);
if (oldForms !== forms) {
setActiveForm((activeForm) => forms.find(({ formId }) => formId === activeForm.formId) ?? activeForm);
setOldForms(forms);
}
/**
* Opening step can be calculated once since the problems are already known on drop-in load
*/
const [oldFirstStepId, setOldFirstStepId] = useState(void 0);
if (oldFirstStepId !== firstStepId) {
/** TODO: need a better solution
* If firstStepId changes it means form configurations are being setup,
* not able to put forms as the dependency here as it also updates whenever validity changes
* should separate out forms into forms and validity
***/
setActiveForm(getOpeningStep(forms, problems?.remediationActions));
setOldFirstStepId(firstStepId);
}
const gotoFormByFormIndex = (nextFormIndex, currentForms) => {
setActiveForm(currentForms?.[nextFormIndex] ?? forms[nextFormIndex]);
};
/**
* We often run into a situation where we want to navigate to a specific form immediately after
* rearranging the forms in the flow.
*
* The issue here is that the callback, (e.g. {@link IdVerificationMethod.handleVerifyByInvite})
* has a reference only to the current forms, not what the forms will be post-rearrangement.
* Hence, it is impossible to call {@link gotoFormByFormIndex} because we have no idea what the
* eventual index of the form will be.
*
* The solution: set a desired form ID, then resolve the navigation as part of the render
* once {@link forms} contains it. The subsequent {@link gotoFormByFormIndex} will immediately
* cancel the current render and trigger a new one.
*/
const [desiredFormId, setDesiredFormId] = useState();
if (desiredFormId) {
const targetFormIndex = forms.findIndex((form) => form.formId === desiredFormId);
if (targetFormIndex === -1) logger$1.warn(`Desired form ${desiredFormId} not present. \nCurrent forms: ${forms.map((f) => f.formId).join(", ")}`);
else {
setDesiredFormId(void 0);
gotoFormByFormIndex(targetFormIndex);
}
}
const gotoFormByFormId = (formId) => {
setDesiredFormId(formId);
};
const trackSubmitClick = () => {
trackNavigation({
userEvents,
actionType: "submit",
label: "submit",
additionalTrackingParams: navigationTrackingParams
});
};
const handleNextClick = async (updatedForms) => {
const currentForms = updatedForms ?? forms;
if (isFormSummaryStep(activeForm)) {
trackSubmitClick();
onSubmit();
return;
}
/**
* Returns validation result of async validators if any
*/
const isAsyncValidationValid = await triggerValidation?.(activeForm.formId) ?? true;
/**
* Checks if both static validations and asyncValidations pass
*/
if (!activeForm.isValid || !isAsyncValidationValid) {
/**
* shouldValidate is not required with useMultiForm
*/
setShouldValidate(true);
trackNavigation({
userEvents,
actionType: "next",
label: "next",
returnValue: "validation error",
additionalTrackingParams: navigationTrackingParams
});
return;
}
if (isFinalStep) {
trackSubmitClick();
onSubmit();
return;
}
setShouldValidate(false);
const toFormIndex = currentStep + 1;
gotoFormByFormIndex(toFormIndex, currentForms);
trackNavigation({
userEvents,
actionType: "next",
toForm: currentForms[toFormIndex]?.formName,
label: "next",
returnValue: "success",
additionalTrackingParams: navigationTrackingParams
});
};
const handleBackClick = () => {
if (currentStep > 0) {
const toForm = forms[currentStep - 1];
setActiveForm(toForm);
trackNavigation({
userEvents,
actionType: "back",
toForm: toForm?.formName,
label: "back",
additionalTrackingParams: navigationTrackingParams
});
}
};
return {
handleBackClick: isFirstStep ? externalBackClick : handleBackClick,
handleNextClick,
gotoFormByFormIndex,
gotoFormByFormId,
activeForm,
shouldValidate,
setShouldValidate,
steps: {
current: currentStep,
total: totalSteps
}
};
};
//#endregion
//#region src/context/FormRouterContext/FormRouterContextProvider.tsx
var logger = createLogger();
function FormRouterContextProvider({ children, forms, setFormIndex, goToFormByFormId }) {
const fieldActionsRef = useRef({});
const registerFieldAction = useCallback((fieldName, action) => {
fieldActionsRef.current[fieldName] = action;
}, []);
const unregisterFieldAction = useCallback((fieldName) => {
delete fieldActionsRef.current[fieldName];
}, []);
const getFieldAction = useCallback((fieldName) => {
return fieldActionsRef.current[fieldName];
}, []);
const contextValue = useMemo(() => ({
setFormIndex,
goToFormByFormId,
goToFormByFieldName: (fieldName) => {
if (fieldName === "idDocument") fieldName = "idVerificationMethod";
const formIndex = forms.findIndex((form) => form?.fields?.some((field) => field === fieldName));
if (formIndex > -1) setFormIndex(formIndex);
else logger.error("No form was found to have that field so form navigation failed.");
},
registerFieldAction,
unregisterFieldAction,
getFieldAction
}), [
setFormIndex,
goToFormByFormId,
registerFieldAction,
unregisterFieldAction,
getFieldAction,
forms
]);
return /* @__PURE__ */ jsx(FormRouterContext.Provider, {
value: contextValue,
children
});
}
//#endregion
//#region src/utils/dropinUtils.ts
/**
* @@description Based requiredfields and optionalFields determine what forms need to be shown and add a summaryStep in the dropin
*
* @param forms
* @param requiredFields
* @param optionalFields
* @param skipSummary
*/
var getRequiredForms = (forms, requiredFields, optionalFields, skipSummary) => {
const reasons = [];
const requiredForms = Object.values(forms).filter(({ formId }) => {
if (requiredFields || optionalFields) {
const hasRequired = Boolean(requiredFields?.[formId]?.length);
const hasOptional = Boolean(optionalFields?.[formId]?.length);
const isIncluded = hasRequired || hasOptional;
const inclusionReasons = [];
if (hasRequired) inclusionReasons.push("has required fields");
if (hasOptional) inclusionReasons.push("has optional fields");
if (isIncluded) reasons.push(`Form '${formId}': INCLUDED (${inclusionReasons.join(" & ")})`);
else reasons.push(`Form '${formId}': EXCLUDED (no required or optional fields)`);
return isIncluded;
}
reasons.push(`Form '${formId}': INCLUDED (no required/optional fields check)`);
return true;
});
formDebugInfo.value = {
...formDebugInfo.value,
__FORMS__: {
status: "INFO",
reasons
}
};
if (skipSummary) return requiredForms;
return [...requiredForms, summaryStep];
};
/**
* @description Based on validity of fields from formValidity and existence of validationErrors/Verification errors from backend determine whether the forms needs to be shown as valid or not
*
* @param forms
* @param formValidity
* @param problems
*/
var addValidityToForms = (forms, formValidity, problems) => forms.map(({ formId, formName, fields }) => ({
formId,
formName,
fields,
isValid: formValidity?.[formId] ?? false,
hasServerValidationErrors: Boolean(problems?.validationErrors?.[formId])
}));
//#endregion
export { useFormComposer as i, getRequiredForms as n, FormRouterContextProvider as r, addValidityToForms as t };