@opexa/portal-components 0.0.139 → 0.0.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ import type { CaptureSubject } from './useKYC';
2
+ interface FileUploadProps {
3
+ name: string;
4
+ value: string;
5
+ hasDescription?: boolean;
6
+ onChange?: (id: string) => void;
7
+ captureObject: CaptureSubject;
8
+ }
9
+ declare function FileUpload({ value, name, hasDescription, onChange, captureObject, }: FileUploadProps): import("react/jsx-runtime").JSX.Element;
10
+ export default FileUpload;
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import Image from 'next/image';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import { useFileQuery } from '../../client/hooks/useFileQuery.js';
6
+ import { useUploadImageFileMutation } from '../../client/hooks/useUploadImageFileMutation.js';
7
+ import { UploadCloud02Icon } from '../../icons/UploadCloud02Icon.js';
8
+ import { Button } from '../../ui/Button/index.js';
9
+ import { useKYCContext } from './KYCContext.js';
10
+ import { validateFile } from './utils.js';
11
+ function FileUpload({ value, name, hasDescription, onChange, captureObject, }) {
12
+ const kyc = useKYCContext();
13
+ const { mutate, isPending } = useUploadImageFileMutation();
14
+ const { data: uploadedImage, isSuccess: uploadedImageReady } = useFileQuery(value);
15
+ if (isPending) {
16
+ return (_jsx("div", { className: "border-border-primary bg-bg-primary flex min-h-[11.25rem] flex-col justify-center gap-lg rounded-xl border px-3xl py-xl", children: _jsx("div", { className: "flex items-center justify-center", children: _jsx("span", { className: "text-text-secondary-700", children: "Uploading..." }) }) }));
17
+ }
18
+ function handleFileChange(event) {
19
+ const file = event.target.files?.[0];
20
+ if (!file) {
21
+ console.error('No file selected');
22
+ return;
23
+ }
24
+ const error = validateFile(file);
25
+ if (error) {
26
+ console.error(error);
27
+ return;
28
+ }
29
+ mutate({ file }, {
30
+ onSuccess: (id) => {
31
+ onChange?.(id);
32
+ },
33
+ });
34
+ }
35
+ function handleUploadClick() {
36
+ const fileInput = document.getElementById(`file-upload-${name}`);
37
+ if (fileInput) {
38
+ fileInput.click();
39
+ }
40
+ }
41
+ return (_jsxs("div", { className: "border-border-primary bg-bg-primary flex min-h-[11.25rem] flex-col justify-center gap-lg rounded-xl border px-3xl py-xl", children: [uploadedImageReady ? (_jsxs("div", { className: "border-border-primary bg-bg-primary min-h-[9.25rem]", children: [_jsx(Image, { src: uploadedImage?.url ?? '', alt: "Uploaded", className: "w-full object-contain", width: 350, height: 350, draggable: false }), _jsx(Button, { size: "xs", variant: "outline", className: twMerge('mx-auto mt-4 w-fit', value && 'hidden'), type: "button", onClick: handleUploadClick, children: "Reupload" })] })) : hasDescription ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-border-primary shadow-xs mx-auto flex size-8 items-center justify-center rounded-sm border", children: _jsx(UploadCloud02Icon, { className: "text-text-quinary size-4" }) }), _jsxs("p", { className: "text-text-tertiary-600 text-center text-sm", children: [_jsx("span", { className: "text-button-secondary-fg cursor-pointer font-semibold", onClick: handleUploadClick, children: "Click to upload" }), ' ', "or drag and drop", _jsx("br", {}), "PNG, JPG or JPEG (max. 10mb)", _jsx("br", {}), "or"] })] })) : null, _jsx("input", { type: "file", id: `file-upload-${name}`, name: name, onChange: handleFileChange, accept: "image/png, image/jpg, image/jpeg", className: "hidden" }), _jsx(Button, { size: "xs", variant: "outline", className: "mx-auto w-fit", onClick: () => kyc.setCapturing(captureObject), disabled: isPending, type: "button", children: value ? 'Retake Photo' : 'Take a Photo' })] }));
42
+ }
43
+ export default FileUpload;
@@ -1,37 +1,107 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import { useEffect } from 'react';
5
+ import { useForm } from 'react-hook-form';
6
+ import { z } from 'zod';
2
7
  import { useShallow } from 'zustand/shallow';
8
+ import { useCreateMemberVerificationMutation } from '../../client/hooks/useCreateMemberVerificationMutation.js';
3
9
  import { useGlobalStore } from '../../client/hooks/useGlobalStore.js';
10
+ import { useMemberVerificationQuery } from '../../client/hooks/useMemberVerificationQuery.js';
11
+ import { useUpdateMemberVerificationMutation } from '../../client/hooks/useUpdateMemberVerificationMutation.js';
12
+ import { toaster } from '../../client/utils/toaster.js';
4
13
  import { HelpCircleIcon } from '../../icons/HelpCircleIcon.js';
5
- import { UploadCloud02Icon } from '../../icons/UploadCloud02Icon.js';
6
14
  import { Button } from '../../ui/Button/index.js';
7
15
  import { Dialog } from '../../ui/Dialog/index.js';
8
16
  import { Field } from '../../ui/Field/index.js';
9
17
  import { Portal } from '../../ui/Portal/index.js';
10
- import DisplayImage from './DisplayImage.js';
18
+ import FileUpload from './FileUpload.js';
11
19
  import { useKYCContext } from './KYCContext.js';
20
+ const formSchema = z.object({
21
+ selfieImage: z.string().min(1, 'Selfie image is required.'),
22
+ idFrontImage: z.string().min(1, 'Front image of ID is required.'),
23
+ });
12
24
  export function IdentityVerification() {
13
25
  const kyc = useKYCContext();
26
+ const { mutate: createMemberVerification, isPending: createPending } = useCreateMemberVerificationMutation({
27
+ onSuccess: () => {
28
+ toaster.success({
29
+ title: 'ID Front Image & Selfie Image uploaded successfully',
30
+ });
31
+ kyc.setStep(3);
32
+ },
33
+ onError: (error) => {
34
+ toaster.error({
35
+ title: 'Failed to upload ID Front Image & Selfie Image',
36
+ description: error.message,
37
+ });
38
+ },
39
+ });
40
+ const { mutate: updateMemberVerification, isPending: updatePending } = useUpdateMemberVerificationMutation({
41
+ onSuccess: () => {
42
+ toaster.success({
43
+ title: 'ID Front Image & Selfie Image updated successfully',
44
+ });
45
+ kyc.setStep(3);
46
+ },
47
+ onError: (error) => {
48
+ toaster.error({
49
+ title: 'Failed to upload ID Front Image & Selfie Image',
50
+ description: error.message,
51
+ });
52
+ },
53
+ });
54
+ const memberVerification = useMemberVerificationQuery();
55
+ const memberId = memberVerification.data?.id;
14
56
  const globalStore = useGlobalStore(useShallow((ctx) => ({
15
57
  kyc: ctx.kyc,
16
58
  })));
17
- return (_jsxs("div", { children: [_jsx(Dialog.Title, { className: "text-center text-lg font-semibold", children: "Verify your identity" }), _jsx(Dialog.Description, { className: "text-text-secondary-700 mt-xs text-center text-sm", children: "Verify your details to confirm your identity and secure your access." }), _jsxs("div", { className: "border-border-primary bg-bg-primary text-text-placeholder mt-7 rounded-xl border p-lg text-xs leading-tight", children: [_jsx("h3", { className: "font-semibold", children: "Instructions" }), _jsxs("ol", { className: "mt-2 list-inside list-decimal", children: [_jsx("li", { children: "Upload a full photo of your ID." }), _jsx("li", { children: "Please ensure that all details in the uploaded image are legible." }), _jsx("li", { children: "Please ensure that the ID uploaded is within the validity period." })] })] }), _jsxs("form", { className: "mt-xl", onSubmit: (e) => {
18
- e.preventDefault();
19
- kyc.setStep(3);
20
- }, children: [_jsxs(Field.Root, { children: [_jsxs(Field.Label, { className: "flex items-center gap-1.5", children: [_jsx("span", { children: "Front of your ID" }), _jsx(HelpCircleIcon, { className: "text-text-quinary size-4" })] }), _jsx(UploadIdDocument, {})] }), _jsxs(Field.Root, { className: "mt-3", children: [_jsxs(Field.Label, { className: "flex items-center gap-1.5", children: [_jsx("span", { children: "Selfie holding your ID" }), _jsx(HelpCircleIcon, { className: "text-text-quinary size-4" })] }), _jsx(UploadSelfie, {})] }), _jsx(Button, { type: "submit", className: "mt-6", children: "Continue" }), _jsx(Button, { variant: "outline", colorScheme: "gray", className: "mt-lg", onClick: () => {
59
+ const form = useForm({
60
+ resolver: zodResolver(formSchema),
61
+ defaultValues: {
62
+ idFrontImage: '',
63
+ selfieImage: '',
64
+ },
65
+ });
66
+ function onSubmit(values) {
67
+ if (!memberId) {
68
+ createMemberVerification({
69
+ selfieImage: values.selfieImage,
70
+ idFrontImage: values.idFrontImage,
71
+ address: '',
72
+ nationality: '',
73
+ natureOfWork: '',
74
+ permanentAddress: '',
75
+ placeOfBirth: '',
76
+ sourceOfIncome: '',
77
+ });
78
+ }
79
+ else {
80
+ updateMemberVerification({
81
+ id: memberId,
82
+ data: {
83
+ selfieImage: values.selfieImage,
84
+ idFrontImage: values.idFrontImage,
85
+ },
86
+ });
87
+ }
88
+ }
89
+ useEffect(() => {
90
+ if (kyc.idFrontImageId) {
91
+ form.setValue('idFrontImage', kyc.idFrontImageId);
92
+ }
93
+ if (kyc.selfieImageId) {
94
+ form.setValue('selfieImage', kyc.selfieImageId);
95
+ }
96
+ }, [form, kyc]);
97
+ return (_jsxs("div", { children: [_jsx(Dialog.Title, { className: "text-center text-lg font-semibold", children: "Verify your identity" }), _jsx(Dialog.Description, { className: "text-text-secondary-700 mt-xs text-center text-sm", children: "Verify your details to confirm your identity and secure your access." }), _jsxs("div", { className: "border-border-primary bg-bg-primary text-text-placeholder mt-7 rounded-xl border p-lg text-xs leading-tight", children: [_jsx("h3", { className: "font-semibold", children: "Instructions" }), _jsxs("ol", { className: "mt-2 list-inside list-decimal", children: [_jsx("li", { children: "Upload a full photo of your ID." }), _jsx("li", { children: "Please ensure that all details in the uploaded image are legible." }), _jsx("li", { children: "Please ensure that the ID uploaded is within the validity period." })] })] }), _jsxs("form", { className: "mt-xl", onSubmit: form.handleSubmit(onSubmit), children: [_jsxs(Field.Root, { invalid: !!form.formState.errors.idFrontImage, children: [_jsxs(Field.Label, { className: "flex items-center gap-1.5", children: [_jsx("span", { children: "Front of your ID" }), _jsx(HelpCircleIcon, { className: "text-text-quinary size-4" })] }), _jsx(FileUpload, { value: kyc.idFrontImageId || '', hasDescription: true, name: "idFrontImage", captureObject: "ID_DOCUMENT", onChange: (file) => {
98
+ form.setValue('idFrontImage', file);
99
+ kyc.setFrontImageId(file);
100
+ } }, "id-front-image-upload"), _jsx(Field.ErrorText, { children: form.formState.errors.idFrontImage?.message })] }), _jsxs(Field.Root, { className: "mt-3", invalid: !!form.formState.errors.selfieImage, children: [_jsxs(Field.Label, { className: "flex items-center gap-1.5", children: [_jsx("span", { children: "Selfie holding your ID" }), _jsx(HelpCircleIcon, { className: "text-text-quinary size-4" })] }), _jsx(FileUpload, { value: kyc.selfieImageId || '', name: "selfieImage", captureObject: "SELFIE", onChange: (file) => {
101
+ form.setValue('selfieImage', file);
102
+ } }, "selfie-image-upload"), _jsx(Field.ErrorText, { children: form.formState.errors.selfieImage?.message })] }), _jsx(Button, { type: "submit", className: "mt-6", disabled: createPending || updatePending, children: "Continue" }), _jsx(Button, { variant: "outline", colorScheme: "gray", className: "mt-lg", type: "button", onClick: () => {
21
103
  kyc.setStep(3);
22
104
  kyc.setDone(true);
23
105
  globalStore.kyc.setOpen(false);
24
106
  }, children: "Skip for now" })] }), _jsx(Dialog.Root, { lazyMount: true, unmountOnExit: true, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, {}), _jsx(Dialog.Positioner, { children: _jsx(Dialog.Content, { className: "bg-bg-primary-alt mx-auto min-h-full w-full p-3xl lg:h-auto lg:w-[400px]" }) })] }) })] }));
25
107
  }
26
- function UploadIdDocument() {
27
- const kyc = useKYCContext();
28
- return (_jsxs("div", { className: "border-border-primary bg-bg-primary flex min-h-[11.25rem] flex-col justify-center gap-lg rounded-xl border px-3xl py-xl", children: [kyc.idFrontImageId ? (_jsx(DisplayImage, { id: kyc.idFrontImageId })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "border-border-primary shadow-xs mx-auto flex size-8 items-center justify-center rounded-sm border", children: _jsx(UploadCloud02Icon, { className: "text-text-quinary size-4" }) }), _jsxs("p", { className: "text-text-tertiary-600 text-center text-sm", children: [_jsx("span", { className: "text-button-secondary-fg font-semibold", children: "Click to upload" }), ' ', "or drag and drop", _jsx("br", {}), "PNG, JPG or JPEG (max. 10mb)", _jsx("br", {}), "or"] })] })), _jsx(Button, { size: "xs", variant: "outline", className: "mx-auto w-fit", onClick: () => {
29
- kyc.setCapturing('ID_DOCUMENT');
30
- }, children: kyc.idFrontImageId ? 'Rekate Photo' : 'Take a Photo' })] }));
31
- }
32
- function UploadSelfie() {
33
- const kyc = useKYCContext();
34
- return (_jsxs("div", { className: "border-border-primary bg-bg-primary flex min-h-[11.25rem] flex-col justify-center gap-lg rounded-xl border px-3xl py-xl", children: [kyc.selfieImageId ? (_jsx(DisplayImage, { id: kyc.selfieImageId })) : (_jsx("div", { className: "border-border-primary shadow-xs mx-auto flex size-8 items-center justify-center rounded-sm border", children: _jsx(UploadCloud02Icon, { className: "text-text-quinary size-4" }) })), _jsx(Button, { size: "xs", variant: "outline", className: "mx-auto w-fit", onClick: () => {
35
- kyc.setCapturing('SELFIE');
36
- }, children: "Take a photo" })] }));
37
- }
@@ -1,7 +1,11 @@
1
+ 'use client';
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import Image from 'next/image';
4
+ import { useEffect } from 'react';
3
5
  import { useShallow } from 'zustand/shallow';
6
+ import { useAccountQuery } from '../../client/hooks/useAccountQuery.js';
4
7
  import { useGlobalStore } from '../../client/hooks/useGlobalStore.js';
8
+ import { useMemberVerificationQuery } from '../../client/hooks/useMemberVerificationQuery.js';
5
9
  import { XIcon } from '../../icons/XIcon.js';
6
10
  import { Dialog } from '../../ui/Dialog/index.js';
7
11
  import { Portal } from '../../ui/Portal/index.js';
@@ -15,6 +19,25 @@ import { PersonalInformation } from './PersonalInformation.js';
15
19
  import { useKYC } from './useKYC.js';
16
20
  export function KYC(props) {
17
21
  const kyc = useKYC();
22
+ const accountQuery = useAccountQuery();
23
+ const memberVerification = useMemberVerificationQuery();
24
+ const isBasicInfoVerified = Boolean(accountQuery.data?.realName) &&
25
+ Boolean(accountQuery.data?.birthDay);
26
+ const isMemberUploaded = Boolean(memberVerification.data?.idFrontImage) &&
27
+ Boolean(memberVerification.data?.selfieImage);
28
+ const memberStatus = memberVerification.data?.status;
29
+ //check steps
30
+ useEffect(() => {
31
+ if (!isBasicInfoVerified) {
32
+ kyc.setStep(1);
33
+ }
34
+ else if (memberStatus === 'REJECTED') {
35
+ kyc.setStep(2);
36
+ }
37
+ else if (isBasicInfoVerified) {
38
+ kyc.setStep(isMemberUploaded ? 3 : 2);
39
+ }
40
+ }, [isBasicInfoVerified, isMemberUploaded, memberStatus, kyc]);
18
41
  const globalStore = useGlobalStore(useShallow((ctx) => ({
19
42
  kyc: ctx.kyc,
20
43
  })));
@@ -7,7 +7,7 @@ type EventDetail = {
7
7
  };
8
8
  type UnsubscribeFn = () => void;
9
9
  type Subscriber = (detail: EventDetail) => UnsubscribeFn;
10
- type CaptureSubject = 'SELFIE' | 'ID_DOCUMENT';
10
+ export type CaptureSubject = 'SELFIE' | 'ID_DOCUMENT';
11
11
  export interface UseKYCReturn {
12
12
  step: number;
13
13
  setStep: (step: number) => void;
@@ -1,2 +1,6 @@
1
1
  export declare function dataURLtoBlob(dataURL: string): Blob;
2
2
  export declare const resizeFile: (file: File) => Promise<unknown>;
3
+ export declare const MAX_FILE_SIZE = 10;
4
+ export declare const MAX_NAME_LENGTH = 52;
5
+ export declare const SUPPORTED_FILES: string[];
6
+ export declare function validateFile(file: File): "File too large" | "File name too long" | "Unsupported file" | undefined;
@@ -15,3 +15,27 @@ export const resizeFile = (file) => new Promise((resolve) => {
15
15
  resolve(uri);
16
16
  }, 'file');
17
17
  });
18
+ export const MAX_FILE_SIZE = 10; /* in MB */
19
+ export const MAX_NAME_LENGTH = 52;
20
+ export const SUPPORTED_FILES = [
21
+ /* add more supported ext */
22
+ 'image/png',
23
+ 'image/jpg',
24
+ 'image/jpeg',
25
+ ];
26
+ function removeFilenameExt(subject) {
27
+ const l = subject.split('.');
28
+ l.pop();
29
+ return l.join('.');
30
+ }
31
+ export function validateFile(file) {
32
+ const fileType = file.type;
33
+ const fileSize = file.size / (1024 * 1024);
34
+ const nameLength = removeFilenameExt(file.name).length;
35
+ if (fileSize > MAX_FILE_SIZE)
36
+ return 'File too large';
37
+ if (nameLength > MAX_NAME_LENGTH)
38
+ return 'File name too long';
39
+ if (!SUPPORTED_FILES.includes(fileType))
40
+ return 'Unsupported file';
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opexa/portal-components",
3
- "version": "0.0.139",
3
+ "version": "0.0.140",
4
4
  "exports": {
5
5
  "./ui/*": {
6
6
  "types": "./dist/ui/*/index.d.ts",