Identity verification (KYC) · Vektos
Tasks / Identity verification (KYC) Identity step of account opening: known details read-only, required personal and document fields with per-field errors, confirmation on submit.
The brief What the agent reads, word for word.
What a good answer has The page must show these texts, or the run is not done. A good answer uses these components.
Best and worst runs By design-system violations.
By context and model Mean design-system violations per run on this task.
The reference answer Written by hand, graded with zero violations, rendered by the browser check in every build. The agent never sees it.
13 Select,
14 Stack,
15 } from "@vektos/ds";
16 import { type FormEvent, useState } from "react";
17 import { css } from "styled-system/css";
18 import data from "./fixture.json";
19
20 const { customer } = data;
21
22 const countries = data.countries.map((country) => ({ label: country.name, value: country.code }));
23 const pepOptions = [
24 { label: "No", value: "no" },
25 { label: "Yes", value: "yes" },
26 ];
27
28 const fields = {
29 birthDate: "Date of birth",
30 nationality: "Nationality",
31 residence: "Country of residence",
32 taxId: "Tax identification number",
33 documentType: "Identity document",
34 documentNumber: "Document number",
35 pep: "Politically exposed person",
36 } as const;
37
38 type Field = keyof typeof fields;
39 const isField = (key: string): key is Field => key in fields;
40 type Values = Record<Field, string>;
41
42 const empty: Values = {
43 birthDate: "",
44 nationality: "",
45 residence: "",
46 taxId: "",
47 documentType: "",
48 documentNumber: "",
49 pep: "",
50 };
51
52 export default function KycPage() {
53 const [values, setValues] = useState<Values>(empty);
54 const [errors, setErrors] = useState<Partial<Record<Field, string>>>({});
55 const [submitted, setSubmitted] = useState(false);
56
57 const set = (field: Field) => (value: string) => {
58 setValues((current) => ({ ...current, [field]: value }));
59 setErrors((current) => ({ ...current, [field]: undefined }));
60 };
61
62 function submit(event: FormEvent<HTMLFormElement>) {
63 event.preventDefault();
64 const missing: Partial<Record<Field, string>> = {};
65 for (const field of Object.keys(fields).filter(isField)) {
66 if (values[field].trim() === "")
67 missing[field] = `Enter your ${fields[field].toLowerCase()}.`;
68 }
69 setErrors(missing);
70 if (Object.keys(missing).length === 0) setSubmitted(true);
71 }
72
73 if (submitted) {
74 return (
75 <main className={css({ p: "6" })}>
76 <Card maxWidth="lg">
77 <CardHeader>
78 <CardTitle asChild>
79 <h1>Thank you, {customer.firstName}</h1>
80 </CardTitle>
81 <CardDescription>
82 We are reviewing your details. We will email you at {customer.email} when your account
83 is ready.
84 </CardDescription>
85 </CardHeader>
86 </Card>
87 </main>
88 );
89 }
90
91 return (
92 <main className={css({ p: "6" })}>
93 <form onSubmit={submit} noValidate className={css({ maxWidth: "lg" })}>
94 <Card>
95 <CardHeader>
96 <CardTitle asChild>
97 <h1>Verify your identity</h1>
98 </CardTitle>
99 <CardDescription>Required by anti-money-laundering rules.</CardDescription>
100 </CardHeader>
101 <CardBody>
102 <Stack gap="4">
103 <FormField label="Full name" readOnly>
104 <Input value={`${customer.firstName} ${customer.lastName}`} readOnly />
105 </FormField>
106 <FormField label="Email" readOnly>
107 <Input value={customer.email} readOnly />
108 </FormField>
109 <FormField label={fields.birthDate} required error={errors.birthDate}>
110 <Input
111 type="date"
112 value={values.birthDate}
113 onChange={(event) => set("birthDate")(event.target.value)}
114 />
115 </FormField>
116 <FormField label={fields.nationality} required error={errors.nationality}>
117 <Select
118 options={countries}
119 value={values.nationality}
120 onValueChange={set("nationality")}
121 placeholder="Select a country"
122 />
123 </FormField>
124 <FormField label={fields.residence} required error={errors.residence}>
125 <Select
126 options={countries}
127 value={values.residence}
128 onValueChange={set("residence")}
129 placeholder="Select a country"
130 />
131 </FormField>
132 <FormField label={fields.taxId} required error={errors.taxId}>
133 <Input
134 value={values.taxId}
135 onChange={(event) => set("taxId")(event.target.value)}
136 />
137 </FormField>
138 <FormField label={fields.documentType} required error={errors.documentType}>
139 <Select
140 options={data.documentTypes}
141 value={values.documentType}
142 onValueChange={set("documentType")}
143 placeholder="Select a document"
144 />
145 </FormField>
146 <FormField label={fields.documentNumber} required error={errors.documentNumber}>
147 <Input
148 value={values.documentNumber}
149 onChange={(event) => set("documentNumber")(event.target.value)}
150 />
151 </FormField>
152 <FormField
153 label={fields.pep}
154 required
155 error={errors.pep}
156 helperText="A person with a prominent public role, or a close relative of one."
157 >
158 <Select options={pepOptions} value={values.pep} onValueChange={set("pep")} />
159 </FormField>
160 </Stack>
161 </CardBody>
162 <CardFooter>
163 <Button type="submit">Submit for review</Button>
164 </CardFooter>
165 </Card>
166 </form>
167 </main>
168 );
169 }
170