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.

475 lines (474 loc) 21.3 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] = "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 };