[READY] Fix 🇧🇷 Avenia KYB Flow (and improve visual feedback)#1116
[READY] Fix 🇧🇷 Avenia KYB Flow (and improve visual feedback)#1116Sharqiewicz wants to merge 8 commits intostagingfrom
Conversation
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
@Sharqiewicz there are some merge conflicts, let's try to resolve them. |
There was a problem hiding this comment.
Pull request overview
This PR fixes the Avenia KYB flow rendering/routing in the widget, reduces unnecessary form initialization/subscriptions by splitting KYC vs KYB hooks/components, and adds improved KYB visual feedback (success styling + new animated SparkleButton). It also migrates several frontend form validators from Yup to Zod v4 via @hookform/resolvers/standard-schema, and updates the BRLA KYB initiation request to include the required redirectUrl field.
Changes:
- Fix KYB flow gating by checking nested XState compound state (
KYBFlow) rather than presence of fetched URLs; add KYB back navigation. - Refactor Avenia form handling: new
useKYBForm, genericAveniaVerificationFormsubmission, and KYC form scoping to reduce idle subscriptions. - Migrate validation (Yup → Zod v4 + standard-schema resolver) and add new UI feedback components/assets (SparkleButton + success SVG variants).
Reviewed changes
Copilot reviewed 24 out of 27 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/shared/src/services/brla/mappings.ts | Updates endpoint typing so KYB Web SDK POST expects { redirectUrl } body. |
| packages/shared/src/services/brla/brlaApiService.ts | Sends { redirectUrl: "" } payload for KYB initiation as required by Avenia. |
| bun.lock | Locks dependency updates (notably Zod v4, resolvers v4) and catalog move for cobe. |
| apps/frontend/package.json | Bumps @hookform/resolvers, migrates to zod@^4, removes yup. |
| apps/frontend/src/pages/widget/index.tsx | Fixes KYB flow rendering by checking compound XState state. |
| apps/frontend/src/machines/types.ts | Adds isInCompoundState helper for XState v5 nested state detection. |
| apps/frontend/src/hooks/useStepBackNavigation.ts | Uses isInCompoundState to detect KYB flow for back navigation. |
| apps/frontend/src/machines/brlaKyc.machine.ts | Broadens FORM_SUBMIT typing to accept KYC or KYB form payloads. |
| apps/frontend/src/hooks/brla/useKYCForm/index.tsx | Migrates KYC validation to Zod + standard-schema; consolidates store sync watch. |
| apps/frontend/src/hooks/brla/useKYBForm/index.tsx | Adds a dedicated KYB-only hook for company flow. |
| apps/frontend/src/components/Avenia/AveniaVerificationForm/index.tsx | Makes verification form reusable via generic onSubmit prop. |
| apps/frontend/src/components/Avenia/AveniaKYCForm.tsx | Scopes KYC form mounting to the form step; swaps layout components. |
| apps/frontend/src/components/Avenia/AveniaKYBForm.tsx | Switches to useKYBForm and uses generic submit handler. |
| apps/frontend/src/components/Avenia/AveniaKYBFlow/index.tsx | Adds menu buttons and consolidates KYB step rendering. |
| apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyStep.tsx | Adds success-state visuals and swaps in SparkleButton after verification starts. |
| apps/frontend/src/components/Avenia/AveniaKYBFlow/AveniaKYBVerifyCompany*.tsx | Supplies verified-state SVGs to the KYB verification step. |
| apps/frontend/src/components/SparkleButton/index.tsx | Introduces animated button with periodic/click sparkle bursts. |
| apps/frontend/src/assets/*-success.svg | Adds success-colored variants for KYB verification images. |
| apps/frontend/src/hooks/useContactForm.ts | Migrates contact form validation to Zod + standard-schema. |
| apps/frontend/src/hooks/ramp/useRampForm.ts | Switches resolver to standard-schema. |
| apps/frontend/src/hooks/ramp/schema.ts | Migrates ramp schema to Zod with custom refinements. |
| apps/frontend/src/hooks/quote/useQuoteForm.ts | Switches resolver to standard-schema. |
| apps/frontend/src/hooks/quote/schema.ts | Migrates quote schema to Zod with custom refinements. |
| apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx | Switches resolver to standard-schema and adjusts Zod types. |
| apps/frontend/src/components/widget-steps/AuthEmailStep/index.tsx | Replaces Yup email validation with Zod v4 z.email() parsing. |
Comments suppressed due to low confidence (1)
apps/frontend/src/pages/alfredpay/FiatAccountRegistration/RegisterFiatAccountScreen.tsx:48
- In
buildZodSchema, the later format-specific branches overwrite the earlierrequiredschema (e.g.,accountNumberfor SPEI/ACH/WIRE androutingNumberfor ACH/WIRE). This drops thefieldRequiredmessage, so an empty value will show a format error like “CLABE must be exactly 18 digits” instead of “{{field}} is required”. Consider chaining the regex onto the existing schema (preserving required/optional) rather than replacing it.
if (f.required) {
schema = z.string().min(1, t("components.fiatAccountRegistration.validation.fieldRequired", { field: f.label }));
} else {
schema = z.string().optional();
}
if (f.field === "accountNumber" && accountType === "SPEI") {
schema = z.string().regex(/^\d{18}$/, t("components.fiatAccountRegistration.validation.clabe"));
}
if (f.field === "routingNumber" && (accountType === "ACH" || accountType === "WIRE")) {
schema = z.string().regex(/^\d{9}$/, t("components.fiatAccountRegistration.validation.routing"));
}
if (f.field === "accountNumber" && accountType === "ACH") {
schema = z.string().regex(/^\d{4,17}$/, t("components.fiatAccountRegistration.validation.accountNumber"));
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .string({ | ||
| error: issue => (issue.input === undefined ? undefined : undefined) | ||
| }) |
There was a problem hiding this comment.
The email field no longer uses the existing “required” message and will report emailFormat even when the value is empty. Also, the error callback passed to z.string({ error: ... }) always returns undefined, which is effectively a no-op and makes the schema harder to understand. Consider using an explicit non-empty constraint (e.g., min(1) with pages.contact.validation.emailRequired) and then applying the format check (regex/email) so empty input shows the required message.
| .string({ | |
| error: issue => (issue.input === undefined ? undefined : undefined) | |
| }) | |
| .string() | |
| .min(1, t("pages.contact.validation.emailRequired")) |
| [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")), |
There was a problem hiding this comment.
Several required fields no longer use their dedicated *.required i18n messages (e.g., fullName.required, cep.required, city.required, street.required). With the current Zod schema, empty input will surface the min-length message (or a generic type error) instead of the intended required message. If these fields are still required, add an explicit non-empty constraint before the min-length/format constraints so empty values show the correct translation.
| <div className={cn("relative", className)}> | ||
| <button | ||
| className={cn("btn relative z-10 flex w-full items-center justify-center gap-2", buttonClass)} | ||
| onClick={handleClick} | ||
| > | ||
| {icon} | ||
| {label} | ||
| </button> |
There was a problem hiding this comment.
<button> defaults to type="submit". Since SparkleButton is meant to be a reusable action button and may be placed inside a <form> in the future, it should explicitly set type="button" (or accept a type prop with a safe default) to avoid accidental form submissions.
| return z | ||
| .object({ | ||
| fiatToken: z.string().optional() as z.ZodType<FiatToken | undefined>, | ||
| moneriumWalletAddress: z.string().optional(), | ||
| pixId: z.string().optional(), | ||
| taxId: z.string().optional(), | ||
| walletAddress: z.string().optional() | ||
| }) |
There was a problem hiding this comment.
fiatToken is currently modeled as z.string().optional() as z.ZodType<FiatToken | undefined>, which bypasses runtime validation and will accept any string. Using z.nativeEnum(FiatToken) (and .optional() if needed) would keep the schema aligned with the actual enum and prevent invalid values from URL params / deserialization from silently passing validation.
| .object({ | ||
| deadline: z.number().optional(), | ||
| fiatToken: z.string() as z.ZodType<FiatToken>, | ||
| inputAmount: z.string().min(1, t("components.swap.validation.inputAmount.required")), | ||
| onChainToken: z.string() as z.ZodType<OnChainTokenSymbol>, | ||
| outputAmount: z.string().optional(), | ||
| pixId: z.string().optional(), | ||
| slippage: z.number().optional(), | ||
| taxId: z.string().optional() |
There was a problem hiding this comment.
fiatToken and onChainToken are typed via as z.ZodType<...> but are only validated as generic strings at runtime. That means invalid token strings can pass schema validation and then skip the superRefine branches (e.g., BRL-specific requirements). Prefer z.nativeEnum(FiatToken) / a z.enum([...]) for OnChainTokenSymbol to enforce valid values at runtime.
Summary
widget/index.tsxnow checks XState's nested state ("KYBFlow" in stateValue) instead of the presence ofkybUrls, preventing the KYB UI from rendering prematurelyuseKYBFormhook — extracted fromuseKYCForm, scoped to the two KYB-only fields (taxId,fullName); avoids initialising the full 12-field KYC form for company usersAveniaVerificationFormdecoupled — replacedaveniaKycActorprop with a genericonSubmit: SubmitHandler<T>prop, making the form component reusable across KYC and KYB flowsAveniaKYCFormscoping fix —useKYCForm(and its two store-syncuseEffects) now only mounts in theAveniaKYCFormStepsub-component, so subscriptions are idle duringVerificationStatus,DocumentUpload, andAveniaLivenessStepstatesStepBackButtonadded toAveniaKYBFlow, using the existinguseStepBackNavigationhandler which already covers theKYBFlowXState statebrlaApiService—initiateKybLevel1now sends{ redirectUrl: "" }in the POST body; Avenia requires the field present even though it ignores the value for the Web SDK flowSparkleButtoncomponent — reusable animated button with randomised particle burst on click and on a periodic interval; supportsprimaryandsuccessthemes via CSS custom properties (var(--color-primary),var(--color-success))business-check-business.svgandbusiness-check-representative.svggenerated with the exactoklch(0.448 0.119 151.328)success color; shown inAveniaKYBVerifyStepwhenisVerificationStartedis true, alongside a success-coloured titlebrlaKyc.machine.ts—FORM_SUBMITevent now acceptsKYCFormData | KYBFormData, removing theas unknown ascast inAveniaKYBFormTest plan
AveniaKYBForm(company name + CNPJ) renders before KYB URLs are fetched;AveniaKYBFlowrenders once inKYBFlowXState stateisVerificationStartedflips totrue; image and title turn green; SparkleButton replaces the primary buttonbun typecheck