@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.
475 lines (474 loc) • 21.3 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] = "681bba30-f665-4021-958c-a93c55a5f59c", e._sentryDebugIdIdentifier = "sentry-dbid-681bba30-f665-4021-958c-a93c55a5f59c");
} catch (e) {}
import { i as Typography, o as createLogger, r as useTranslation } from "./translation-BFxyJ1c5.js";
import { r as Loader, t as Button } from "./Button-oj6H8OrC.js";
import { n as httpGet, r as httpPost, s as useApiContext } from "./http-D1NDkBxF.js";
import { r as useLegalEntity } from "./useLegalEntity-yxi9XhLi.js";
import { a as drop, r as cloneObject, s as noop, u as entriesOf } from "./useAnalyticsContext-BVFDMrVE.js";
import { t as useToggleContext } from "./useToggleContext-DaQUBF8O.js";
import { t as StackLayout } from "./StackLayout-Bhbj68nx.js";
import { t as TaskVerificationStatus } from "./TaskVerificationStatus-kCWYhocq.js";
import { r as TaskStatuses } from "./taskStatus-C7XU4UIF.js";
import { n as datasetUtilities } from "./datasetUtil-Zd4TCTDn.js";
import { p as TaskTypes } from "./entityAssociationUtil-BEzUdPbm.js";
import { t as DecisionMakerTypes } from "./decision-maker-type-Cow3CpUd.js";
import { t as useToastContext } from "./useToastContext-CYgfHjSb.js";
import { t as Modal } from "./Modal-CioQJ7Q7.js";
import { t as Confirm } from "./Confirm-B6TWSuab.js";
import { t as getTrustedEntityAssociations } from "./getTrustedFields-UVTLadau.js";
import { i as GuidanceQuestionValues, o as useCreateInvitation, t as mapLegalEntityToIndividualSchema } from "./mapLegalEntityToIndividualSchema-D9JCcC77.js";
import { t as List } from "./List-DLrcpMVd.js";
import { t as StructuredList } from "./StructuredList-w0Z2zLTk.js";
import { i as mapAddressLabels } from "./utils-CfTVU2Hq.js";
import { t as localizeDateString } from "./localizeDateString-1A3eC6hp.js";
import { t as DetailViewLayout } from "./DetailViewLayout-Dd5YzmcW.js";
import { t as ListItem } from "./ListItem-BWH4IPrj.js";
import { useMemo, useState } from "preact/hooks";
import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
import { useMutation, useQueries, useQuery } from "@tanstack/preact-query";
//#region src/api/invitations/useInvitationStatus.ts
async function getInvitationStatus(rootLegalEntityId, baseUrl, decisionMakerId) {
const { active } = await httpGet({
baseUrl,
path: `legalEntities/${rootLegalEntityId}/invitations/associations/${decisionMakerId}/status`
});
return {
decisionMakerId,
active
};
}
function useInvitationStatus(decisionMakerId, options) {
const { rootLegalEntityId, baseUrl } = useApiContext();
return useQuery({
queryKey: ["invitationStatus", decisionMakerId],
queryFn: () => getInvitationStatus(rootLegalEntityId.value, baseUrl.value, decisionMakerId),
...options
});
}
function useInvitationStatuses(decisionMakerIds, options) {
const { rootLegalEntityId, baseUrl } = useApiContext();
return useQueries({
queries: decisionMakerIds.map((id) => ({
queryKey: ["invitationStatus", id],
queryFn: () => getInvitationStatus(rootLegalEntityId.value, baseUrl.value, id),
initialData: {
decisionMakerId: id,
active: false
},
staleTime: 0,
...options
})),
combine: (results) => results.reduce((acc, { data }) => {
if (data) {
const { decisionMakerId, active } = data;
acc[decisionMakerId] = active ?? false;
}
return acc;
}, {})
});
}
//#endregion
//#region src/api/invitations/useWithdrawInvitation.ts
async function withdrawInvitation(baseUrl, organizationLegalEntityId, inviteeLegalEntityId) {
return httpPost({
baseUrl,
path: `legalEntities/${organizationLegalEntityId}/invitations/associations/${inviteeLegalEntityId}/withdraw`
});
}
var useWithdrawInvitation = (options) => {
const { baseUrl } = useApiContext();
return useMutation({
mutationFn: ({ organizationLegalEntityId, inviteeLegalEntityId }) => {
return withdrawInvitation(baseUrl.value, organizationLegalEntityId, inviteeLegalEntityId);
},
...options
});
};
//#endregion
//#region src/components/Individual/tasks/DecisionMakers/hooks/useShareInvitation.ts
function useShareInvitation() {
const { t } = useTranslation("individual");
const { showToast } = useToastContext();
return async function shareInvitation({ inviteeName, url }) {
if (!navigator.share) {
await navigator.clipboard.writeText(url);
showToast({
label: t(($) => $.linkCopied),
variant: "info"
});
return;
}
await navigator.share({
title: t(($) => $.verifyYourDetails),
text: t(($) => $.verifyYourDetailsMessage, { inviteeName }),
url
});
};
}
//#endregion
//#region src/components/Individual/tasks/DecisionMakers/utils.ts
var BENEFICIAL_OWNER_ROLES = [
DecisionMakerTypes.OWNER,
DecisionMakerTypes.CONTROLLING_PERSON,
DecisionMakerTypes.DIRECTOR
];
var getDecisionMakerDescription = (types, t) => {
const isSignatory = types.includes(DecisionMakerTypes.SIGNATORY);
const isBeneficialOwner = types.some((type) => BENEFICIAL_OWNER_ROLES.includes(type));
return [isSignatory && t(($) => $.signatory), isBeneficialOwner && t(($) => $.beneficialOwner)].filter(Boolean).join(", ");
};
/**
* Maps the current task submission status for invited decision maker to the appropriate landing layout properties,
* returning translated titles, descriptions, and content based on the state.
*/
var getLandingLayoutProps = (submitStatus, isErrorOrDetailsRequired, t) => {
if (submitStatus === TaskStatuses.PROCESSING) return {
title: t(($) => $["thankYou"]),
description: t(($) => $["weWillReviewYourDetails"])
};
if (isErrorOrDetailsRequired) return {
title: t(($) => $["weNeedSomeAdditionalDetails"]),
description: t(($) => $["weHaveReviewedTheDetailsYouSubmitted"])
};
return {
title: t(($) => $["yourDetailsHaveBeenVerified"]),
description: t(($) => $["thankYouForYourAssistance"]),
content: t(($) => $["youCanSafelyCloseThisPage"])
};
};
//#endregion
//#region src/components/Individual/tasks/DecisionMakers/DecisionMakerDetails/utils.ts
/**
* Checks if the ID verification method is either 'manualVerification' or 'existingDocument'.
* This is a helper function to determine if the user has opted for a manual upload process
* rather than an automated one like Onfido's SDK.
*
* @param idVerificationMethod - The selected ID verification method.
* @returns `true` if the method is 'manualVerification' or 'existingDocument', otherwise `false`.
*
* @example
* isManualVerificationOrExistingDoc('manualVerification'); // true
* isManualVerificationOrExistingDoc('instantVerification'); // false
*/
var isManualVerificationOrExistingDoc = (idVerificationMethod) => idVerificationMethod === "manualVerification" || idVerificationMethod === "existingDocument";
/**
* Aggregates and formats file summary data from different parts of the form.
* This function is responsible for preparing the file-related information for the summary view.
*
* @param data - The complete form data.
* @param locale
* @returns An object containing formatted file information.
*/
var formatFileSummaryData = (data, locale) => {
const { idVerificationMethod, proofOfNationalId, proofOfResidency, proofOfRelationship } = data;
const proofOfNationalIdFile = proofOfNationalId?.proofOfNationalId?.[0];
const proofOfResidencyFile = proofOfResidency?.proofOfResidency?.[0];
const proofOfRelationshipFile = proofOfRelationship?.proofOfRelationship?.[0];
const idDocument = {
idDocumentType: idVerificationMethod?.idDocument?.idDocumentType,
uploadedDate: idVerificationMethod?.idDocument?.modificationDate ? localizeDateString(idVerificationMethod?.idDocument?.modificationDate ?? "", locale) : void 0
};
return {
...idDocument?.idDocumentType && { idDocument },
...proofOfNationalIdFile && { proofOfNationalId: { fileName: proofOfNationalIdFile.name } },
...proofOfResidencyFile && { proofOfResidency: { fileName: proofOfResidencyFile.name } },
...proofOfRelationshipFile && { proofOfRelationship: { fileName: proofOfRelationshipFile.name } }
};
};
/**
* Formats the entire form data for the summary view.
* This function transforms raw form data into a user-friendly format, including localization and data mapping.
*
* @param dataToFormat - The raw form data.
* @param locale - language string
* @param commonT - The translation function.
* @returns A new object with the formatted data for the summary.
*/
var formatDataForSummary = (dataToFormat, locale, commonT) => {
if (!dataToFormat) return {};
const datasetUtils = datasetUtilities(locale);
let formattedSummaryData = cloneObject(dataToFormat);
if (formattedSummaryData?.basicDetails?.phoneNumber) formattedSummaryData.basicDetails.phoneNumber = formattedSummaryData.basicDetails.phoneNumber.number;
if (formattedSummaryData.additionalPersonalDetails?.country) formattedSummaryData.additionalPersonalDetails.country = datasetUtils.getCountryName(formattedSummaryData.additionalPersonalDetails.country);
if (formattedSummaryData.additionalPersonalDetails?.nationality) formattedSummaryData.additionalPersonalDetails.nationality = datasetUtils.getCountryName(formattedSummaryData.additionalPersonalDetails.nationality);
if (formattedSummaryData.additionalPersonalDetails) formattedSummaryData.address = mapAddressLabels(formattedSummaryData.additionalPersonalDetails, datasetUtils);
if (formattedSummaryData.basicDetails?.accountHolder) formattedSummaryData.basicDetails.accountHolder = commonT(($) => $[formattedSummaryData.basicDetails.accountHolder]);
if (formattedSummaryData.additionalPersonalDetails?.birthDate) formattedSummaryData.additionalPersonalDetails.birthDate = localizeDateString(formattedSummaryData.additionalPersonalDetails.birthDate, locale);
if (isManualVerificationOrExistingDoc(formattedSummaryData.idVerificationMethod?.idVerificationMethod) || formattedSummaryData.proofOfResidency) {
const fileSummaryData = formatFileSummaryData(dataToFormat, locale);
formattedSummaryData = {
...formattedSummaryData,
...fileSummaryData
};
formattedSummaryData.manualIdUpload = { ...formattedSummaryData.idDocument };
formattedSummaryData.idVerificationMethod.idDocument = { ...formattedSummaryData.idDocument };
}
if (formattedSummaryData.additionalPersonalDetails?.taxInformation) {
const taxInformationByNationality = formattedSummaryData.additionalPersonalDetails.taxInformation.find((taxId) => taxId.country === dataToFormat.additionalPersonalDetails?.nationality);
if (taxInformationByNationality) {
delete formattedSummaryData.additionalPersonalDetails.taxInformation;
formattedSummaryData.additionalPersonalDetails[taxInformationByNationality.type] = taxInformationByNationality.number;
}
if (!taxInformationByNationality) {
const taxInformationByCountry = formattedSummaryData.additionalPersonalDetails.taxInformation.find((taxId) => taxId.country === dataToFormat.additionalPersonalDetails?.country);
if (taxInformationByCountry) {
delete formattedSummaryData.additionalPersonalDetails.taxInformation;
formattedSummaryData.additionalPersonalDetails[taxInformationByCountry.type] = taxInformationByCountry.number;
}
}
}
if (formattedSummaryData?.signatoryQuestionnaire) formattedSummaryData.signatoryQuestionnaire = dataToFormat?.signatoryQuestionnaire?.isSignatory === GuidanceQuestionValues.YES ? [commonT(($) => $["signatorySummaryLabel"])] : [];
if (formattedSummaryData?.uboQuestionnaire) {
const roleFieldToLabelMap = {
isOwner: "ownerSummaryLabel",
isControllingPerson: "controllingPersonSummaryLabel",
isDirector: "directorSummaryLabel"
};
formattedSummaryData.uboQuestionnaire = Object.keys(roleFieldToLabelMap).flatMap((field) => dataToFormat?.uboQuestionnaire?.[field] === GuidanceQuestionValues.YES ? [commonT(($) => $[roleFieldToLabelMap[field]])] : []);
}
return formattedSummaryData;
};
/**
* Determines which keys to omit from the summary data based on the form data and task type.
*
* @param data - The form data.
* @param taskType - The type of task being performed.
* @returns An array of keys to be omitted from the summary.
*
* @example
* getOmittedKeysForSummary({ idVerificationMethod: { idVerificationMethod: 'manualVerification' } }, 'INDIVIDUAL');
* // Returns: ['idVerificationMethod', 'address']
*/
var getOmittedKeysForSummary = (data, taskType) => [
...taskType === TaskTypes.DECISION_MAKER ? ["role"] : [],
...data?.additionalPersonalDetails?.idNumberExempt ? ["idNumber"] : [],
...isManualVerificationOrExistingDoc(data?.idVerificationMethod?.idVerificationMethod) ? ["idVerificationMethod"] : [],
"address",
"manualIdUpload"
];
/**
* Prepares the final summary data by formatting and filtering the raw form data.
*
* @param data - The raw form data.
* @param locale - language string
* @param commonT - The translation function.
* @param taskType - The type of task being performed.
* @returns The final summary data object.
*/
var getSummaryData = (data, locale, commonT, taskType) => {
const summaryData = formatDataForSummary(data, locale, commonT);
const omittedKeys = getOmittedKeysForSummary(data, taskType)?.filter(Boolean);
return {
...drop(...omittedKeys).from(summaryData),
basicDetails: drop("firstName", "lastName", "role", "nomineeShareholder").from(summaryData.basicDetails),
additionalPersonalDetails: drop("idNumberExempt").from(summaryData.additionalPersonalDetails),
idDocument: summaryData.idVerificationMethod?.idDocument
};
};
/**
* Transforms a record of data into a structured list of items for display.
* Each item in the list has a term (label) and details (value).
*
* @param data - The data record to transform.
* @param commonT - The translation function.
* @returns An array of structured list items.
*
* @example
* const data = { name: 'John Doe', age: 30 };
* getStructuredListItems(data, t);
* // Returns: [{ term: 'Name', details: 'John Doe', type: 'text' }, { term: 'Age', details: '30', type: 'text' }]
*/
var getStructuredListItems = (data, commonT) => {
if (typeof data !== "object" || data === null || Array.isArray(data)) return [];
return Object.entries(data).filter(([itemKey, itemValue]) => itemValue != null && itemValue !== "" && !["nomineeDirector", "nomineeShareholder"].includes(itemKey) && (!Array.isArray(itemValue) || itemValue.length > 0)).map(([itemKey, itemValue]) => ({
term: commonT(($) => $[itemKey]),
details: Array.isArray(itemValue) ? itemValue.join(", ") : commonT(($) => $[itemValue]) || String(itemValue),
type: Array.isArray(itemValue) ? "list" : "text"
}));
};
//#endregion
//#region src/components/Individual/tasks/DecisionMakers/DecisionMakerDetails/DecisionMakerDetails.tsx
var logger = createLogger();
var DecisionMakerDetails = ({ decisionMaker, parentLegalEntity, handleClose, handleDelete, handleEdit }) => {
const { showToast } = useToastContext();
const { t: commonT, i18n } = useTranslation("common");
const { t: individualT } = useTranslation("individual");
const { isFeatureEnabled } = useToggleContext();
const isVerifyByInviteEnabled = isFeatureEnabled("EnableVerifyByInvite");
const { data: invitationStatus, refetch: refetchInvitationStatus } = useInvitationStatus(decisionMaker.reference, { enabled: isVerifyByInviteEnabled });
const { mutateAsync: createInvitation } = useCreateInvitation();
const { mutateAsync: withdrawInvitation } = useWithdrawInvitation();
const shareInvitation = useShareInvitation();
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [confirmRevokeInviteOpen, setConfirmRevokeInviteOpen] = useState(false);
const [reminderSent, setReminderSent] = useState(false);
const { data: legalEntity, isLoading: isLegalEntityLoading } = useLegalEntity(decisionMaker.reference);
const isTrusted = parentLegalEntity ? getTrustedEntityAssociations(parentLegalEntity).some((ea) => ea.reference === decisionMaker.reference) : false;
const invitationActive = invitationStatus?.active === true;
const canEdit = !invitationActive;
const formattedSummary = useMemo(() => {
if (legalEntity) return getSummaryData(mapLegalEntityToIndividualSchema(legalEntity, false, true, parentLegalEntity), i18n.language, commonT, TaskTypes.DECISION_MAKER);
}, [
legalEntity,
parentLegalEntity,
i18n,
commonT
]);
async function handleSendReminder() {
if (!parentLegalEntity?.id) {
logger.warn("Cannot send reminder for decision maker without parent legal entity ID");
return;
}
try {
const { emailSent } = await createInvitation({
organizationLegalEntityId: parentLegalEntity.id,
inviteeLegalEntityId: decisionMaker.reference,
data: {
sendEmail: true,
locale: i18n.language
}
});
if (emailSent) {
showToast({
variant: "success",
label: individualT(($) => $.reminderSent)
});
setReminderSent(true);
return;
}
} catch (err) {
logger.error("Unable to send reminder", err);
}
showToast({
variant: "error",
label: individualT(($) => $.unableToSendReminder)
});
}
async function handleShareInvite() {
if (!parentLegalEntity?.id) {
logger.warn("Cannot share invite for decision maker without parent legal entity ID");
return;
}
const { url } = await createInvitation({
organizationLegalEntityId: parentLegalEntity.id,
inviteeLegalEntityId: decisionMaker.reference,
data: {
sendEmail: false,
locale: i18n.language
}
});
await shareInvitation({
url,
inviteeName: decisionMaker.name
});
}
async function handleRevokeInvite() {
if (!parentLegalEntity?.id) {
logger.warn("Cannot revoke invite for decision maker without parent legal entity ID");
return;
}
try {
await withdrawInvitation({
organizationLegalEntityId: parentLegalEntity.id,
inviteeLegalEntityId: decisionMaker.reference
});
setConfirmRevokeInviteOpen(false);
await refetchInvitationStatus();
} catch (err) {
logger.error("Unable to revoke invite", err);
showToast({
variant: "error",
label: individualT(($) => $.unableToRevokeInvite)
});
}
}
const actions = /* @__PURE__ */ jsxs(Fragment, { children: [
canEdit && /* @__PURE__ */ jsx(Button, {
onClick: handleEdit,
variant: "primary",
fullWidth: true,
children: commonT(($) => $["editDetails"])
}),
invitationActive && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(Button, {
onClick: handleSendReminder,
variant: "secondary",
fullWidth: true,
disabled: reminderSent,
children: individualT(($) => $.sendReminder)
}),
/* @__PURE__ */ jsx(Button, {
onClick: handleShareInvite,
variant: "secondary",
fullWidth: true,
children: individualT(($) => $.shareInvite)
}),
/* @__PURE__ */ jsx(Button, {
onClick: () => setConfirmRevokeInviteOpen(true),
variant: "secondary",
fullWidth: true,
children: individualT(($) => $.revokeInvite)
})
] }),
handleDelete && /* @__PURE__ */ jsx(Button, {
onClick: () => setConfirmDeleteOpen(true),
variant: "secondary",
fullWidth: true,
disabled: isTrusted,
children: commonT(($) => $["delete"])
})
] });
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(Modal, {
onClose: handleClose,
ariaLabel: commonT(($) => $["individualDetails"]),
inset: true,
children: /* @__PURE__ */ jsx(DetailViewLayout, {
title: commonT(($) => $["individualDetails"]),
actions,
children: isLegalEntityLoading || !legalEntity ? /* @__PURE__ */ jsx(Loader, {}) : /* @__PURE__ */ jsxs(StackLayout, {
gap: "medium",
children: [/* @__PURE__ */ jsx(ListItem, {
accessory: /* @__PURE__ */ jsx(TaskVerificationStatus, { status: decisionMaker.status }),
avatar: {
iconName: "user",
circle: true
},
title: decisionMaker.name,
description: getDecisionMakerDescription(decisionMaker.types, individualT)
}), formattedSummary && entriesOf(formattedSummary).map(([key, value]) => {
const items = getStructuredListItems(value, commonT);
if (items.length === 0) return null;
return /* @__PURE__ */ jsxs(List, {
variant: "grouped-secondary",
children: [/* @__PURE__ */ jsx(Typography, {
variant: "title",
children: individualT(($) => $[key])
}), /* @__PURE__ */ jsx(StructuredList, { items })]
}, key);
}).filter(Boolean)]
})
})
}),
confirmDeleteOpen && /* @__PURE__ */ jsx(Confirm, {
title: commonT(($) => $["deleteItem"], { item: decisionMaker.name }),
description: commonT(($) => $["thisActionIsPermanent"]),
onConfirm: handleDelete ?? noop,
confirmText: commonT(($) => $["delete"]),
onCancel: () => setConfirmDeleteOpen(false),
critical: true
}),
confirmRevokeInviteOpen && /* @__PURE__ */ jsx(Confirm, {
title: individualT(($) => $.revokeInviteQuestion),
description: individualT(($) => $.nameWillLoseAccessToSelfVerify, { name: decisionMaker.name }),
onConfirm: handleRevokeInvite,
confirmText: individualT(($) => $.revoke),
onCancel: () => setConfirmRevokeInviteOpen(false),
critical: true
})
] });
};
//#endregion
export { useInvitationStatuses as i, getDecisionMakerDescription as n, getLandingLayoutProps as r, DecisionMakerDetails as t };