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.

511 lines (510 loc) 23.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] = "36528c2a-d11d-4fd8-aec5-54c855fecedd", e._sentryDebugIdIdentifier = "sentry-dbid-36528c2a-d11d-4fd8-aec5-54c855fecedd"); } catch (e) {} import { i as Typography, o as createLogger, r as useTranslation } from "./translation-BYvhW5zA.js"; import { T as datasetUtilities, j as useToggleContext, x as Alert } from "./resolveEnvironment-DNmu53Rr.js"; import { r as Loader, t as Button } from "./Button-i8I2dHP8.js"; import { n as httpGet, r as httpPost, s as useApiContext } from "./http-8qgzqVqk.js"; import { r as useLegalEntity } from "./useLegalEntity-CtlUQv_J.js"; import { t as LegalEntityTypes } from "./legal-entity-type-0MfCGzUf.js"; import { t as getLegalEntityCountry } from "./getLegalEntityCountry-BHh7Ux8v.js"; import { a as noop, c as entriesOf, i as drop, n as cloneObject } from "./AnalyticsContext-BFrJmsp0.js"; import { t as StackLayout } from "./StackLayout-IUdNMDgW.js"; import { t as TaskVerificationStatus } from "./TaskVerificationStatus-CKHPTEpH.js"; import { i as TaskStatuses } from "./taskStatus-BtekPA8E.js"; import { t as TaskTypes } from "./taskTypes-NawDTp5n.js"; import { t as DecisionMakerTypes } from "./decision-maker-type-NR4GFAeT.js"; import { i as useAnalyticsContext, t as Modal } from "./Modal-BLP2aF-u.js"; import { n as useToastContext } from "./invalidateRootLegalEntity-DLpPeD5p.js"; import { t as getIdNowMissingDataPoints } from "./bafinUtils-BKq-s1Ka.js"; import { t as Confirm } from "./Confirm-CA-gL38n.js"; import { t as useScenarios } from "./useScenarios-DciPyzQ-.js"; import { i as GuidanceQuestionValues, o as shouldNavigateToAdditionalPersonalDetailsForMissingData, s as useCreateInvitation, t as mapLegalEntityToIndividualSchema } from "./mapLegalEntityToIndividualSchema-7sVfeCn9.js"; import { t as List } from "./List-ywoRPg-q.js"; import { t as StructuredList } from "./StructuredList-CUyo9-3i.js"; import { i as mapAddressLabels } from "./utils-DNIZ6F6L.js"; import { t as localizeDateString } from "./localizeDateString-Bmx3yBcR.js"; import { t as DetailViewLayout } from "./DetailViewLayout-DNCGEd7b.js"; import { t as ListItem } from "./ListItem-V4x0raZw.js"; import { t as getTrustedDecisionMakers } from "./getTrustedDecisionMakers-DUpv5ug2.js"; import { useMemo, useState } from "preact/hooks"; import { Fragment, jsx, jsxs } from "preact/jsx-runtime"; import { skipToken, 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) acc[data.decisionMakerId] = data; 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/utils/analytics/trackClickHandler.ts /** * Creates a click handler that automatically logs an analytics event before executing the provided handler function. * * @param userEvents - The UserEvents instance from the analytics context. * @param label - The label describing the button or element being clicked. * @param action - The type of action performed. * @param handler - The actual function to execute when the click occurs. * @returns A higher-order function that handles the click, logs the event, and calls the original handler. */ var trackClickHandler = (userEvents, label, action, handler) => () => { userEvents.addPageEvent("Clicked button", { actionType: action, label }); handler(); }; //#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, isInvitedEntity }) => { const userEvents = useAnalyticsContext(); 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 { data: scenarios } = useScenarios(parentLegalEntity ? getLegalEntityCountry(parentLegalEntity) : skipToken, LegalEntityTypes.INDIVIDUAL); const isTrusted = parentLegalEntity ? getTrustedDecisionMakers(parentLegalEntity).some((ea) => ea.reference === decisionMaker.reference) : false; const canEdit = !(invitationStatus?.active === true) || isInvitedEntity; const idNowMissingDataPoints = legalEntity && !isLegalEntityLoading ? getIdNowMissingDataPoints(decisionMaker.problems, legalEntity, scenarios, decisionMaker.types) : null; const formattedSummary = useMemo(() => { if (legalEntity) return getSummaryData(mapLegalEntityToIndividualSchema(legalEntity, false, true, parentLegalEntity), i18n.language, commonT, TaskTypes.DECISION_MAKER); }, [ legalEntity, parentLegalEntity, i18n, commonT ]); const handleSendReminder = trackClickHandler(userEvents, "sendReminder", "select", async () => { 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: { reminder: true, 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) }); }); const handleShareInvite = trackClickHandler(userEvents, "shareInvite", "select", async () => { 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 handleMissingDataEdit = () => { shouldNavigateToAdditionalPersonalDetailsForMissingData.value = true; handleEdit(); }; const handleEditClick = trackClickHandler(userEvents, "editDetails", "edit", handleEdit); const handleRevokeInviteClick = trackClickHandler(userEvents, "revokeInvite", "select", () => setConfirmRevokeInviteOpen(true)); const handleDeleteClick = trackClickHandler(userEvents, "delete", "remove", () => setConfirmDeleteOpen(true)); const actions = /* @__PURE__ */ jsxs(Fragment, { children: [canEdit ? /* @__PURE__ */ jsx(Button, { onClick: handleEditClick, variant: "primary", fullWidth: true, children: commonT(($) => $["editDetails"]) }) : /* @__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: handleRevokeInviteClick, variant: "secondary", fullWidth: true, children: individualT(($) => $.revokeInvite) }) ] }), handleDelete && /* @__PURE__ */ jsx(Button, { onClick: handleDeleteClick, 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: [ idNowMissingDataPoints && /* @__PURE__ */ jsx(Alert, { variant: "warning", title: individualT(($) => $["idNowMissingFields"], { fields: [idNowMissingDataPoints.missingPlaceOfBirth && commonT(($) => $["placeOfBirth"]), idNowMissingDataPoints.missingNationality && commonT(($) => $["nationality"])].filter(Boolean).join(` ${commonT(($) => $["and"])} `) }), children: /* @__PURE__ */ jsx(Button, { variant: "link", onClick: handleMissingDataEdit, children: commonT(($) => $["addDetails"]) }) }), /* @__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 };