diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 45bc0c956..506c0d81b 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -2,7 +2,7 @@ "dependencies": { "@fontsource/roboto": "^5.0.8", "@heroicons/react": "^2.1.3", - "@hookform/resolvers": "^3.4.2", + "@hookform/resolvers": "^4.1.3", "@monerium/sdk": "^3.4.2", "@pendulum-chain/api": "catalog:", "@pendulum-chain/api-solang": "catalog:", @@ -72,8 +72,7 @@ "wagmi": "catalog:", "web3": "^4.16.0", "xstate": "^5.20.1", - "yup": "^1.4.0", - "zod": "3", + "zod": "^4.3.6", "zustand": "^5.0.2" }, "devDependencies": { diff --git a/apps/frontend/src/assets/business-check-business-success.svg b/apps/frontend/src/assets/business-check-business-success.svg new file mode 100644 index 000000000..9cdd15d95 --- /dev/null +++ b/apps/frontend/src/assets/business-check-business-success.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/business-check-representative-success.svg b/apps/frontend/src/assets/business-check-representative-success.svg new file mode 100644 index 000000000..72554cf26 --- /dev/null +++ b/apps/frontend/src/assets/business-check-representative-success.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/liveness-check-success.svg b/apps/frontend/src/assets/liveness-check-success.svg new file mode 100644 index 000000000..cdf5310b3 --- /dev/null +++ b/apps/frontend/src/assets/liveness-check-success.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/liveness-check.svg b/apps/frontend/src/assets/liveness-check.svg index 03789489e..f6dd719c7 100644 --- a/apps/frontend/src/assets/liveness-check.svg +++ b/apps/frontend/src/assets/liveness-check.svg @@ -12,9 +12,9 @@ + transform="translate(736 161.332)" fill="#0049c1"/> + transform="translate(736 161.332)" fill="#0049c1"/> + transform="matrix(0.914, -0.407, 0.407, 0.914, 356.801, 457.531)" fill="#0049c1"/> + transform="matrix(0.914, -0.407, 0.407, 0.914, 1026.098, 714.082)" fill="#0049c1"/> diff --git a/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompany.tsx b/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompany.tsx index 7320451e9..6a2b1d5a2 100644 --- a/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompany.tsx +++ b/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompany.tsx @@ -1,4 +1,5 @@ import BusinessCheck from "../../../assets/business-check-business.svg"; +import BusinessCheckSuccess from "../../../assets/business-check-business-success.svg"; import { useAveniaKycActor, useAveniaKycSelector } from "../../../contexts/rampState"; import { AveniaKYBVerifyStep } from "./AveniaKYBVerifyStep"; @@ -15,6 +16,7 @@ export const AveniaKYBVerifyCompany = () => { return ( aveniaKycActor.send({ type: "GO_BACK" })} onVerificationDone={() => aveniaKycActor.send({ type: "KYB_COMPANY_DONE" })} diff --git a/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompanyRepresentative.tsx b/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompanyRepresentative.tsx index ee276e448..46a5eea25 100644 --- a/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompanyRepresentative.tsx +++ b/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompanyRepresentative.tsx @@ -1,4 +1,5 @@ import BusinessCheckRepresentative from "../../../assets/business-check-representative.svg"; +import BusinessCheckRepresentativeSuccess from "../../../assets/business-check-representative-success.svg"; import { useAveniaKycActor, useAveniaKycSelector } from "../../../contexts/rampState"; import { AveniaKYBVerifyStep } from "./AveniaKYBVerifyStep"; @@ -15,6 +16,7 @@ export const AveniaKYBVerifyCompanyRepresentative = () => { return ( aveniaKycActor.send({ type: "KYB_COMPANY_BACK" })} diff --git a/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyStep.tsx b/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyStep.tsx index a5be09f5f..9bcd458fd 100644 --- a/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyStep.tsx +++ b/apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyStep.tsx @@ -1,11 +1,13 @@ -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/solid"; +import { ArrowTopRightOnSquareIcon, ShieldCheckIcon } from "@heroicons/react/24/solid"; import { Trans, useTranslation } from "react-i18next"; -import { useQuote } from "../../../stores/quote/useQuoteStore"; +import { cn } from "../../../helpers/cn"; +import { SparkleButton } from "../../SparkleButton"; import { StepFooter } from "../../StepFooter"; interface AveniaKYBVerifyStepProps { titleKey: string; imageSrc: string; + imageSrcVerified?: string; verificationUrl: string; isVerificationStarted: boolean; onCancel: () => void; @@ -19,6 +21,7 @@ interface AveniaKYBVerifyStepProps { export const AveniaKYBVerifyStep = ({ titleKey, imageSrc, + imageSrcVerified, verificationUrl, isVerificationStarted, onCancel, @@ -35,12 +38,19 @@ export const AveniaKYBVerifyStep = ({
-

{t(titleKey)}

+

+ {t(titleKey)} +

Business Check {!isVerificationStarted && ( @@ -79,15 +89,17 @@ export const AveniaKYBVerifyStep = ({
-
- {isVerificationStarted ? ( - + } + label={t("components.aveniaKYB.buttons.iHaveVerified")} + onClick={onVerificationDone} + /> ) : ( { if (!aveniaState) return null; - if (aveniaState.context.kybStep === "company") { - return ; - } else if (aveniaState.context.kybStep === "representative") { - return ; + let content; + if (aveniaState.context.kybStep === "representative") { + content = ; } else if (aveniaState.context.kybStep === "verification") { - return ; + content = ; + } else { + content = ; } - return ; + return ( +
+ + {content} +
+ ); }; diff --git a/apps/frontend/src/components/Avenia/AveniaKYBForm.tsx b/apps/frontend/src/components/Avenia/AveniaKYBForm.tsx index e40a3118e..ed90ea3a4 100644 --- a/apps/frontend/src/components/Avenia/AveniaKYBForm.tsx +++ b/apps/frontend/src/components/Avenia/AveniaKYBForm.tsx @@ -1,28 +1,22 @@ -import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useAveniaKycActor, useAveniaKycSelector } from "../../contexts/rampState"; -import { useKYCForm } from "../../hooks/brla/useKYCForm"; -import { QuoteSummary } from "../QuoteSummary"; +import { useKYBForm } from "../../hooks/brla/useKYBForm"; +import { MenuButtons } from "../MenuButtons"; import { AveniaFieldProps, ExtendedAveniaFieldOptions } from "./AveniaField"; import { AveniaVerificationForm } from "./AveniaVerificationForm"; -/** - * AveniaKYBForm - A simplified KYC form for companies (CNPJ) - * Only collects the company name - */ export const AveniaKYBForm = () => { const aveniaKycActor = useAveniaKycActor(); const aveniaState = useAveniaKycSelector(); const { t } = useTranslation(); - const { kycForm } = useKYCForm({ cpfApiError: null, initialData: aveniaState?.context.kycFormData }); - - useEffect(() => { - if (aveniaState?.context.taxId) { - kycForm.setValue(ExtendedAveniaFieldOptions.TAX_ID, aveniaState.context.taxId); + const { kybForm } = useKYBForm({ + initialData: { + fullName: aveniaState?.context.kycFormData?.fullName, + taxId: aveniaState?.context.taxId } - }, [aveniaState?.context.taxId, kycForm]); + }); if (!aveniaState) return null; if (!aveniaKycActor) return null; @@ -41,7 +35,7 @@ export const AveniaKYBForm = () => { }, { id: ExtendedAveniaFieldOptions.TAX_ID, - index: 2, + index: 1, label: "CNPJ", placeholder: "", readOnly: true, @@ -52,10 +46,15 @@ export const AveniaKYBForm = () => { return (
-
- -
- + + { + aveniaKycActor.send({ formData: data, type: "FORM_SUBMIT" }); + }} + />
); }; diff --git a/apps/frontend/src/components/Avenia/AveniaKYCForm.tsx b/apps/frontend/src/components/Avenia/AveniaKYCForm.tsx index 4d02236d7..76544f1df 100644 --- a/apps/frontend/src/components/Avenia/AveniaKYCForm.tsx +++ b/apps/frontend/src/components/Avenia/AveniaKYCForm.tsx @@ -2,20 +2,63 @@ import { isValidCnpj } from "@vortexfi/shared"; import { useTranslation } from "react-i18next"; import { useAveniaKycActor, useAveniaKycSelector } from "../../contexts/rampState"; import { useKYCForm } from "../../hooks/brla/useKYCForm"; -import { QuoteSummary } from "../QuoteSummary"; -import { StepBackButton } from "../StepBackButton"; +import { AveniaKycActorRef, SelectedAveniaData } from "../../machines/types"; +import { MenuButtons } from "../MenuButtons"; import { AveniaLivenessStep } from "../widget-steps/AveniaLivenessStep"; import { AveniaFieldProps, ExtendedAveniaFieldOptions } from "./AveniaField"; import { AveniaVerificationForm } from "./AveniaVerificationForm"; import { DocumentUpload } from "./DocumentUpload"; import { VerificationStatus } from "./VerificationStatus"; +interface AveniaKYCContentProps { + aveniaKycActor: AveniaKycActorRef; + aveniaState: SelectedAveniaData; + fields: AveniaFieldProps[]; +} + +const AveniaKYCFormStep = ({ aveniaKycActor, aveniaState, fields }: AveniaKYCContentProps) => { + const { kycForm } = useKYCForm({ cpfApiError: null, initialData: aveniaState.context.kycFormData }); + return ( + { + aveniaKycActor.send({ formData: data, type: "FORM_SUBMIT" }); + }} + /> + ); +}; + +const AveniaKYCContent = ({ aveniaKycActor, aveniaState, fields }: AveniaKYCContentProps) => { + const { stateValue } = aveniaState; + + if ( + stateValue === "Verifying" || + stateValue === "Submit" || + stateValue === "Success" || + stateValue === "Rejected" || + stateValue === "Failure" + ) { + return ; + } + + if (stateValue === "DocumentUpload") { + return ; + } + + if (stateValue === "LivenessCheck" || stateValue === "RefreshingLivenessUrl") { + return ; + } + + return ; +}; + export const AveniaKYCForm = () => { const aveniaKycActor = useAveniaKycActor(); const aveniaState = useAveniaKycSelector(); const { t } = useTranslation(); - const { kycForm } = useKYCForm({ cpfApiError: null, initialData: aveniaState?.context.kycFormData }); if (!aveniaState) return null; if (!aveniaKycActor) return null; @@ -23,7 +66,7 @@ export const AveniaKYCForm = () => { return null; } - const pixformFields: AveniaFieldProps[] = [ + const fields: AveniaFieldProps[] = [ { id: ExtendedAveniaFieldOptions.FULL_NAME, index: 0, @@ -99,7 +142,7 @@ export const AveniaKYCForm = () => { ]; if (isValidCnpj(aveniaState.context.taxId)) { - pixformFields.push({ + fields.push({ id: ExtendedAveniaFieldOptions.COMPANY_NAME, index: 10, label: t("components.brlaExtendedForm.form.companyName"), @@ -107,14 +150,14 @@ export const AveniaKYCForm = () => { required: true, type: "text" }); - pixformFields.push({ + fields.push({ id: ExtendedAveniaFieldOptions.START_DATE, index: 11, label: t("components.brlaExtendedForm.form.startDate"), required: true, type: "date" }); - pixformFields.push({ + fields.push({ id: ExtendedAveniaFieldOptions.PARTNER_CPF, index: 12, label: t("components.brlaExtendedForm.form.partnerCpf"), @@ -123,36 +166,10 @@ export const AveniaKYCForm = () => { }); } - let content; - if ( - aveniaState.stateValue === "Verifying" || - aveniaState.stateValue === "Submit" || - aveniaState.stateValue === "Success" || - aveniaState.stateValue === "Rejected" || - aveniaState.stateValue === "Failure" - ) { - content = ; - } else if (aveniaState.stateValue === "DocumentUpload") { - content = ; - } else if (aveniaState.stateValue === "LivenessCheck" || aveniaState.stateValue === "RefreshingLivenessUrl") { - content = ; - } else { - content = ( - - ); - } - return (
-
-
-
- -
- {content} -
-
- + +
); }; diff --git a/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx b/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx index 3402ae8ef..3e2fd1ded 100644 --- a/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx +++ b/apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx @@ -1,43 +1,34 @@ -import { motion } from "motion/react"; -import { FormProvider, UseFormReturn } from "react-hook-form"; +import { FieldValues, FormProvider, SubmitHandler, UseFormReturn } from "react-hook-form"; import { Trans, useTranslation } from "react-i18next"; -import { KYCFormData } from "../../../hooks/brla/useKYCForm"; import { useMaintenanceAwareButton } from "../../../hooks/useMaintenanceAware"; -import { AveniaKycActorRef } from "../../../machines/types"; import { StepFooter } from "../../StepFooter"; import { AveniaField, AveniaFieldProps, ExtendedAveniaFieldOptions } from "../AveniaField"; -interface AveniaVerificationFormProps { +interface AveniaVerificationFormProps { fields: AveniaFieldProps[]; - form: UseFormReturn; - aveniaKycActor: AveniaKycActorRef; + form: UseFormReturn; + onSubmit: SubmitHandler; isCompany?: boolean; } -export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany = false }: AveniaVerificationFormProps) => { +export const AveniaVerificationForm = ({ + form, + fields, + onSubmit, + isCompany = false +}: AveniaVerificationFormProps) => { const { handleSubmit } = form; const { t } = useTranslation(); const { buttonProps, isMaintenanceDisabled } = useMaintenanceAwareButton(); - const onSubmit = () => { - const formData = form.getValues(); - aveniaKycActor.send({ formData, type: "FORM_SUBMIT" }); - }; - // formState.isValid is not working as expected, so we need to check the errors const isFormInvalid = Object.keys(form.formState.errors).length > 0 || form.formState.isSubmitting; return ( - +

{isCompany ? t("components.aveniaKYB.title.default") : t("components.aveniaKYC.title")} @@ -50,7 +41,8 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany ExtendedAveniaFieldOptions.PIX_ID, ExtendedAveniaFieldOptions.TAX_ID, ExtendedAveniaFieldOptions.FULL_NAME, - ExtendedAveniaFieldOptions.COMPANY_NAME + ExtendedAveniaFieldOptions.COMPANY_NAME, + ExtendedAveniaFieldOptions.EMAIL ].includes(field.id as ExtendedAveniaFieldOptions) ? "col-span-2" : "" @@ -69,7 +61,7 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany i18nKey={"components.aveniaKYC.description"} > Complete these quick identity checks (typically 90 seconds). Data is processed securely by{" "} - + Avenia {" "} using bank-grade encryption for transaction security. @@ -91,7 +83,7 @@ export const AveniaVerificationForm = ({ form, fields, aveniaKycActor, isCompany : t("components.aveniaKYC.buttons.next")} - + ); }; diff --git a/apps/frontend/src/components/Avenia/DocumentUpload/index.tsx b/apps/frontend/src/components/Avenia/DocumentUpload/index.tsx index 64692de17..9d220ea3f 100644 --- a/apps/frontend/src/components/Avenia/DocumentUpload/index.tsx +++ b/apps/frontend/src/components/Avenia/DocumentUpload/index.tsx @@ -5,6 +5,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { durations, easings } from "../../../constants/animations"; +import { cn } from "../../../helpers/cn"; import { useMaintenanceAwareButton } from "../../../hooks/useMaintenanceAware"; import { AveniaKycActorRef } from "../../../machines/types"; import { BrlaService } from "../../../services/api"; @@ -156,7 +157,12 @@ export const DocumentUpload: React.FC = ({ aveniaKycActor, Icon: React.ComponentType>, fileName?: string ) => ( -