diff --git a/frontend/src/app/(authenticated)/kyc/page.tsx b/frontend/src/app/(authenticated)/kyc/page.tsx index 77586800..77587f61 100644 --- a/frontend/src/app/(authenticated)/kyc/page.tsx +++ b/frontend/src/app/(authenticated)/kyc/page.tsx @@ -1,11 +1,35 @@ -import { Metadata } from "next"; -import KycPageContent from "@/components/KycPageContent"; - -export const metadata: Metadata = { - title: "KYC Verification | PLUTO", - description: "Complete your KYC verification to unlock full platform features", -}; - -export default function KycPage() { - return ; -} +/** + * KYC Verification Page — React Server Component + * + * RSC migration: + * - generateMetadata uses next-intl's getTranslations so title/description + * are resolved from the active locale on the server, no client JS needed. + * - The page itself is a pure async Server Component: zero client bundle cost. + * - KycPageContent is also an RSC; the interactive form is pushed down to the + * minimum "use client" leaf (KycSubmissionForm). + */ + +import { type Metadata } from "next"; +import { getTranslations } from "next-intl/server"; +import KycPageContent from "@/components/KycPageContent"; + +// ── i18n-aware metadata ─────────────────────────────────────────────────────── + +export async function generateMetadata(): Promise { + const t = await getTranslations("kycPage"); + + return { + title: `${t("title")} | PLUTO`, + description: t("description"), + openGraph: { + title: `${t("title")} | PLUTO`, + description: t("description"), + }, + }; +} + +// ── Page — pure RSC ─────────────────────────────────────────────────────────── + +export default async function KycPage() { + return ; +} diff --git a/frontend/src/components/KycFormShell.tsx b/frontend/src/components/KycFormShell.tsx new file mode 100644 index 00000000..8638f0fa --- /dev/null +++ b/frontend/src/components/KycFormShell.tsx @@ -0,0 +1,69 @@ +/** + * KycFormShell — React Server Component + * + * Renders the static chrome around the KYC form on the server: + * - Card border/background container + * - Any server-resolved props forwarded to the client form + * + * The interactive form (KycSubmissionForm) is imported as a dynamic client + * component wrapped in so the static shell streams immediately + * while the client bundle loads. KycFormSkeleton is the Suspense fallback. + * + * Why a separate shell instead of inlining in KycPageContent? + * - Isolates the Suspense boundary so only the form stream is deferred. + * - Makes the "use client" boundary explicit and easy to audit. + * - Allows passing serialisable server-resolved props (locale, initial values + * from session, feature flags) to the client form without a round-trip. + */ + +import { Suspense } from "react"; +import { getTranslations } from "next-intl/server"; +import dynamic from "next/dynamic"; +import KycFormSkeleton from "@/components/KycFormSkeleton"; +import type { KycInitialValues } from "@/components/KycSubmissionForm"; + +// Lazy-load the client form — only sent to the browser when needed. +// ssr: false ensures it never runs during server rendering (it uses browser +// APIs like useId, useState, useReducer). +const KycSubmissionForm = dynamic( + () => import("@/components/KycSubmissionForm"), + { + ssr: false, + loading: () => , + }, +); + +// ── Props ───────────────────────────────────────────────────────────────────── + +interface KycFormShellProps { + /** + * Optional server-resolved initial values (e.g. pre-filled from a session + * or a previous incomplete submission). Only serialisable primitives — no + * File objects. + */ + initialValues?: KycInitialValues; +} + +// ── Component ───────────────────────────────────────────────────────────────── + +export default async function KycFormShell({ initialValues }: KycFormShellProps) { + // Resolve the form title server-side for the accessible landmark label. + const t = await getTranslations("kycForm"); + const formTitle = t("formTitle"); + + return ( + /* + * The outer div gives the shell its visual card styling. + * The role/aria-label is forwarded as a data attribute so the client + * form can apply it on mount without a hydration mismatch. + */ +
+ }> + + +
+ ); +} diff --git a/frontend/src/components/KycFormSkeleton.tsx b/frontend/src/components/KycFormSkeleton.tsx new file mode 100644 index 00000000..a9a266e0 --- /dev/null +++ b/frontend/src/components/KycFormSkeleton.tsx @@ -0,0 +1,111 @@ +/** + * KycFormSkeleton + * + * Server-safe Suspense fallback rendered while the KycSubmissionForm client + * bundle is streaming in. Uses plain CSS classes (no framer-motion, no hooks) + * so it is safe to render in a Server Component or as a Suspense boundary + * fallback. + * + * Structure mirrors the real form so the layout shift on hydration is minimal: + * - Progress bar row + * - Step indicator dots + * - Skeleton field rows (title + 4 fields) + * - Navigation button row + */ + +import React from "react"; + +// ── Shimmer bone ────────────────────────────────────────────────────────────── + +function Bone({ className = "" }: { className?: string }) { + return ( +