ekyc

Electronic KYC process with uploading ID document using OAuth 2.1 (experimental)
Log | Files | Refs | README | LICENSE

id-document.tsx (6088B)


      1 import { MRZInfo } from "#core/application/id_document/mrzscan.ts";
      2 import { PhotoCaptureInput } from "#http/islands/photo_capture_input.tsx";
      3 import { AppState } from "#http/routes/_middleware.ts";
      4 import { Handlers, PageProps } from "$fresh/src/server/types.ts";
      5 import * as V from "$valita";
      6 
      7 type Props = {
      8   side: "doc-front" | "doc-back" | "face-left" | "face-front" | "face-right";
      9   next: URL | null;
     10   back: URL | null;
     11   info: MRZInfo | null;
     12 };
     13 
     14 const NEXT = {
     15   "doc-front": "doc-back",
     16   "doc-back": "face-left",
     17   "face-left": "face-front",
     18   "face-front": "face-right",
     19   "face-right": false,
     20 };
     21 
     22 export const handler: Handlers<Props, AppState<"/register/id-document">> = {
     23   async GET(_req, ctx) {
     24     const { app, forms, formContext } = ctx.state;
     25     const { customerInfo } = app;
     26     if (formContext === null) {
     27       return forms.redirect({
     28         form: "/connect",
     29         context: {
     30           back: {
     31             form: "/register/id-document",
     32             context: { side: "doc-front", back: "/" },
     33           },
     34         },
     35       });
     36     }
     37     const { side, back } = formContext;
     38     if (forms.session === null) {
     39       return forms.redirect({
     40         form: "/connect",
     41         context: {
     42           back: {
     43             form: "/register/id-document",
     44             context: { side: "doc-front", back },
     45           },
     46         },
     47       });
     48     }
     49 
     50     const result = await customerInfo.execute({ uuid: forms.session.uuid });
     51     if (result.idDocumentRegistered) {
     52       return forms.redirect(back);
     53     }
     54 
     55     return ctx.render({
     56       side,
     57       next: forms.link({
     58         form: "/register/id-document",
     59         context: { side: NEXT[side] as never, back },
     60       }),
     61       back: null,
     62       info: null,
     63     });
     64   },
     65 
     66   async POST(req, ctx) {
     67     const { app, forms, formContext } = ctx.state;
     68     const { idDocumentCapture } = app;
     69 
     70     if (formContext === null) {
     71       return forms.redirect({ form: "/connect", context: { back: "/" } });
     72     }
     73 
     74     const { side, back } = formContext;
     75     if (forms.session === null) {
     76       return forms.redirect({
     77         form: "/connect",
     78         context: {
     79           back: {
     80             form: "/register/id-document",
     81             context: { side: "doc-front", back },
     82           },
     83         },
     84       });
     85     }
     86 
     87     const { picture } = await forms.inputs(
     88       req,
     89       V.object({ picture: V.string() }),
     90     );
     91 
     92     const result = await idDocumentCapture.execute({
     93       uuid: forms.session.uuid,
     94       side,
     95       picture,
     96     });
     97 
     98     if (result.status === "scanned") {
     99       return ctx.render({
    100         side,
    101         next: forms.link({
    102           form: "/register/id-document",
    103           context: { side: "face-left" as never, back },
    104         }),
    105         back: forms.link({
    106           form: "/register/id-document",
    107           context: { side: "doc-front" as never, back },
    108         }),
    109         info: result,
    110       });
    111     }
    112 
    113     if (result.status === "scan-failure") {
    114       return forms.redirect({
    115         form: "/connect",
    116         context: {
    117           back: {
    118             form: "/register/id-document",
    119             context: { side: "doc-back", back },
    120           },
    121         },
    122       });
    123     }
    124 
    125 
    126     if (result.status === "invalid") {
    127       return forms.redirect({
    128         form: "/connect",
    129         context: {
    130           back: {
    131             form: "/register/id-document",
    132             context: { side: "doc-front", back },
    133           },
    134         },
    135       });
    136     }
    137 
    138     if (result.status === "captured" && NEXT[side] !== false) {
    139       return forms.redirect({
    140         form: "/register/id-document",
    141         context: { side: NEXT[side] as never, back },
    142       });
    143     }
    144 
    145     return forms.redirect(back);
    146   },
    147 };
    148 
    149 const LABELS = {
    150   "doc-front": "Take photo of ID document front side",
    151   "doc-back": "Take photo of ID document back side",
    152   "face-left": "Take photo of you facing left",
    153   "face-front": "Take photo of you facing front",
    154   "face-right": "Take photo of you facing right",
    155 };
    156 
    157 export default function RegisterPages({ data }: PageProps<Props>) {
    158   if (data.info !== null) {
    159     return (
    160       <article>
    161         <header style="text-align: center;">
    162           <b>ID Information</b>
    163         </header>
    164         <div>
    165           <table>
    166             <tr>
    167               <th>
    168                 <b>First name</b>
    169               </th>
    170               <td>{data.info.firstName ? data.info.firstName : "—"}</td>
    171             </tr>
    172             <tr>
    173               <th>
    174                 <b>Last name</b>
    175               </th>
    176               <td>{data.info.lastName ? data.info.lastName : "—"}</td>
    177             </tr>
    178             <tr>
    179               <th>
    180                 <b>Sex</b>
    181               </th>
    182               <td>{data.info.sex ? data.info.sex : "—"}</td>
    183             </tr>
    184             <tr>
    185               <th>
    186                 <b>Birth date</b>
    187               </th>
    188               <td>
    189                 {data.info.birthDate
    190                   ? data.info.birthDate.toLocaleDateString("en", {
    191                     year: "numeric",
    192                     month: "long",
    193                     day: "numeric",
    194                   })
    195                   : "—"}
    196               </td>
    197             </tr>
    198             <tr>
    199               <th>
    200                 <b>Nationality</b>
    201               </th>
    202               <td>{data.info.nationality ? data.info.nationality : "—"}</td>
    203             </tr>
    204             <tr>
    205               <th>
    206                 <b>Country</b>
    207               </th>
    208               <td>{data.info.country ? data.info.country : "—"}</td>
    209             </tr>
    210           </table>
    211           <div role="group">
    212             <a href={data.back!.href} role="button" class="secondary">Back</a>
    213             <a href={data.next!.href} role="button">Confirm</a>
    214           </div>
    215         </div>
    216       </article>
    217     );
    218   }
    219 
    220   return (
    221     <article>
    222       <header style="text-align: center;">
    223         <b>ID Document</b>
    224       </header>
    225       <form method="POST">
    226         <PhotoCaptureInput
    227           camera={data.side.startsWith("face") ? "user" : "environment"}
    228         >
    229           {LABELS[data.side]}
    230         </PhotoCaptureInput>
    231       </form>
    232     </article>
    233   );
    234 }