diff --git a/src/app/verify-identity/page.tsx b/src/app/verify-identity/page.tsx new file mode 100644 index 0000000..39f7e8c --- /dev/null +++ b/src/app/verify-identity/page.tsx @@ -0,0 +1,18 @@ +import IdentityVerificationWizard from "@/components/identity/IdentityVerificationWizard"; + +export default function VerifyIdentityPage() { + return ( +
+ + Onboarding + +

Verify your identity

+

+ A few quick steps to confirm who you are. You can save your progress and come back any + time before submitting. +

+ + +
+ ); +} diff --git a/src/components/ProfileForm.tsx b/src/components/ProfileForm.tsx index 59cc322..502621a 100644 --- a/src/components/ProfileForm.tsx +++ b/src/components/ProfileForm.tsx @@ -1,12 +1,14 @@ "use client"; import { useState } from "react"; +import Link from "next/link"; import { useForm } from "react-hook-form"; import { useDropzone } from "react-dropzone"; import { HiHome, HiCalendar, HiUser, + HiShieldCheck, HiLockClosed, HiMail, HiQuestionMarkCircle, @@ -30,6 +32,7 @@ const navItems = [ { icon: HiHome, label: "Dashboard" }, { icon: HiCalendar, label: "Appointment" }, { icon: HiUser, label: "My Profile", active: true }, + { icon: HiShieldCheck, label: "Verify Identity", href: "/verify-identity" }, { icon: HiLockClosed, label: "Login Details" }, { icon: HiMail, label: "Message" }, { icon: HiQuestionMarkCircle, label: "Help" }, @@ -67,16 +70,24 @@ export default function ProfileForm() {

My Account

diff --git a/src/components/identity/DocumentDropzone.tsx b/src/components/identity/DocumentDropzone.tsx new file mode 100644 index 0000000..efdf420 --- /dev/null +++ b/src/components/identity/DocumentDropzone.tsx @@ -0,0 +1,49 @@ +import { useDropzone } from "react-dropzone"; +import { HiCheckCircle, HiUpload } from "react-icons/hi"; + +interface DocumentDropzoneProps { + label: string; + hint: string; + file: File | null; + onSelect: (file: File) => void; + error?: string; +} + +/** A single labeled image dropzone — shared by the document-upload and + selfie steps so both look and behave the same way. */ +export default function DocumentDropzone({ label, hint, file, onSelect, error }: DocumentDropzoneProps) { + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop: (accepted) => { + if (accepted[0]) onSelect(accepted[0]); + }, + accept: { "image/*": [] }, + maxFiles: 1, + }); + + return ( +
+ {label} +
+ + {file ? ( +
+ +

{file.name}

+

Click or drop to replace

+
+ ) : ( +
+ +

{isDragActive ? "Drop the image here..." : hint}

+
+ )} +
+ {error &&

{error}

} +
+ ); +} diff --git a/src/components/identity/IdentityStepper.tsx b/src/components/identity/IdentityStepper.tsx new file mode 100644 index 0000000..fa7723a --- /dev/null +++ b/src/components/identity/IdentityStepper.tsx @@ -0,0 +1,51 @@ +import { HiCheck } from "react-icons/hi"; +import { STEP_IDS, STEP_LABELS, type StepId } from "@/lib/identityVerification"; + +interface IdentityStepperProps { + currentIndex: number; + /** Highest index the user has successfully validated up to — steps at or + before this are clickable, steps beyond it are disclosed but inert + (progressive disclosure: visible so users know what's ahead, not yet + reachable until earlier steps are complete). */ + furthestValidatedIndex: number; + onStepSelect: (index: number) => void; +} + +export default function IdentityStepper({ + currentIndex, + furthestValidatedIndex, + onStepSelect, +}: IdentityStepperProps) { + return ( +
    + {STEP_IDS.map((id: StepId, index) => { + const isCurrent = index === currentIndex; + const isCompleted = index < furthestValidatedIndex || (index === furthestValidatedIndex && index < currentIndex); + const isReachable = index <= furthestValidatedIndex; + + return ( +
  1. + + + {STEP_LABELS[id]} + +
  2. + ); + })} +
+ ); +} diff --git a/src/components/identity/IdentityVerificationWizard.tsx b/src/components/identity/IdentityVerificationWizard.tsx new file mode 100644 index 0000000..16968df --- /dev/null +++ b/src/components/identity/IdentityVerificationWizard.tsx @@ -0,0 +1,288 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { FormProvider, useForm, type FieldPath } from "react-hook-form"; +import { HiExclamationCircle } from "react-icons/hi"; +import Button from "@/components/ui/Button"; +import Card from "@/components/ui/Card"; +import Badge from "@/components/ui/Badge"; +import IdentityStepper from "./IdentityStepper"; +import PersonalDetailsStep from "./steps/PersonalDetailsStep"; +import DocumentSelectionStep from "./steps/DocumentSelectionStep"; +import DocumentUploadStep from "./steps/DocumentUploadStep"; +import SelfieStep from "./steps/SelfieStep"; +import ReviewStep from "./steps/ReviewStep"; +import { + DOCUMENT_TYPES_REQUIRING_BACK, + EMPTY_FORM_VALUES, + STEP_IDS, + STEP_VALIDATION_FIELDS, + clearProgress, + loadProgress, + saveProgress, + submitIdentityVerification, + type IdentityVerificationFormValues, + type StepId, +} from "@/lib/identityVerification"; + +type UploadKey = "front" | "back" | "selfie"; + +const UPLOAD_STEP_INDEX = STEP_IDS.indexOf("upload"); +const REVIEW_STEP_INDEX = STEP_IDS.indexOf("review"); + +export default function IdentityVerificationWizard() { + const form = useForm({ + mode: "onChange", + defaultValues: EMPTY_FORM_VALUES, + }); + const { trigger, watch, reset } = form; + + const [stepIndex, setStepIndex] = useState(0); + const [furthestValidatedIndex, setFurthestValidatedIndex] = useState(0); + const [files, setFiles] = useState>({ + front: null, + back: null, + selfie: null, + }); + const [fileErrors, setFileErrors] = useState>>({}); + const [resumeState, setResumeState] = useState<"checking" | "offered" | "dismissed">("checking"); + const [resumeSummary, setResumeSummary] = useState<{ step: number; savedAt: string } | null>(null); + const [submission, setSubmission] = useState< + { status: "idle" | "submitting" } | { status: "error"; message: string } | { status: "success"; referenceId: string } + >({ status: "idle" }); + const [needsReattachNotice, setNeedsReattachNotice] = useState(false); + + const saveTimer = useRef | null>(null); + + // Offer to resume a saved session once, on mount. + useEffect(() => { + const saved = loadProgress(); + if (saved) { + setResumeSummary({ step: saved.step, savedAt: saved.savedAt }); + setResumeState("offered"); + } else { + setResumeState("dismissed"); + } + }, []); + + // Debounce-persist step data (never file contents) whenever it changes. + useEffect(() => { + if (resumeState === "checking") return; + const subscription = watch((values) => { + if (saveTimer.current) clearTimeout(saveTimer.current); + saveTimer.current = setTimeout(() => { + saveProgress({ + step: stepIndex, + values: values as IdentityVerificationFormValues, + attachedFileNames: { + front: files.front?.name, + back: files.back?.name, + selfie: files.selfie?.name, + }, + }); + }, 500); + }); + return () => { + subscription.unsubscribe(); + if (saveTimer.current) clearTimeout(saveTimer.current); + }; + }, [watch, stepIndex, files, resumeState]); + + function handleResume() { + const saved = loadProgress(); + if (!saved) return; + reset(saved.values); + // Files can't be restored (see lib doc comment) — if the saved session + // had gotten past the upload/selfie steps, send the user back to + // whichever of those comes first so they can re-attach before + // continuing, rather than silently landing past a step with no file. + const hadUploads = Object.keys(saved.attachedFileNames).length > 0; + const targetStep = + hadUploads && saved.step > UPLOAD_STEP_INDEX ? UPLOAD_STEP_INDEX : saved.step; + setNeedsReattachNotice(hadUploads && saved.step > UPLOAD_STEP_INDEX); + setStepIndex(targetStep); + setFurthestValidatedIndex(Math.max(targetStep, saved.step > UPLOAD_STEP_INDEX ? UPLOAD_STEP_INDEX : saved.step)); + setResumeState("dismissed"); + } + + function handleStartOver() { + clearProgress(); + reset(EMPTY_FORM_VALUES); + setFiles({ front: null, back: null, selfie: null }); + setStepIndex(0); + setFurthestValidatedIndex(0); + setResumeState("dismissed"); + } + + function validateUploadStep(): boolean { + const documentType = form.getValues("document.documentType"); + const needsBack = DOCUMENT_TYPES_REQUIRING_BACK.includes( + documentType as (typeof DOCUMENT_TYPES_REQUIRING_BACK)[number] + ); + const errors: Partial> = {}; + if (!files.front) errors.front = "Please upload the front of your document"; + if (needsBack && !files.back) errors.back = "Please upload the back of your document"; + setFileErrors(errors); + return Object.keys(errors).length === 0; + } + + function validateSelfieStep(): boolean { + if (!files.selfie) { + setFileErrors({ selfie: "Please upload a selfie" }); + return false; + } + setFileErrors({}); + return true; + } + + async function handleContinue() { + const stepId: StepId = STEP_IDS[stepIndex]; + let valid = true; + + const fieldsToValidate = STEP_VALIDATION_FIELDS[stepId]; + if (fieldsToValidate) { + valid = await trigger(fieldsToValidate as FieldPath[]); + } else if (stepId === "upload") { + valid = validateUploadStep(); + } else if (stepId === "selfie") { + valid = validateSelfieStep(); + } + + if (!valid) return; + + setNeedsReattachNotice(false); + const nextIndex = Math.min(stepIndex + 1, STEP_IDS.length - 1); + setStepIndex(nextIndex); + setFurthestValidatedIndex((prev) => Math.max(prev, nextIndex)); + } + + function handleBack() { + setStepIndex((prev) => Math.max(prev - 1, 0)); + } + + function handleStepSelect(index: number) { + if (index <= furthestValidatedIndex) setStepIndex(index); + } + + async function handleSubmit() { + setSubmission({ status: "submitting" }); + try { + const values = form.getValues(); + const result = await submitIdentityVerification({ + ...values, + hasFrontUpload: Boolean(files.front), + hasBackUpload: Boolean(files.back), + hasSelfie: Boolean(files.selfie), + }); + clearProgress(); + setSubmission({ status: "success", referenceId: result.referenceId }); + } catch (error) { + setSubmission({ + status: "error", + message: error instanceof Error ? error.message : "Something went wrong. Please try again.", + }); + } + } + + if (submission.status === "success") { + return ( + + + Pending review + +

Verification submitted

+

+ Reference {submission.referenceId}. We'll + notify you once it's been reviewed — this usually takes 1–2 business days. +

+
+ ); + } + + const stepId = STEP_IDS[stepIndex]; + const isLastStep = stepIndex === REVIEW_STEP_INDEX; + + return ( + + {resumeState === "offered" && resumeSummary && ( +
+ + You have an unfinished verification from{" "} + {new Date(resumeSummary.savedAt).toLocaleString()}. + +
+ + +
+
+ )} + + {needsReattachNotice && ( +
+ For your security, we don't save uploaded documents between sessions — please + re-attach your document and selfie to continue. +
+ )} + + + + +
+ {stepId === "personal" && } + {stepId === "document" && } + {stepId === "upload" && ( + setFiles((prev) => ({ ...prev, front: file }))} + onBackChange={(file) => setFiles((prev) => ({ ...prev, back: file }))} + errors={fileErrors} + /> + )} + {stepId === "selfie" && ( + setFiles((prev) => ({ ...prev, selfie: file }))} + error={fileErrors.selfie} + /> + )} + {stepId === "review" && ( + + )} +
+
+ + {submission.status === "error" && ( +
+ + {submission.message} +
+ )} + +
+ + {isLastStep ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/src/components/identity/steps/DocumentSelectionStep.tsx b/src/components/identity/steps/DocumentSelectionStep.tsx new file mode 100644 index 0000000..4cb761a --- /dev/null +++ b/src/components/identity/steps/DocumentSelectionStep.tsx @@ -0,0 +1,41 @@ +import { useFormContext } from "react-hook-form"; +import Input from "@/components/ui/Input"; +import Select from "@/components/ui/Select"; +import type { IdentityVerificationFormValues } from "@/lib/identityVerification"; + +export default function DocumentSelectionStep() { + const { + register, + formState: { errors }, + } = useFormContext(); + + return ( +
+
+ + {errors.document?.documentType && ( +

{errors.document.documentType.message}

+ )} +
+ +
+ + {errors.document?.documentNumber && ( +

{errors.document.documentNumber.message}

+ )} +
+
+ ); +} diff --git a/src/components/identity/steps/DocumentUploadStep.tsx b/src/components/identity/steps/DocumentUploadStep.tsx new file mode 100644 index 0000000..15736d8 --- /dev/null +++ b/src/components/identity/steps/DocumentUploadStep.tsx @@ -0,0 +1,54 @@ +import { useFormContext } from "react-hook-form"; +import DocumentDropzone from "@/components/identity/DocumentDropzone"; +import { + DOCUMENT_TYPES_REQUIRING_BACK, + type IdentityVerificationFormValues, +} from "@/lib/identityVerification"; + +interface DocumentUploadStepProps { + frontFile: File | null; + backFile: File | null; + onFrontChange: (file: File) => void; + onBackChange: (file: File) => void; + errors: { front?: string; back?: string }; +} + +export default function DocumentUploadStep({ + frontFile, + backFile, + onFrontChange, + onBackChange, + errors, +}: DocumentUploadStepProps) { + const { watch } = useFormContext(); + const documentType = watch("document.documentType"); + const needsBack = DOCUMENT_TYPES_REQUIRING_BACK.includes( + documentType as (typeof DOCUMENT_TYPES_REQUIRING_BACK)[number] + ); + + return ( +
+ + + {/* Progressive disclosure: the back-of-document upload only appears + once a document type that actually has a back (national ID, + driver's license) has been chosen — a passport's single data page + never needs it. */} + {needsBack && ( + + )} +
+ ); +} diff --git a/src/components/identity/steps/PersonalDetailsStep.tsx b/src/components/identity/steps/PersonalDetailsStep.tsx new file mode 100644 index 0000000..a5a0e42 --- /dev/null +++ b/src/components/identity/steps/PersonalDetailsStep.tsx @@ -0,0 +1,79 @@ +import { useFormContext } from "react-hook-form"; +import Input from "@/components/ui/Input"; +import Select from "@/components/ui/Select"; +import type { IdentityVerificationFormValues } from "@/lib/identityVerification"; + +const NATIONALITIES = [ + "Nigeria", + "Ghana", + "Kenya", + "South Africa", + "United Kingdom", + "United States", + "Other", +]; + +export default function PersonalDetailsStep() { + const { + register, + formState: { errors }, + } = useFormContext(); + + return ( +
+
+ + {errors.personal?.fullName && ( +

{errors.personal.fullName.message}

+ )} +
+ +
+ + {errors.personal?.dateOfBirth && ( +

{errors.personal.dateOfBirth.message}

+ )} +
+ +
+ + {errors.personal?.nationality && ( +

{errors.personal.nationality.message}

+ )} +
+ +
+ + {errors.personal?.phoneNumber && ( +

{errors.personal.phoneNumber.message}

+ )} +
+
+ ); +} diff --git a/src/components/identity/steps/ReviewStep.tsx b/src/components/identity/steps/ReviewStep.tsx new file mode 100644 index 0000000..e005ba5 --- /dev/null +++ b/src/components/identity/steps/ReviewStep.tsx @@ -0,0 +1,58 @@ +import { useFormContext } from "react-hook-form"; +import type { IdentityVerificationFormValues } from "@/lib/identityVerification"; + +interface ReviewStepProps { + frontFile: File | null; + backFile: File | null; + selfieFile: File | null; +} + +function ReviewRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value || "—"} +
+ ); +} + +const DOCUMENT_TYPE_LABELS: Record = { + "national-id": "National ID", + passport: "Passport", + "drivers-license": "Driver's license", +}; + +export default function ReviewStep({ frontFile, backFile, selfieFile }: ReviewStepProps) { + const { getValues } = useFormContext(); + const { personal, document } = getValues(); + + return ( +
+
+

+ Personal details +

+ + + + +
+ +
+

+ Document +

+ + + + {backFile && } + +
+ +

+ By submitting, you confirm the information above is accurate and consent to it being used + to verify your identity. +

+
+ ); +} diff --git a/src/components/identity/steps/SelfieStep.tsx b/src/components/identity/steps/SelfieStep.tsx new file mode 100644 index 0000000..2373a6c --- /dev/null +++ b/src/components/identity/steps/SelfieStep.tsx @@ -0,0 +1,25 @@ +import DocumentDropzone from "@/components/identity/DocumentDropzone"; + +interface SelfieStepProps { + selfieFile: File | null; + onSelfieChange: (file: File) => void; + error?: string; +} + +export default function SelfieStep({ selfieFile, onSelfieChange, error }: SelfieStepProps) { + return ( +
+

+ Take a clear, well-lit photo of your face. Remove sunglasses or hats, and make sure + there's no glare over your eyes. +

+ +
+ ); +} diff --git a/src/lib/identityVerification.ts b/src/lib/identityVerification.ts new file mode 100644 index 0000000..0ac7faf --- /dev/null +++ b/src/lib/identityVerification.ts @@ -0,0 +1,144 @@ +/** + * Onboarding identity verification wizard — state shape, save-and-resume + * persistence, and the submission call. + * + * WHY THIS IS A FRONTEND-ONLY STUB (read before wiring a real endpoint) + * `guildworkman-core` doesn't expose an identity-verification endpoint yet — + * only `/api/v1/client/*`, `/api/v1/skilledWorker/*`, and `/api/v1/auth/*` + * (see `src/lib/api.ts`). Rather than invent a REST shape the backend can't + * answer, `submitIdentityVerification` below simulates the round trip + * (latency + a success/failure outcome) so the wizard's step validation, + * save-and-resume, and error-recovery UI are fully exercised today. Swap its + * body for a real `postJson(...)` call in `lib/api.ts` once the backend adds + * the route — the wizard component doesn't need to change, only this + * function's implementation. + * + * WHY UPLOADED FILES ARE NOT PERSISTED + * `localStorage` is the only realistic client-side store for save-and-resume + * (it's what `slotLock.ts` already uses), but it's a poor — and, for a photo + * ID or a selfie, actively unsafe — place to keep sensitive document images: + * it's unencrypted, has no expiry of its own, and a multi-MB data URL risks + * blowing the ~5MB quota outright. So only non-sensitive step data (names, + * document numbers, which step you were on) is persisted; uploaded files + * live in React state only. Resuming after a reload restores every answered + * field but asks the user to re-attach documents/selfie — a deliberate + * trade-off of a little resume friction for not writing ID photos to disk. + */ + +export type DocumentType = "national-id" | "passport" | "drivers-license"; + +export interface PersonalDetails { + fullName: string; + dateOfBirth: string; + nationality: string; + phoneNumber: string; +} + +export interface DocumentDetails { + documentType: DocumentType | ""; + documentNumber: string; +} + +/** Document types whose back side actually carries information; passports + are a single data page, so we don't ask for a "back" scan of one. */ +export const DOCUMENT_TYPES_REQUIRING_BACK: DocumentType[] = ["national-id", "drivers-license"]; + +export interface IdentityVerificationFormValues { + personal: PersonalDetails; + document: DocumentDetails; +} + +export const EMPTY_FORM_VALUES: IdentityVerificationFormValues = { + personal: { fullName: "", dateOfBirth: "", nationality: "", phoneNumber: "" }, + document: { documentType: "", documentNumber: "" }, +}; + +export const STEP_IDS = ["personal", "document", "upload", "selfie", "review"] as const; +export type StepId = (typeof STEP_IDS)[number]; + +export const STEP_LABELS: Record = { + personal: "Personal details", + document: "Document type", + upload: "Upload document", + selfie: "Selfie check", + review: "Review & submit", +}; + +/** Field paths (dot-notation, matching react-hook-form's `trigger` API) + validated before a step is allowed to advance. Upload/selfie/review are + validated separately since they gate on files held in component state, + not registered form fields. */ +export const STEP_VALIDATION_FIELDS: Partial> = { + personal: ["personal.fullName", "personal.dateOfBirth", "personal.nationality", "personal.phoneNumber"], + document: ["document.documentType", "document.documentNumber"], +}; + +const STORAGE_KEY = "gw-identity-verification-v1"; + +export interface PersistedProgress { + step: number; + values: IdentityVerificationFormValues; + /** Filenames only, so a resumed session can show "you'd attached X" — + never the file contents themselves (see module doc comment). */ + attachedFileNames: Partial>; + savedAt: string; +} + +export function loadProgress(): PersistedProgress | null { + if (typeof window === "undefined") return null; + try { + const raw = window.localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + return JSON.parse(raw) as PersistedProgress; + } catch { + return null; + } +} + +export function saveProgress(progress: Omit): void { + if (typeof window === "undefined") return; + const record: PersistedProgress = { ...progress, savedAt: new Date().toISOString() }; + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(record)); + } catch { + // Quota exceeded or storage disabled — resume just won't be available. + } +} + +export function clearProgress(): void { + if (typeof window === "undefined") return; + window.localStorage.removeItem(STORAGE_KEY); +} + +export interface IdentityVerificationPayload extends IdentityVerificationFormValues { + hasFrontUpload: boolean; + hasBackUpload: boolean; + hasSelfie: boolean; +} + +export interface IdentityVerificationResult { + referenceId: string; + status: "pending-review"; +} + +/** Simulated submission — see the module doc comment for why this doesn't + call a real backend endpoint yet. Fails roughly 1 in 6 tries so the + wizard's error-recovery path (inline error + retry, form left intact) is + reachable without special test hooks. */ +export function submitIdentityVerification( + payload: IdentityVerificationPayload +): Promise { + void payload; // not sent anywhere yet — see module doc comment + return new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() < 1 / 6) { + reject(new Error("We couldn't reach the verification service. Please try again.")); + return; + } + resolve({ + referenceId: `IDV-${Date.now().toString(36).toUpperCase()}`, + status: "pending-review", + }); + }, 1200); + }); +} diff --git a/src/lib/test/identityVerification.test.ts b/src/lib/test/identityVerification.test.ts new file mode 100644 index 0000000..b4f2a80 --- /dev/null +++ b/src/lib/test/identityVerification.test.ts @@ -0,0 +1,62 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { + DOCUMENT_TYPES_REQUIRING_BACK, + EMPTY_FORM_VALUES, + clearProgress, + loadProgress, + saveProgress, +} from "../identityVerification"; + +describe("saveProgress / loadProgress / clearProgress", () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + it("returns null when nothing has been saved", () => { + expect(loadProgress()).toBeNull(); + }); + + it("round-trips step index and form values", () => { + saveProgress({ + step: 2, + values: { + ...EMPTY_FORM_VALUES, + personal: { ...EMPTY_FORM_VALUES.personal, fullName: "Ada Lovelace" }, + }, + attachedFileNames: { front: "id-front.jpg" }, + }); + + const loaded = loadProgress(); + expect(loaded?.step).toBe(2); + expect(loaded?.values.personal.fullName).toBe("Ada Lovelace"); + expect(loaded?.attachedFileNames.front).toBe("id-front.jpg"); + expect(loaded?.savedAt).toBeTruthy(); + }); + + it("never persists file contents, only filenames", () => { + saveProgress({ step: 3, values: EMPTY_FORM_VALUES, attachedFileNames: { selfie: "me.png" } }); + const raw = window.localStorage.getItem("gw-identity-verification-v1") ?? ""; + expect(raw).toContain("me.png"); + expect(raw).not.toContain("data:image"); + }); + + it("clears a saved session", () => { + saveProgress({ step: 1, values: EMPTY_FORM_VALUES, attachedFileNames: {} }); + expect(loadProgress()).not.toBeNull(); + clearProgress(); + expect(loadProgress()).toBeNull(); + }); + + it("recovers gracefully from corrupted storage", () => { + window.localStorage.setItem("gw-identity-verification-v1", "{not-json"); + expect(loadProgress()).toBeNull(); + }); +}); + +describe("DOCUMENT_TYPES_REQUIRING_BACK", () => { + it("requires a back scan for national ID and driver's license, not passport", () => { + expect(DOCUMENT_TYPES_REQUIRING_BACK).toContain("national-id"); + expect(DOCUMENT_TYPES_REQUIRING_BACK).toContain("drivers-license"); + expect(DOCUMENT_TYPES_REQUIRING_BACK).not.toContain("passport"); + }); +});