diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index af549d57..7a3b0ad7 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -481,5 +481,57 @@
"genericHint": "This should only take a moment.",
"iframeTitle": "Anchor deposit form",
"genericError": "Deposit failed"
+ },
+ "kycPage": {
+ "title": "KYC Verification",
+ "description": "Complete the verification process to comply with regulatory requirements and unlock full access to platform features.",
+ "whyKyc": "Why KYC?",
+ "reasonComply": "Comply with regulatory requirements",
+ "reasonLimits": "Increase transaction limits",
+ "reasonFeatures": "Access premium features",
+ "reasonSecurity": "Enhanced account security"
+ },
+ "kycForm": {
+ "formTitle": "KYC Submission Form",
+ "step": "Step",
+ "of": "of",
+ "steps": "Steps",
+ "completed": "completed",
+ "current": "current",
+ "upcoming": "upcoming",
+ "personalInfo": "Personal Information",
+ "addressInfo": "Address Information",
+ "documents": "Documents",
+ "review": "Review",
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "email": "Email",
+ "nationality": "Nationality",
+ "dateOfBirth": "Date of Birth",
+ "street": "Street",
+ "city": "City",
+ "state": "State",
+ "postalCode": "Postal Code",
+ "country": "Country",
+ "idType": "ID Type",
+ "selectIdType": "Select ID type",
+ "passport": "Passport",
+ "driversLicense": "Driver's License",
+ "nationalId": "National ID",
+ "idNumber": "ID Number",
+ "idFront": "ID Front",
+ "idBack": "ID Back",
+ "selfie": "Selfie",
+ "required": "Required",
+ "validationError": "Please fill in all required fields",
+ "back": "Back",
+ "next": "Next",
+ "submit": "Submit KYC",
+ "submitting": "Submitting...",
+ "submitError": "Submission failed",
+ "submitAnother": "Submit Another",
+ "successTitle": "KYC Submitted Successfully!",
+ "successDescription": "Your KYC verification has been submitted and is under review.",
+ "dash": "—"
}
}
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index d070f9ad..c5535002 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -481,5 +481,57 @@
"genericHint": "Esto solo deberia tomar un momento.",
"iframeTitle": "Formulario de deposito del anchor",
"genericError": "El deposito fallo"
+ },
+ "kycPage": {
+ "title": "Verificacion KYC",
+ "description": "Completa el proceso de verificacion para cumplir con los requisitos regulatorios y desbloquear el acceso completo a las funciones de la plataforma.",
+ "whyKyc": "Por que KYC?",
+ "reasonComply": "Cumplir con los requisitos regulatorios",
+ "reasonLimits": "Aumentar los limites de transaccion",
+ "reasonFeatures": "Acceder a funciones premium",
+ "reasonSecurity": "Seguridad mejorada de la cuenta"
+ },
+ "kycForm": {
+ "formTitle": "Formulario de envio KYC",
+ "step": "Paso",
+ "of": "de",
+ "steps": "Pasos",
+ "completed": "completado",
+ "current": "actual",
+ "upcoming": "proximo",
+ "personalInfo": "Informacion Personal",
+ "addressInfo": "Informacion de Direccion",
+ "documents": "Documentos",
+ "review": "Revisar",
+ "firstName": "Nombre",
+ "lastName": "Apellido",
+ "email": "Correo electronico",
+ "nationality": "Nacionalidad",
+ "dateOfBirth": "Fecha de nacimiento",
+ "street": "Calle",
+ "city": "Ciudad",
+ "state": "Estado",
+ "postalCode": "Codigo postal",
+ "country": "Pais",
+ "idType": "Tipo de ID",
+ "selectIdType": "Seleccionar tipo de ID",
+ "passport": "Pasaporte",
+ "driversLicense": "Licencia de conducir",
+ "nationalId": "ID Nacional",
+ "idNumber": "Numero de ID",
+ "idFront": "Frente del ID",
+ "idBack": "Reverso del ID",
+ "selfie": "Foto selfie",
+ "required": "Requerido",
+ "validationError": "Por favor completa todos los campos requeridos",
+ "back": "Atras",
+ "next": "Siguiente",
+ "submit": "Enviar KYC",
+ "submitting": "Enviando...",
+ "submitError": "Error al enviar",
+ "submitAnother": "Enviar otro",
+ "successTitle": "KYC enviado exitosamente!",
+ "successDescription": "Tu verificacion KYC ha sido enviada y esta en revision.",
+ "dash": "—"
}
}
diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json
index 29e8446d..76411c77 100644
--- a/frontend/messages/pt.json
+++ b/frontend/messages/pt.json
@@ -481,5 +481,57 @@
"genericHint": "Isso deve levar apenas um momento.",
"iframeTitle": "Formulario de deposito do anchor",
"genericError": "Falha no deposito"
+ },
+ "kycPage": {
+ "title": "Verificacao KYC",
+ "description": "Complete o processo de verificacao para atender aos requisitos regulatorios e desbloquear o acesso completo aos recursos da plataforma.",
+ "whyKyc": "Por que KYC?",
+ "reasonComply": "Atender aos requisitos regulatorios",
+ "reasonLimits": "Aumentar os limites de transacao",
+ "reasonFeatures": "Acessar recursos premium",
+ "reasonSecurity": "Seguranca aprimorada da conta"
+ },
+ "kycForm": {
+ "formTitle": "Formulario de envio KYC",
+ "step": "Passo",
+ "of": "de",
+ "steps": "Passos",
+ "completed": "concluido",
+ "current": "atual",
+ "upcoming": "proximo",
+ "personalInfo": "Informacoes Pessoais",
+ "addressInfo": "Informacoes de Endereco",
+ "documents": "Documentos",
+ "review": "Revisar",
+ "firstName": "Nome",
+ "lastName": "Sobrenome",
+ "email": "E-mail",
+ "nationality": "Nacionalidade",
+ "dateOfBirth": "Data de nascimento",
+ "street": "Rua",
+ "city": "Cidade",
+ "state": "Estado",
+ "postalCode": "Codigo postal",
+ "country": "Pais",
+ "idType": "Tipo de ID",
+ "selectIdType": "Selecionar tipo de ID",
+ "passport": "Passaporte",
+ "driversLicense": "Carteira de motorista",
+ "nationalId": "ID Nacional",
+ "idNumber": "Numero do ID",
+ "idFront": "Frente do ID",
+ "idBack": "Verso do ID",
+ "selfie": "Foto selfie",
+ "required": "Obrigatorio",
+ "validationError": "Por favor preencha todos os campos obrigatorios",
+ "back": "Voltar",
+ "next": "Proximo",
+ "submit": "Enviar KYC",
+ "submitting": "Enviando...",
+ "submitError": "Falha no envio",
+ "submitAnother": "Enviar outro",
+ "successTitle": "KYC enviado com sucesso!",
+ "successDescription": "Sua verificacao KYC foi enviada e esta em revisao.",
+ "dash": "—"
}
}
diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml
new file mode 100644
index 00000000..dc8e9f17
--- /dev/null
+++ b/frontend/pnpm-workspace.yaml
@@ -0,0 +1,6 @@
+allowBuilds:
+ '@parcel/watcher': set this to true or false
+ '@sentry/cli': set this to true or false
+ '@swc/core': set this to true or false
+ chromedriver: set this to true or false
+ unrs-resolver: set this to true or false
diff --git a/frontend/src/app/(authenticated)/kyc/page.tsx b/frontend/src/app/(authenticated)/kyc/page.tsx
index ed21e7db..77586800 100644
--- a/frontend/src/app/(authenticated)/kyc/page.tsx
+++ b/frontend/src/app/(authenticated)/kyc/page.tsx
@@ -1,5 +1,5 @@
import { Metadata } from "next";
-import KycSubmissionForm from "@/components/KycSubmissionForm";
+import KycPageContent from "@/components/KycPageContent";
export const metadata: Metadata = {
title: "KYC Verification | PLUTO",
@@ -7,29 +7,5 @@ export const metadata: Metadata = {
};
export default function KycPage() {
- return (
-
-
-
KYC Verification
-
- Complete the verification process to comply with regulatory requirements
- and unlock full access to platform features.
-
-
-
-
-
-
-
-
-
Why KYC?
-
- - • Comply with regulatory requirements
- - • Increase transaction limits
- - • Access premium features
- - • Enhanced account security
-
-
-
- );
+ return ;
}
diff --git a/frontend/src/components/KycPageContent.tsx b/frontend/src/components/KycPageContent.tsx
new file mode 100644
index 00000000..7c84c4fc
--- /dev/null
+++ b/frontend/src/components/KycPageContent.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { useTranslations } from "next-intl";
+import KycSubmissionForm from "@/components/KycSubmissionForm";
+
+export default function KycPageContent() {
+ const t = useTranslations("kycPage");
+
+ return (
+
+
+
{t("title")}
+
{t("description")}
+
+
+
+
+
+
+
+
{t("whyKyc")}
+
+ - • {t("reasonComply")}
+ - • {t("reasonLimits")}
+ - • {t("reasonFeatures")}
+ - • {t("reasonSecurity")}
+
+
+
+ );
+}
diff --git a/frontend/src/components/KycSubmissionForm.test.tsx b/frontend/src/components/KycSubmissionForm.test.tsx
index 2a6dd412..909c41d7 100644
--- a/frontend/src/components/KycSubmissionForm.test.tsx
+++ b/frontend/src/components/KycSubmissionForm.test.tsx
@@ -19,7 +19,8 @@ vi.mock("next-intl", () => ({
}));
// framer-motion: render plain HTML elements so jsdom doesn't choke on
-// animation APIs. AnimatePresence just renders its children.
+// animation APIs. AnimatePresence uses mode="wait" to ensure only the
+// current step renders at a time.
vi.mock("framer-motion", async () => {
const React = await import("react");
const motion = new Proxy(
@@ -43,8 +44,14 @@ vi.mock("framer-motion", async () => {
);
return {
motion,
- AnimatePresence: ({ children }: { children: React.ReactNode }) =>
- React.createElement(React.Fragment, null, children),
+ AnimatePresence: ({ children, mode }: { children: React.ReactNode; mode?: string }) => {
+ // mode="wait" means only render the last child (current step)
+ if (mode === "wait") {
+ const childArray = React.Children.toArray(children);
+ return React.createElement(React.Fragment, null, childArray[childArray.length - 1]);
+ }
+ return React.createElement(React.Fragment, null, children);
+ },
type: {},
};
});
@@ -79,6 +86,13 @@ function navigateToStep(targetIndex: number) {
}
}
+/** Navigate without re-filling personal step (use when already past step 1). */
+function clickNext(times: number) {
+ for (let i = 0; i < times; i++) {
+ fireEvent.click(screen.getByText("next"));
+ }
+}
+
// ─── Tests ────────────────────────────────────────────────────────────────────
describe("KycSubmissionForm", () => {
@@ -148,7 +162,7 @@ describe("KycSubmissionForm", () => {
navigateToStep(1);
expect(screen.getByText("2 of 4")).toBeInTheDocument();
- navigateToStep(2);
+ clickNext(1);
expect(screen.getByText("3 of 4")).toBeInTheDocument();
});
@@ -296,7 +310,7 @@ describe("KycSubmissionForm", () => {
fireEvent.click(screen.getByText("submit"));
await waitFor(() => {
- expect(screen.getByText("successTitle")).toBeInTheDocument();
+ expect(screen.getAllByText("successTitle").length).toBeGreaterThanOrEqual(1);
});
expect(mockToastSuccess).toHaveBeenCalled();
});
@@ -353,7 +367,7 @@ describe("KycSubmissionForm", () => {
fireEvent.click(screen.getByText("submit"));
await waitFor(() => {
- expect(screen.getByText("successTitle")).toBeInTheDocument();
+ expect(screen.getAllByText("successTitle").length).toBeGreaterThanOrEqual(1);
});
fireEvent.click(screen.getByText("submitAnother"));
@@ -410,7 +424,7 @@ describe("KycSubmissionForm", () => {
it("marks the form container with role=region", () => {
render(React.createElement(KycSubmissionForm));
- expect(screen.getByRole("region")).toBeInTheDocument();
+ expect(screen.getByRole("region", { name: "formTitle" })).toBeInTheDocument();
});
// ── Screen reader support ─────────────────────────────────────────────────
@@ -528,8 +542,8 @@ describe("KycSubmissionForm", () => {
fireEvent.click(screen.getByText("next")); // → address
fireEvent.click(screen.getByText("next")); // → documents
fireEvent.click(screen.getByText("next")); // → review
- // city was not filled, so it renders the placeholder dash
- const dashes = screen.getAllByText("—");
+ // city was not filled, so it renders the placeholder dash (mock returns key)
+ const dashes = screen.getAllByText("dash");
expect(dashes.length).toBeGreaterThan(0);
});
diff --git a/frontend/src/components/KycSubmissionForm.tsx b/frontend/src/components/KycSubmissionForm.tsx
index 844d9e45..a59cfc00 100644
--- a/frontend/src/components/KycSubmissionForm.tsx
+++ b/frontend/src/components/KycSubmissionForm.tsx
@@ -20,6 +20,12 @@ const STEP_LABEL_KEYS: Record = {
review: "review",
};
+const STEP_STATUS_KEYS: Record = {
+ completed: "completed",
+ current: "current",
+ upcoming: "upcoming",
+};
+
const stepVariants: Variants = {
enter: (dir: number) => ({
x: dir > 0 ? 48 : -48,
@@ -78,7 +84,7 @@ function Field({
}
function KycSubmissionForm() {
- const t = useTranslations();
+ const t = useTranslations("kycForm");
const uid = useId();
const [state, dispatch] = useReducer(kycFlowReducer, initialKycFlowState);
const [direction, setDirection] = useState(1);
@@ -91,8 +97,8 @@ function KycSubmissionForm() {
const errs: Record = {};
if (state.currentStep === "personal") {
- if (!state.personal.firstName.trim()) errs.firstName = t("required") || "Required";
- if (!state.personal.lastName.trim()) errs.lastName = t("required") || "Required";
+ if (!state.personal.firstName.trim()) errs.firstName = t("required");
+ if (!state.personal.lastName.trim()) errs.lastName = t("required");
}
setStepErrors(errs);
@@ -101,7 +107,7 @@ function KycSubmissionForm() {
const goNext = useCallback(() => {
if (!validateCurrentStep()) {
- setAnnouncement(t("validationError") || "Please fill required fields");
+ setAnnouncement(t("validationError"));
return;
}
if (stepIndex < TOTAL_STEPS - 1) {
@@ -110,7 +116,7 @@ function KycSubmissionForm() {
setStepErrors({});
const nextStep = STEPS[stepIndex + 1]!;
setAnnouncement(
- `${t("step") || "Step"} ${stepIndex + 2} ${t("of") || "of"} ${TOTAL_STEPS}: ${t(STEP_LABEL_KEYS[nextStep]) || nextStep}`,
+ `${t("step")} ${stepIndex + 2} ${t("of")} ${TOTAL_STEPS}: ${t(STEP_LABEL_KEYS[nextStep])}`,
);
}
}, [validateCurrentStep, stepIndex, t]);
@@ -125,7 +131,7 @@ function KycSubmissionForm() {
const handleSubmit = useCallback(async () => {
dispatch({ type: "SUBMIT" });
- setAnnouncement(t("submitting") || "Submitting...");
+ setAnnouncement(t("submitting"));
try {
const res = await fetch("/api/kyc", {
@@ -141,11 +147,11 @@ function KycSubmissionForm() {
}),
});
- if (!res.ok) throw new Error(t("submitError") || "Submission failed");
+ if (!res.ok) throw new Error(t("submitError"));
dispatch({ type: "SUBMIT_SUCCESS", submittedAt: new Date().toISOString() });
- setAnnouncement(t("successTitle") || "KYC submitted successfully!");
- toast.success(t("successTitle") || "KYC submitted successfully!");
+ setAnnouncement(t("successTitle"));
+ toast.success(t("successTitle"));
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
dispatch({ type: "SUBMIT_FAILURE", error: msg });
@@ -159,7 +165,7 @@ function KycSubmissionForm() {
{announcement}
@@ -195,10 +201,7 @@ function KycSubmissionForm() {
{t("successTitle")}
-
- {t("successDescription") ||
- "Your KYC verification has been submitted and is under review."}
-
+
{t("successDescription")}