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)} + {!isVerificationStarted && ( @@ -79,15 +89,17 @@ export const AveniaKYBVerifyStep = ({ - - + + {t(cancelButtonKey)} {isVerificationStarted ? ( - - {t("components.aveniaKYB.buttons.iHaveVerified")} - + } + 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 ) => ( - + {label} {fileName || t("components.documentUpload.helperText")} @@ -170,7 +176,7 @@ export const DocumentUpload: React.FC = ({ aveniaKycActor, initial={{ scale: 0 }} transition={shouldReduceMotion ? { duration: 0 } : { damping: 15, stiffness: 200, type: "spring" }} > - + )} @@ -246,7 +252,7 @@ export const DocumentUpload: React.FC = ({ aveniaKycActor, {error && ( void; + theme?: SparkleButtonTheme; + icon?: React.ReactNode; + className?: string; +} + +const THEME_CONFIG: Record = { + primary: { + buttonClass: "btn-vortex-primary", + sparkleColor: "var(--color-primary)" + }, + success: { + buttonClass: "btn-vortex-success", + sparkleColor: "var(--color-success)" + } +}; + +interface SparkleConfig { + id: number; + x: number; + y: number; + size: number; + delay: number; +} + +let idCounter = 0; + +const generateConfigs = (): SparkleConfig[] => + Array.from({ length: 5 }, () => { + const angle = Math.random() * 360; + const distance = 25 + Math.random() * 30; + return { + delay: Math.random() * 0.08, + id: idCounter++, + size: 3 + Math.random() * 3, + x: Math.cos((angle * Math.PI) / 180) * distance, + y: Math.sin((angle * Math.PI) / 180) * distance + }; + }); + +export const SparkleButton = ({ label, onClick, theme = "success", icon, className }: SparkleButtonProps) => { + const [configs, setConfigs] = useState([]); + const { buttonClass, sparkleColor } = THEME_CONFIG[theme]; + + const burst = useCallback(() => setConfigs(generateConfigs()), []); + + useEffect(() => { + const id = setInterval(burst, 1000); + return () => clearInterval(id); + }, [burst]); + + const handleClick = useCallback(() => { + onClick(); + burst(); + }, [onClick, burst]); + + return ( + + + {icon} + {label} + + {configs.map(s => ( + + ))} + + ); +}; diff --git a/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx b/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx index 325a43ad3..91cafb067 100644 --- a/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx +++ b/apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx @@ -1,14 +1,12 @@ import { useSelector } from "@xstate/react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import * as yup from "yup"; +import { z } from "zod"; import { useRampActor } from "../../../contexts/rampState"; import { cn } from "../../../helpers/cn"; import { MenuButtons } from "../../MenuButtons"; import { StepFooter } from "../../StepFooter"; -const emailSchema = yup.string().email().required(); - export interface AuthEmailStepProps { className?: string; } @@ -30,7 +28,7 @@ export const AuthEmailStep = ({ className }: AuthEmailStepProps) => { e.preventDefault(); const trimmedEmail = email.trim(); - if (!emailSchema.isValidSync(trimmedEmail)) { + if (!z.email().safeParse(trimmedEmail).success) { setLocalError(t("components.authEmailStep.validation.invalidEmail")); return; } diff --git a/apps/frontend/src/components/widget-steps/AveniaLivenessStep/index.tsx b/apps/frontend/src/components/widget-steps/AveniaLivenessStep/index.tsx index 5d68a794b..471696aa5 100644 --- a/apps/frontend/src/components/widget-steps/AveniaLivenessStep/index.tsx +++ b/apps/frontend/src/components/widget-steps/AveniaLivenessStep/index.tsx @@ -1,8 +1,10 @@ -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid"; +import { ArrowTopRightOnSquareIcon, ShieldCheckIcon } from "@heroicons/react/20/solid"; import { motion } from "motion/react"; import React, { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import livenessCheck from "../../../assets/liveness-check.svg"; +import livenessCheckSuccess from "../../../assets/liveness-check-success.svg"; +import { cn } from "../../../helpers/cn"; import { AveniaKycActorRef, SelectedAveniaData } from "../../../machines/types"; import { StepFooter } from "../../StepFooter"; @@ -48,7 +50,9 @@ export const AveniaLivenessStep: React.FC = ({ aveniaSt transition={{ duration: 0.3 }} > - {t("components.aveniaLiveness.title")} + + {t("components.aveniaLiveness.title")} + = ({ aveniaSt {t("components.aveniaLiveness.cameraWarning")} - + {livenessCheckOpened && ( @@ -93,11 +101,12 @@ export const AveniaLivenessStep: React.FC = ({ aveniaSt {livenessCheckOpened ? ( + {t("components.aveniaLiveness.livenessDone")} ) : ( diff --git a/apps/frontend/src/hooks/brla/useKYBForm/index.tsx b/apps/frontend/src/hooks/brla/useKYBForm/index.tsx new file mode 100644 index 000000000..2d9f029ef --- /dev/null +++ b/apps/frontend/src/hooks/brla/useKYBForm/index.tsx @@ -0,0 +1,38 @@ +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; +import { isValidCnpj, isValidCpf } from "@vortexfi/shared"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { ExtendedAveniaFieldOptions } from "../../../components/Avenia/AveniaField"; +import { useTaxId } from "../../../stores/quote/useQuoteFormStore"; + +const createKybFormSchema = (t: (key: string) => string) => + z.object({ + [ExtendedAveniaFieldOptions.TAX_ID]: z + .string() + .min(1, t("components.brlaExtendedForm.validation.taxId.required")) + .refine(value => isValidCpf(value) || isValidCnpj(value), t("components.brlaExtendedForm.validation.taxId.format")), + [ExtendedAveniaFieldOptions.FULL_NAME]: z.string().min(3, t("components.brlaExtendedForm.validation.fullName.minLength")) + }); + +export type KYBFormData = z.infer>; + +export interface UseKYBFormProps { + initialData?: Partial; +} + +export const useKYBForm = ({ initialData }: UseKYBFormProps) => { + const { t } = useTranslation(); + const taxIdFromStore = useTaxId(); + + const kybForm = useForm({ + defaultValues: { + [ExtendedAveniaFieldOptions.TAX_ID]: initialData?.taxId || taxIdFromStore || "", + [ExtendedAveniaFieldOptions.FULL_NAME]: initialData?.fullName || "" + }, + mode: "onBlur", + resolver: standardSchemaResolver(createKybFormSchema(t)) + }); + + return { kybForm }; +}; diff --git a/apps/frontend/src/hooks/brla/useKYCForm/index.tsx b/apps/frontend/src/hooks/brla/useKYCForm/index.tsx index fc8886194..5d834b2ca 100644 --- a/apps/frontend/src/hooks/brla/useKYCForm/index.tsx +++ b/apps/frontend/src/hooks/brla/useKYCForm/index.tsx @@ -1,102 +1,89 @@ -import { yupResolver } from "@hookform/resolvers/yup"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import { isValidCnpj, isValidCpf } from "@vortexfi/shared"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import * as yup from "yup"; +import { z } from "zod"; import { ExtendedAveniaFieldOptions } from "../../../components/Avenia/AveniaField"; import { usePixId, useQuoteFormStoreActions, useTaxId } from "../../../stores/quote/useQuoteFormStore"; +const toISODateString = (date: Date): string => date.toISOString().split("T")[0]; + export interface UseKYCFormProps { cpfApiError: string | null; initialData?: KYCFormData; } -const getEnumInitialValues = (enumType: Record): Record => { - return Object.values(enumType).reduce((acc, field) => ({ ...acc, [field]: undefined }), {}); -}; - const createKycFormSchema = (t: (key: string) => string) => - yup - .object({ - [ExtendedAveniaFieldOptions.TAX_ID]: yup - .string() - .required(t("components.brlaExtendedForm.validation.taxId.required")) - .test("is-valid-tax-id", t("components.brlaExtendedForm.validation.taxId.format"), value => { - if (!value) { - return false; - } - return isValidCpf(value) || isValidCnpj(value); - }), - [ExtendedAveniaFieldOptions.PIX_ID]: yup.string().required(t("components.brlaExtendedForm.validation.pixId.required")), - - [ExtendedAveniaFieldOptions.FULL_NAME]: yup - .string() - .required(t("components.brlaExtendedForm.validation.fullName.required")) - .min(3, t("components.brlaExtendedForm.validation.fullName.minLength")) - .matches(/^[a-zA-Z\s]*$/, t("components.brlaExtendedForm.validation.fullName.format")), + z.object({ + [ExtendedAveniaFieldOptions.TAX_ID]: z + .string() + .min(1, t("components.brlaExtendedForm.validation.taxId.required")) + .refine(value => isValidCpf(value) || isValidCnpj(value), t("components.brlaExtendedForm.validation.taxId.format")), - [ExtendedAveniaFieldOptions.CEP]: yup - .string() - .required(t("components.brlaExtendedForm.validation.cep.required")) - .min(3, t("components.brlaExtendedForm.validation.cep.minLength")), + [ExtendedAveniaFieldOptions.PIX_ID]: z.string().min(1, t("components.brlaExtendedForm.validation.pixId.required")), - [ExtendedAveniaFieldOptions.CITY]: yup - .string() - .required(t("components.brlaExtendedForm.validation.city.required")) - .min(5, t("components.brlaExtendedForm.validation.city.minLength")), + [ExtendedAveniaFieldOptions.FULL_NAME]: z + .string() + .min(3, t("components.brlaExtendedForm.validation.fullName.minLength")) + .regex(/^[a-zA-Z\s]*$/, t("components.brlaExtendedForm.validation.fullName.format")), - [ExtendedAveniaFieldOptions.STATE]: yup - .string() - .required(t("components.brlaExtendedForm.validation.state.required")) - .max(2, t("components.brlaExtendedForm.validation.state.maxLength")), + [ExtendedAveniaFieldOptions.CEP]: z.string().min(3, t("components.brlaExtendedForm.validation.cep.minLength")), - [ExtendedAveniaFieldOptions.STREET]: yup - .string() - .required(t("components.brlaExtendedForm.validation.street.required")) - .min(5, t("components.brlaExtendedForm.validation.street.minLength")), + [ExtendedAveniaFieldOptions.CITY]: z.string().min(5, t("components.brlaExtendedForm.validation.city.minLength")), - [ExtendedAveniaFieldOptions.NUMBER]: yup.string().required(t("components.brlaExtendedForm.validation.number.required")), + [ExtendedAveniaFieldOptions.STATE]: z + .string() + .min(1, t("components.brlaExtendedForm.validation.state.required")) + .max(2, t("components.brlaExtendedForm.validation.state.maxLength")), - [ExtendedAveniaFieldOptions.BIRTHDATE]: yup - .date() - .transform((value: Date | undefined, originalValue: any) => { - return originalValue === "" ? undefined : value; - }) - .required(t("components.brlaExtendedForm.validation.birthdate.required")) + [ExtendedAveniaFieldOptions.STREET]: z.string().min(5, t("components.brlaExtendedForm.validation.street.minLength")), + + [ExtendedAveniaFieldOptions.NUMBER]: z.string().min(1, t("components.brlaExtendedForm.validation.number.required")), + + [ExtendedAveniaFieldOptions.BIRTHDATE]: z.preprocess( + val => (val === "" || val === undefined ? undefined : new Date(val as string)), + z + .date({ error: () => t("components.brlaExtendedForm.validation.birthdate.required") }) .max(new Date(), t("components.brlaExtendedForm.validation.birthdate.future")) .min(new Date(1900, 0, 1), t("components.brlaExtendedForm.validation.birthdate.tooOld")) - .test("is-18-or-older", t("components.brlaExtendedForm.validation.birthdate.tooYoung"), value => { - if (!value) return true; - const birthDate = new Date(value); - const ageDate = new Date(birthDate); + .refine(value => { + const ageDate = new Date(value); ageDate.setFullYear(ageDate.getFullYear() + 18); return ageDate <= new Date(); - }), - - [ExtendedAveniaFieldOptions.COMPANY_NAME]: yup - .string() - .min(3, t("components.brlaExtendedForm.validation.companyName.minLength")), - - [ExtendedAveniaFieldOptions.START_DATE]: yup + }, t("components.brlaExtendedForm.validation.birthdate.tooYoung")) + .transform(toISODateString) + ), + + [ExtendedAveniaFieldOptions.COMPANY_NAME]: z.preprocess( + val => (val === "" ? undefined : val), + z.string().min(3, t("components.brlaExtendedForm.validation.companyName.minLength")).optional() + ), + + [ExtendedAveniaFieldOptions.START_DATE]: z.preprocess( + val => (val === "" || val === undefined ? undefined : new Date(val as string)), + z .date() - .transform((value: Date | undefined, originalValue: any) => { - return originalValue === "" ? undefined : value; - }) .max(new Date(), t("components.brlaExtendedForm.validation.startDate.future")) - .min(new Date(1900, 0, 1), t("components.brlaExtendedForm.validation.startDate.tooOld")), + .min(new Date(1900, 0, 1), t("components.brlaExtendedForm.validation.startDate.tooOld")) + .optional() + ), - [ExtendedAveniaFieldOptions.PARTNER_CPF]: yup + [ExtendedAveniaFieldOptions.PARTNER_CPF]: z.preprocess( + val => (val === "" ? undefined : val), + z .string() - .matches(/^\d{3}(\.\d{3}){2}-\d{2}$|^\d{11}$/, t("components.brlaExtendedForm.validation.partnerCpf.format")), - [ExtendedAveniaFieldOptions.EMAIL]: yup - .string() - .email(t("components.brlaExtendedForm.validation.email.format")) - .required(t("components.brlaExtendedForm.validation.email.required")) - }) - .required(); + .regex(/^\d{3}(\.\d{3}){2}-\d{2}$|^\d{11}$/, t("components.brlaExtendedForm.validation.partnerCpf.format")) + .optional() + ), + + [ExtendedAveniaFieldOptions.EMAIL]: z + .string() + .min(1, t("components.brlaExtendedForm.validation.email.required")) + .pipe(z.email(t("components.brlaExtendedForm.validation.email.format"))) + }); -export type KYCFormData = yup.InferType>; +export type KYCFormData = z.infer>; export const useKYCForm = ({ cpfApiError, initialData }: UseKYCFormProps) => { const { t } = useTranslation(); @@ -105,35 +92,27 @@ export const useKYCForm = ({ cpfApiError, initialData }: UseKYCFormProps) => { const { setTaxId, setPixId } = useQuoteFormStoreActions(); - const kycFormSchema = createKycFormSchema(t); - - const defaultValues = { - ...getEnumInitialValues(ExtendedAveniaFieldOptions), - ...initialData, - [ExtendedAveniaFieldOptions.TAX_ID]: initialData?.taxId || taxIdFromStore || "", - [ExtendedAveniaFieldOptions.PIX_ID]: initialData?.pixId || pixIdFromStore || "" - }; - const kycForm = useForm({ - defaultValues: defaultValues, + defaultValues: { + ...initialData, + [ExtendedAveniaFieldOptions.TAX_ID]: initialData?.taxId || taxIdFromStore || "", + [ExtendedAveniaFieldOptions.PIX_ID]: initialData?.pixId || pixIdFromStore || "" + }, mode: "onBlur", - resolver: yupResolver(kycFormSchema) + resolver: standardSchemaResolver(createKycFormSchema(t)) }); - const watchedCpf = kycForm.watch(ExtendedAveniaFieldOptions.TAX_ID); - const watchedPixId = kycForm.watch(ExtendedAveniaFieldOptions.PIX_ID); - useEffect(() => { - if (watchedCpf !== undefined && watchedCpf !== taxIdFromStore && watchedCpf !== "") { - setTaxId(watchedCpf); - } - }, [watchedCpf, taxIdFromStore, setTaxId]); - - useEffect(() => { - if (watchedPixId !== undefined && watchedPixId !== pixIdFromStore && watchedPixId !== "") { - setPixId(watchedPixId); - } - }, [watchedPixId, pixIdFromStore, setPixId]); + const subscription = kycForm.watch((values, { name }) => { + if (name === ExtendedAveniaFieldOptions.TAX_ID && values.taxId && values.taxId !== taxIdFromStore) { + setTaxId(values.taxId); + } + if (name === ExtendedAveniaFieldOptions.PIX_ID && values.pixId && values.pixId !== pixIdFromStore) { + setPixId(values.pixId); + } + }); + return () => subscription.unsubscribe(); + }, [kycForm, taxIdFromStore, pixIdFromStore, setTaxId, setPixId]); useEffect(() => { if (cpfApiError) { @@ -146,7 +125,7 @@ export const useKYCForm = ({ cpfApiError, initialData }: UseKYCFormProps) => { kycForm.clearErrors(ExtendedAveniaFieldOptions.TAX_ID); } } - }, [t, cpfApiError, kycForm.setError, kycForm.clearErrors, kycForm.formState.errors]); + }, [t, cpfApiError, kycForm]); return { kycForm }; }; diff --git a/apps/frontend/src/hooks/quote/schema.ts b/apps/frontend/src/hooks/quote/schema.ts index f7f6a199d..8ed504fcc 100644 --- a/apps/frontend/src/hooks/quote/schema.ts +++ b/apps/frontend/src/hooks/quote/schema.ts @@ -1,6 +1,6 @@ -import { FiatToken, OnChainToken, OnChainTokenSymbol, RampDirection } from "@vortexfi/shared"; +import { FiatToken, OnChainTokenSymbol, RampDirection } from "@vortexfi/shared"; import { useTranslation } from "react-i18next"; -import * as Yup from "yup"; +import { z } from "zod"; import { useRampDirection } from "../../stores/rampDirectionStore"; export type QuoteFormValues = { @@ -14,12 +14,6 @@ export type QuoteFormValues = { taxId?: string; }; -const transformNumber = (value: unknown, originalValue: unknown) => { - if (!originalValue) return 0; - if (typeof originalValue === "string" && originalValue !== "") value = Number(originalValue) ?? 0; - return value; -}; - const cpfRegex = /^\d{3}(\.\d{3}){2}-\d{2}$|^\d{11}$/; const cnpjRegex = /^(\d{2}\.?\d{3}\.?\d{3}\/?\d{4}-?\d{2})$/; @@ -40,40 +34,44 @@ const pixKeyRegex = [ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/ // Random ]; -export const createQuoteFormSchema = ( - t: (key: string) => string, - rampDirection: RampDirection -): Yup.ObjectSchema => { - return Yup.object().shape({ - deadline: Yup.number().transform(transformNumber), - fiatToken: Yup.mixed().required(t("components.swap.validation.fiatToken.required")), - inputAmount: Yup.string().required(t("components.swap.validation.inputAmount.required")), - onChainToken: Yup.mixed().required(t("components.swap.validation.onChainToken.required")), - outputAmount: Yup.string().optional(), - pixId: Yup.string().when("fiatToken", { - is: (value: FiatToken) => value === FiatToken.BRL && rampDirection === RampDirection.SELL, - otherwise: schema => schema.optional(), - then: schema => - schema - .required(t("components.swap.validation.pixId.required")) - .test("matches-one", t("components.swap.validation.pixId.format"), value => { - if (!value) return false; - return pixKeyRegex.some(regex => regex.test(value)); - }) - }), - slippage: Yup.number().transform(transformNumber), - taxId: Yup.string().when("fiatToken", { - is: (value: FiatToken) => value === FiatToken.BRL, - otherwise: schema => schema.optional(), - then: schema => - schema - .required(t("components.swap.validation.taxId.required")) - .test("matches-one", t("components.swap.validation.taxId.format"), value => { - if (!value) return false; - return isValidCnpj(value) || isValidCpf(value); - }) +export const createQuoteFormSchema = (t: (key: string) => string, rampDirection: RampDirection) => { + return z + .object({ + deadline: z.number().optional(), + fiatToken: z.string() as z.ZodType, + inputAmount: z.string().min(1, t("components.swap.validation.inputAmount.required")), + onChainToken: z.string() as z.ZodType, + outputAmount: z.string().optional(), + pixId: z.string().optional(), + slippage: z.number().optional(), + taxId: z.string().optional() }) - }); + .superRefine((data, ctx) => { + if (data.fiatToken === FiatToken.BRL && rampDirection === RampDirection.SELL) { + const { pixId } = data; + if (!pixId) { + ctx.addIssue({ + code: "custom", + message: t("components.swap.validation.pixId.required"), + path: ["pixId"] + }); + } else if (!pixKeyRegex.some(regex => regex.test(pixId))) { + ctx.addIssue({ code: "custom", message: t("components.swap.validation.pixId.format"), path: ["pixId"] }); + } + } + if (data.fiatToken === FiatToken.BRL) { + const { taxId } = data; + if (!taxId) { + ctx.addIssue({ + code: "custom", + message: t("components.swap.validation.taxId.required"), + path: ["taxId"] + }); + } else if (!isValidCnpj(taxId) && !isValidCpf(taxId)) { + ctx.addIssue({ code: "custom", message: t("components.swap.validation.taxId.format"), path: ["taxId"] }); + } + } + }); }; export const useSchema = () => { diff --git a/apps/frontend/src/hooks/quote/useQuoteForm.ts b/apps/frontend/src/hooks/quote/useQuoteForm.ts index 217272006..24b1fc163 100644 --- a/apps/frontend/src/hooks/quote/useQuoteForm.ts +++ b/apps/frontend/src/hooks/quote/useQuoteForm.ts @@ -1,4 +1,4 @@ -import { yupResolver } from "@hookform/resolvers/yup"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import { FiatToken } from "@vortexfi/shared"; import { useCallback, useEffect } from "react"; import { UseFormReturn, useForm } from "react-hook-form"; @@ -31,7 +31,7 @@ export const useQuoteForm = (): { const form = useForm({ defaultValues: DEFAULT_QUOTE_FORM_VALUES, - resolver: yupResolver(formSchema) + resolver: standardSchemaResolver(formSchema) }); const rampActor = useRampActor(); diff --git a/apps/frontend/src/hooks/ramp/schema.ts b/apps/frontend/src/hooks/ramp/schema.ts index 106a4931a..ce4fb53b6 100644 --- a/apps/frontend/src/hooks/ramp/schema.ts +++ b/apps/frontend/src/hooks/ramp/schema.ts @@ -2,7 +2,7 @@ import { decodeAddress, encodeAddress } from "@polkadot/keyring"; import { hexToU8a, isHex } from "@polkadot/util"; import { CNPJ_REGEX, CPF_REGEX, FiatToken, isValidCnpj, isValidCpf, Networks, RampDirection } from "@vortexfi/shared"; import { useTranslation } from "react-i18next"; -import * as Yup from "yup"; +import { z } from "zod"; import { useQuote } from "../../stores/quote/useQuoteStore"; import { useRampDirection } from "../../stores/rampDirectionStore"; @@ -14,13 +14,15 @@ export type RampFormValues = { fiatToken?: FiatToken; }; -export const PHONE_REGEX = /^\+[1-9][0-9]\d{1,14}$/; -export const EMAIL_REGEX = - /^(([^<>()[\]\\.,;:\s@"]+(.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; -export const RANDOM_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; +const pixKeySchema = z.union([ + z.string().regex(CPF_REGEX), + z.string().regex(CNPJ_REGEX), + z.string().regex(/^\+[1-9][0-9]\d{1,14}$/), + z.email(), + z.guid() +]); -// Regex adopted from here https://developers.international.pagseguro.com/reference/pix-key-validation-and-regex-1 -const pixKeyRegex = [CPF_REGEX, CNPJ_REGEX, PHONE_REGEX, EMAIL_REGEX, RANDOM_REGEX]; +const evmAddressSchema = z.string().regex(/^(0x)?[0-9a-f]{40}$/i); const isValidPolkadotAddress = (address: string) => { try { @@ -34,50 +36,65 @@ const isValidPolkadotAddress = (address: string) => { } }; -const isValidEvmAddress = (address: string) => { - return /^(0x)?[0-9a-f]{40}$/i.test(address); -}; - export const createRampFormSchema = ( t: (key: string) => string, rampDirection: RampDirection, requiresWalletAddress: "substrate" | "evm" | false ) => { - return Yup.object().shape({ - pixId: Yup.string().when("fiatToken", { - is: (value: FiatToken) => value === FiatToken.BRL && rampDirection === RampDirection.SELL, - otherwise: schema => schema.optional(), - then: schema => - schema - .required(t("components.swap.validation.pixId.required")) - .test("matches-one", t("components.swap.validation.pixId.format"), value => { - if (!value) return false; - return pixKeyRegex.some(regex => regex.test(value)); - }) - }), - taxId: Yup.string().when("fiatToken", { - is: (value: FiatToken) => value === FiatToken.BRL, - otherwise: schema => schema.optional(), - then: schema => - schema - .required(t("components.swap.validation.taxId.required")) - .test("matches-one", t("components.swap.validation.taxId.format"), value => { - if (!value) return false; - return isValidCnpj(value) || isValidCpf(value); - }) - }), - walletAddress: Yup.string() - .test("is-valid-evm-address", t("components.swap.validation.walletAddress.formatEvm"), value => { - if (!requiresWalletAddress || requiresWalletAddress === "substrate") return true; - if (!value) return false; - if (requiresWalletAddress === "evm") return isValidEvmAddress(value); - }) - .test("is-valid-substrate-address", t("components.swap.validation.walletAddress.formatSubstrate"), value => { - if (!requiresWalletAddress || requiresWalletAddress === "evm") return true; - if (!value) return false; - if (requiresWalletAddress === "substrate") return isValidPolkadotAddress(value); - }) - }); + return z + .object({ + fiatToken: z.string().optional() as z.ZodType, + moneriumWalletAddress: z.string().optional(), + pixId: z.string().optional(), + taxId: z.string().optional(), + walletAddress: z.string().optional() + }) + .superRefine((data, ctx) => { + if (data.fiatToken === FiatToken.BRL && rampDirection === RampDirection.SELL) { + const { pixId } = data; + if (!pixId) { + ctx.addIssue({ + code: "custom", + message: t("components.swap.validation.pixId.required"), + path: ["pixId"] + }); + } else if (!pixKeySchema.safeParse(pixId).success) { + ctx.addIssue({ code: "custom", message: t("components.swap.validation.pixId.format"), path: ["pixId"] }); + } + } + if (data.fiatToken === FiatToken.BRL) { + const { taxId } = data; + if (!taxId) { + ctx.addIssue({ + code: "custom", + message: t("components.swap.validation.taxId.required"), + path: ["taxId"] + }); + } else if (!isValidCnpj(taxId) && !isValidCpf(taxId)) { + ctx.addIssue({ code: "custom", message: t("components.swap.validation.taxId.format"), path: ["taxId"] }); + } + } + if (requiresWalletAddress === "evm") { + const { walletAddress } = data; + if (!walletAddress || !evmAddressSchema.safeParse(walletAddress).success) { + ctx.addIssue({ + code: "custom", + message: t("components.swap.validation.walletAddress.formatEvm"), + path: ["walletAddress"] + }); + } + } + if (requiresWalletAddress === "substrate") { + const { walletAddress } = data; + if (!walletAddress || !isValidPolkadotAddress(walletAddress)) { + ctx.addIssue({ + code: "custom", + message: t("components.swap.validation.walletAddress.formatSubstrate"), + path: ["walletAddress"] + }); + } + } + }); }; export const useSchema = () => { diff --git a/apps/frontend/src/hooks/ramp/useRampForm.ts b/apps/frontend/src/hooks/ramp/useRampForm.ts index 6e3cd4312..9fcc47a2f 100644 --- a/apps/frontend/src/hooks/ramp/useRampForm.ts +++ b/apps/frontend/src/hooks/ramp/useRampForm.ts @@ -1,4 +1,4 @@ -import { yupResolver } from "@hookform/resolvers/yup"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import { useEffect, useRef } from "react"; import { UseFormReturn, useForm } from "react-hook-form"; import { RampFormValues, useSchema } from "./schema"; @@ -13,7 +13,7 @@ export const useRampForm = ( const form = useForm({ defaultValues, - resolver: yupResolver(formSchema) + resolver: standardSchemaResolver(formSchema) }); const isAddressSet = useRef(false); diff --git a/apps/frontend/src/hooks/useContactForm.ts b/apps/frontend/src/hooks/useContactForm.ts index a235a5b94..f487c0566 100644 --- a/apps/frontend/src/hooks/useContactForm.ts +++ b/apps/frontend/src/hooks/useContactForm.ts @@ -1,26 +1,38 @@ -import { yupResolver } from "@hookform/resolvers/yup"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import * as yup from "yup"; +import { z } from "zod"; const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const createContactFormSchema = (t: (key: string) => string) => - yup.object({ - email: yup - .string() - .required(t("pages.contact.validation.emailRequired")) - .matches(EMAIL_REGEX, t("pages.contact.validation.emailFormat")), - fullName: yup.string().required(t("pages.contact.validation.fullNameRequired")), - inquiry: yup.string().required(t("pages.contact.validation.inquiryRequired")), - privacyPolicyAccepted: yup + z.object({ + email: z + .string({ + error: issue => (issue.input === undefined ? undefined : undefined) + }) + .regex(EMAIL_REGEX, t("pages.contact.validation.emailFormat")), + fullName: z + .string({ + error: issue => (issue.input === undefined ? undefined : undefined) + }) + .min(1, t("pages.contact.validation.fullNameRequired")), + inquiry: z + .string({ + error: issue => (issue.input === undefined ? undefined : undefined) + }) + .min(1, t("pages.contact.validation.inquiryRequired")), + privacyPolicyAccepted: z .boolean() - .oneOf([true], t("pages.contact.validation.privacyPolicyRequired")) - .required(t("pages.contact.validation.privacyPolicyRequired")), - projectName: yup.string().required(t("pages.contact.validation.projectNameRequired")) + .refine(val => val === true, { message: t("pages.contact.validation.privacyPolicyRequired") }), + projectName: z + .string({ + error: issue => (issue.input === undefined ? undefined : undefined) + }) + .min(1, t("pages.contact.validation.projectNameRequired")) }); -export type ContactFormData = yup.InferType>; +export type ContactFormData = z.infer>; export function useContactForm() { const { t } = useTranslation(); @@ -35,7 +47,7 @@ export function useContactForm() { projectName: "" }, mode: "onChange", - resolver: yupResolver(schema) + resolver: standardSchemaResolver(schema) }); return { form, schema }; diff --git a/apps/frontend/src/hooks/useStepBackNavigation.ts b/apps/frontend/src/hooks/useStepBackNavigation.ts index d61190cc5..d50eee4b7 100644 --- a/apps/frontend/src/hooks/useStepBackNavigation.ts +++ b/apps/frontend/src/hooks/useStepBackNavigation.ts @@ -3,6 +3,7 @@ import { useSelector } from "@xstate/react"; import { useEffect, useRef } from "react"; import { useFiatAccountActor, useFiatAccountSelector } from "../contexts/FiatAccountMachineContext"; import { useAveniaKycActor, useAveniaKycSelector, useRampActor } from "../contexts/rampState"; +import { isInCompoundState } from "../machines/types"; export const useStepBackNavigation = () => { const navigate = useNavigate(); @@ -60,7 +61,7 @@ export const useStepBackNavigation = () => { aveniaKycActor.send({ type: "GO_BACK" }); return; } - if (typeof childState === "object" && childState !== null && "KYBFlow" in childState) { + if (isInCompoundState(childState, "KYBFlow")) { aveniaKycActor.send({ type: "GO_BACK" }); return; } diff --git a/apps/frontend/src/machines/actors/brla/submitLevel1.actor.ts b/apps/frontend/src/machines/actors/brla/submitLevel1.actor.ts index bd7e14678..b3e2ab21f 100644 --- a/apps/frontend/src/machines/actors/brla/submitLevel1.actor.ts +++ b/apps/frontend/src/machines/actors/brla/submitLevel1.actor.ts @@ -19,7 +19,7 @@ export const submitActor = fromPromise(async ({ input }: { input: AveniaKycConte city: kycFormData.city, country: "BRA", countryOfTaxId: "BRA", - dateOfBirth: kycFormData.birthdate as unknown as string, + dateOfBirth: kycFormData.birthdate, email: kycFormData.email, fullName: kycFormData.fullName, state: kycFormData.state, diff --git a/apps/frontend/src/machines/brlaKyc.machine.ts b/apps/frontend/src/machines/brlaKyc.machine.ts index 56239b40d..074f1d288 100644 --- a/apps/frontend/src/machines/brlaKyc.machine.ts +++ b/apps/frontend/src/machines/brlaKyc.machine.ts @@ -1,4 +1,5 @@ import { assign, DoneActorEvent, fromPromise, setup } from "xstate"; +import { KYBFormData } from "../hooks/brla/useKYBForm"; import { KYCFormData } from "../hooks/brla/useKYCForm"; import { BrlaService } from "../services/api"; import { KycStatus, KycSubmissionRejectedError } from "../services/signingService"; @@ -47,7 +48,7 @@ export const aveniaKycMachine = setup({ types: { context: {} as AveniaKycContext, events: {} as - | { type: "FORM_SUBMIT"; formData: KYCFormData } + | { type: "FORM_SUBMIT"; formData: KYCFormData | KYBFormData } | { type: "LIVENESS_DONE" } | { type: "DOCUMENTS_SUBMIT"; documentsId: UploadIds } | { type: "CLOSE_SUCCESS_MODAL" } diff --git a/apps/frontend/src/machines/types.ts b/apps/frontend/src/machines/types.ts index f7fbfd388..b0ba26672 100644 --- a/apps/frontend/src/machines/types.ts +++ b/apps/frontend/src/machines/types.ts @@ -112,6 +112,24 @@ export type SelectedAveniaData = { context: AveniaKycContext; }; +/** + * Checks whether an XState v5 machine is currently in a compound (parent) state. + * + * In XState v5, `state.value` is a plain string when the machine is in a simple state, + * but becomes an object when it is in a nested state. For example, a machine in + * `KYBFlow > CompanyVerification` has `state.value === { KYBFlow: "CompanyVerification" }`. + * + * @see https://stately.ai/docs/xstate-v5/state-machine-actors#state-value + * + * @example + * isInCompoundState(state.value, "KYBFlow") + * // true when state.value === { KYBFlow: "CompanyVerification" } + * // false when state.value === "DocumentUpload" + */ +export function isInCompoundState(stateValue: unknown, state: string): boolean { + return typeof stateValue === "object" && stateValue !== null && state in (stateValue as object); +} + export type SelectedAlfredpayData = { stateValue: AlfredpayKycSnapshot["value"]; context: AlfredpayKycContext; diff --git a/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx b/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx index 1557e1938..b8e019371 100644 --- a/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx +++ b/apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx @@ -1,4 +1,4 @@ -import { zodResolver } from "@hookform/resolvers/zod"; +import { standardSchemaResolver } from "@hookform/resolvers/standard-schema"; import type { AlfredpayFiatAccountType } from "@vortexfi/shared"; import type { TFunction } from "i18next"; import { Controller, useForm } from "react-hook-form"; @@ -25,11 +25,11 @@ function buildZodSchema( fields: FieldDef[], accountType: FiatAccountTypeKey, t: TFunction -): z.ZodObject> { - const shape: Record = {}; +): z.ZodObject> { + const shape: Record = {}; for (const f of fields) { - let schema: z.ZodTypeAny; + let schema: z.ZodType; if (f.required) { schema = z.string().min(1, t("components.fiatAccountRegistration.validation.fieldRequired", { field: f.label })); @@ -72,7 +72,7 @@ export function RegisterFiatAccountScreen({ country, accountType, onSuccess }: R formState: { errors }, handleSubmit, register - } = useForm({ resolver: zodResolver(schema) }); + } = useForm({ resolver: standardSchemaResolver(schema) }); const alfredType = ACCOUNT_TYPE_TO_ALFRED_TYPE[accountType] as AlfredpayFiatAccountType; diff --git a/apps/frontend/src/pages/widget/index.tsx b/apps/frontend/src/pages/widget/index.tsx index edbb3d838..856274d50 100644 --- a/apps/frontend/src/pages/widget/index.tsx +++ b/apps/frontend/src/pages/widget/index.tsx @@ -26,6 +26,7 @@ import { } from "../../contexts/rampState"; import { cn } from "../../helpers/cn"; import { useAuthTokens } from "../../hooks/useAuthTokens"; +import { isInCompoundState } from "../../machines/types"; import { FiatAccountRegistration } from "../alfredpay/FiatAccountRegistration"; export interface WidgetProps { @@ -122,7 +123,9 @@ const WidgetContent = () => { if (aveniaKycActor) { const isCnpj = aveniaState?.context.taxId ? isValidCnpj(aveniaState.context.taxId) : false; - if (isCnpj && aveniaState?.context.kybUrls) { + const isInKybFlow = isCnpj && isInCompoundState(aveniaState?.stateValue, "KYBFlow"); + + if (isInKybFlow) { return ; } diff --git a/bun.lock b/bun.lock index 1138fe351..2c98dfb74 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,6 @@ "name": "vortex-monorepo", "dependencies": { "big.js": "^7.0.1", - "cobe": "^2.0.1", "husky": "^9.1.7", "lint-staged": "^16.1.0", "numora-react": "^3.0.3", @@ -105,7 +104,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:", @@ -152,6 +151,7 @@ "buffer": "^6.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cobe": "catalog:", "crypto-js": "^4.2.0", "i18next": "^24.2.3", "input-otp": "^1.4.2", @@ -174,8 +174,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": { @@ -381,6 +380,7 @@ "bcrypt": "5.1.1", "big.js": "^7.0.1", "clsx": "^1.2.1", + "cobe": "^2.0.1", "concurrently": "^9.1.2", "prettier": "^2.8.4", "stellar-sdk": "^13.1.0", @@ -938,7 +938,7 @@ "@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="], - "@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="], + "@hookform/resolvers": ["@hookform/resolvers@4.1.3", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ=="], "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], @@ -1756,6 +1756,8 @@ "@stablelib/wipe": ["@stablelib/wipe@1.0.1", "", {}, "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@stellar/js-xdr": ["@stellar/js-xdr@3.1.2", "", {}, "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ=="], "@stellar/stellar-base": ["@stellar/stellar-base@13.1.0", "", { "dependencies": { "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", "bignumber.js": "^9.1.2", "buffer": "^6.0.3", "sha.js": "^2.3.6", "tweetnacl": "^1.0.3" }, "optionalDependencies": { "sodium-native": "^4.3.3" } }, "sha512-90EArG+eCCEzDGj3OJNoCtwpWDwxjv+rs/RNPhvg4bulpjN/CSRj+Ys/SalRcfM4/WRC5/qAfjzmJBAuquWhkA=="], @@ -3862,8 +3864,6 @@ "propagate": ["propagate@2.0.1", "", {}, "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag=="], - "property-expr": ["property-expr@2.0.6", "", {}, "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="], - "proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -4290,8 +4290,6 @@ "timers-ext": ["timers-ext@0.1.8", "", { "dependencies": { "es5-ext": "^0.10.64", "next-tick": "^1.1.0" } }, "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww=="], - "tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -4320,8 +4318,6 @@ "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], - "toposort": ["toposort@2.0.2", "", {}, "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="], - "toposort-class": ["toposort-class@1.0.1", "", {}, "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg=="], "touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="], @@ -4620,9 +4616,7 @@ "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], - "yup": ["yup@1.7.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="], @@ -4652,6 +4646,8 @@ "@coinbase/cdp-sdk/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], + "@coinbase/cdp-sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@coinbase/wallet-sdk/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], "@coinbase/wallet-sdk/clsx": ["clsx@1.2.1", "", {}, "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="], @@ -4962,10 +4958,16 @@ "@tanstack/router-generator/prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/router-plugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + "@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@tanstack/router-utils/diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "@tanstack/zod-adapter/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], @@ -5308,8 +5310,6 @@ "porto/ox": ["ox@0.9.17", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-rKAnhzhRU3Xh3hiko+i1ZxywZ55eWQzeS/Q4HRKLx2PqfHOolisZHErSsJVipGlmQKHW5qwOED/GighEw9dbLg=="], - "porto/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], @@ -5474,6 +5474,8 @@ "web3-validator/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], + "web3-validator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "wordwrapjs/typical": ["typical@5.2.0", "", {}, "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -5482,8 +5484,6 @@ "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "yup/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -6014,6 +6014,8 @@ "wcwidth/defaults/clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "web3-eth-abi/abitype/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "web3-eth-accounts/ethereum-cryptography/@noble/curves": ["@noble/curves@1.4.2", "", { "dependencies": { "@noble/hashes": "1.4.0" } }, "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw=="], "web3-eth-accounts/ethereum-cryptography/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], diff --git a/contracts/relayer/typechain-types/contracts/TokenRelayer.ts b/contracts/relayer/typechain-types/contracts/TokenRelayer.ts index 9d405d34a..c7019692c 100644 --- a/contracts/relayer/typechain-types/contracts/TokenRelayer.ts +++ b/contracts/relayer/typechain-types/contracts/TokenRelayer.ts @@ -124,7 +124,7 @@ export interface TokenRelayerInterface extends Interface { export namespace EIP712DomainChangedEvent { export type InputTuple = []; export type OutputTuple = []; - export interface OutputObject {} + export type OutputObject = {}; export type Event = TypedContractEvent; export type Filter = TypedDeferredTopicFilter; export type Log = TypedEventLog; diff --git a/packages/shared/src/services/brla/brlaApiService.ts b/packages/shared/src/services/brla/brlaApiService.ts index f2c8ba002..798313254 100644 --- a/packages/shared/src/services/brla/brlaApiService.ts +++ b/packages/shared/src/services/brla/brlaApiService.ts @@ -357,6 +357,7 @@ export class BrlaApiService { */ public async initiateKybLevel1(subAccountId: string): Promise { const query = `subAccountId=${encodeURIComponent(subAccountId)}`; + // Avenia requires the field to be present but ignores its value for the Web SDK flow. const payload = { redirectUrl: "" }; return await this.sendRequest(Endpoint.KybLevel1WebSdk, "POST", query, payload); } diff --git a/packages/shared/src/services/brla/mappings.ts b/packages/shared/src/services/brla/mappings.ts index 33cefb73a..6f456dbd6 100644 --- a/packages/shared/src/services/brla/mappings.ts +++ b/packages/shared/src/services/brla/mappings.ts @@ -170,7 +170,7 @@ export interface EndpointMapping { }; [Endpoint.KybLevel1WebSdk]: { POST: { - body: { redirectUrl: string | undefined }; + body: { redirectUrl: string }; response: KybLevel1Response; }; GET: {