@opexa/portal-components 0.0.238 → 0.0.239

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.
@@ -1,6 +1,7 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
+ export type KnownCameraErrorName = 'CameraError' | 'PermissionDeniedError' | 'DeviceNotFoundError';
2
3
  export interface CameraError<T extends string = never> {
3
- name: 'CameraError' | 'PermissionDeniedError' | 'DeviceNotFoundError' | T;
4
+ name: KnownCameraErrorName | T;
4
5
  message: string;
5
6
  }
6
7
  export interface CameraData {
@@ -32,7 +33,7 @@ export interface UseCameraReturn<T extends string = never> {
32
33
  videoProps: ComponentPropsWithRef<'video'>;
33
34
  }
34
35
  export interface UseCameraOptions<T extends string = never> extends MediaStreamConstraints {
35
- transform?: (data: CameraData) => CameraData | Promise<CameraData>;
36
- validate?: (data: CameraData) => CameraError<T> | null | undefined | Promise<CameraError<T> | null | undefined>;
36
+ transform?(data: CameraData): CameraData | Promise<CameraData>;
37
+ validate?(data: CameraData): CameraError<T> | void | null | undefined | Promise<CameraError<T> | void | null | undefined>;
37
38
  }
38
39
  export declare function useCamera<T extends string = never>(options?: UseCameraOptions<T>): UseCameraReturn<T>;
@@ -1,4 +1,4 @@
1
- import { isBoolean, isFunction } from 'lodash-es';
1
+ import { isBoolean, isFunction, isObject } from 'lodash-es';
2
2
  import { useCallback, useMemo, useRef, useState, } from 'react';
3
3
  import invariant from 'tiny-invariant';
4
4
  export function useCamera(options = {}) {
@@ -8,6 +8,7 @@ export function useCamera(options = {}) {
8
8
  const [data, setData] = useState(null);
9
9
  const [error, setError] = useState(null);
10
10
  const [loading, setLoading] = useState(false);
11
+ const [mirror, setMirror] = useState(false);
11
12
  const open = useCallback(async () => {
12
13
  setData(null);
13
14
  setError(null);
@@ -40,6 +41,11 @@ export function useCamera(options = {}) {
40
41
  });
41
42
  node.srcObject = result;
42
43
  stream.current = result;
44
+ setMirror(!isObject(constraints.video)
45
+ ? true
46
+ : constraints.video.facingMode === 'environment'
47
+ ? false
48
+ : true);
43
49
  }
44
50
  catch (e) {
45
51
  if (e instanceof Error) {
@@ -114,6 +120,10 @@ export function useCamera(options = {}) {
114
120
  canvas.height = h * scaleFactor;
115
121
  context.imageSmoothingEnabled = true;
116
122
  context.imageSmoothingQuality = 'high';
123
+ if (mirror) {
124
+ context.translate(canvas.width, 0);
125
+ context.scale(-1, 1);
126
+ }
117
127
  context.drawImage(video, x, y, w, h, 0, 0, w * scaleFactor, h * scaleFactor);
118
128
  canvas.toBlob(async (blob) => {
119
129
  if (!blob) {
@@ -135,7 +145,7 @@ export function useCamera(options = {}) {
135
145
  else
136
146
  setData(isFunction(transform) ? await transform(data) : data);
137
147
  }, 'image/jpeg', 1);
138
- }, [transform, validate]);
148
+ }, [mirror, transform, validate]);
139
149
  const reset = useCallback(() => {
140
150
  setData(null);
141
151
  setError(null);
@@ -161,8 +171,11 @@ export function useCamera(options = {}) {
161
171
  position: 'relative',
162
172
  boxSizing: 'border-box',
163
173
  background: 'transparent',
174
+ objectFit: 'cover',
175
+ objectPosition: 'center',
176
+ transform: mirror ? 'scaleX(-1)' : 'none',
164
177
  },
165
- }), []);
178
+ }), [mirror]);
166
179
  return {
167
180
  snap,
168
181
  open,
@@ -22,9 +22,9 @@ export function AccountVerification(props) {
22
22
  const profileCompletion = profileCompletionQuery.data;
23
23
  const verificationQuery = useMemberVerificationQuery();
24
24
  const verification = verificationQuery.data;
25
- const isKycCompleted = verification &&
26
- verification.status !== 'UNVERIFIED' &&
27
- verification.status !== 'REJECTED';
25
+ const isKycCompleted = Boolean(verification?.status === 'VERIFIED') ||
26
+ Boolean(verification?.status === 'PENDING') ||
27
+ Boolean(verification?.status === 'APPROVED');
28
28
  const empty = verification == null;
29
29
  const pending = verification?.status === 'PENDING';
30
30
  const success = verification?.status === 'VERIFIED' || verification?.status === 'APPROVED';
@@ -1,6 +1,6 @@
1
1
  export declare const OnlineBankDepositContext: (props: {
2
2
  value: {
3
- view: "vca" | "form";
3
+ view: "form" | "vca";
4
4
  status: "waiting" | "processing" | "failed" | "verification-waiting" | "verification-processing" | "verification-failed" | "verification-success";
5
5
  verify: () => void;
6
6
  reset: () => void;
@@ -13,7 +13,7 @@ export declare const OnlineBankDepositContext: (props: {
13
13
  } & {
14
14
  children?: import("react").ReactNode | undefined;
15
15
  }) => React.ReactNode, useOnlineBankDepositContext: () => {
16
- view: "vca" | "form";
16
+ view: "form" | "vca";
17
17
  status: "waiting" | "processing" | "failed" | "verification-waiting" | "verification-processing" | "verification-failed" | "verification-success";
18
18
  verify: () => void;
19
19
  reset: () => void;
@@ -1,7 +1,7 @@
1
1
  import type { Deposit } from '../../../../types';
2
2
  export type UseOnlineBankDepositReturn = ReturnType<typeof useOnlineBankDeposit>;
3
3
  export declare function useOnlineBankDeposit(): {
4
- view: "vca" | "form";
4
+ view: "form" | "vca";
5
5
  status: "waiting" | "processing" | "failed" | "verification-waiting" | "verification-processing" | "verification-failed" | "verification-success";
6
6
  verify: () => void;
7
7
  reset: () => void;
@@ -1,6 +1,6 @@
1
1
  export declare const QRPHDepositContext: (props: {
2
2
  value: {
3
- view: "qrCode" | "form";
3
+ view: "form" | "qrCode";
4
4
  status: "waiting" | "processing" | "failed" | "verification-waiting" | "verification-processing" | "verification-failed" | "verification-success";
5
5
  verify: () => void;
6
6
  reset: () => void;
@@ -13,7 +13,7 @@ export declare const QRPHDepositContext: (props: {
13
13
  } & {
14
14
  children?: import("react").ReactNode | undefined;
15
15
  }) => React.ReactNode, useQRPHDepositContext: () => {
16
- view: "qrCode" | "form";
16
+ view: "form" | "qrCode";
17
17
  status: "waiting" | "processing" | "failed" | "verification-waiting" | "verification-processing" | "verification-failed" | "verification-success";
18
18
  verify: () => void;
19
19
  reset: () => void;
@@ -1,7 +1,7 @@
1
1
  import type { Deposit } from '../../../../types';
2
2
  export type UseQRPHDepositReturn = ReturnType<typeof useQRPHDeposit>;
3
3
  export declare function useQRPHDeposit(): {
4
- view: "qrCode" | "form";
4
+ view: "form" | "qrCode";
5
5
  status: "waiting" | "processing" | "failed" | "verification-waiting" | "verification-processing" | "verification-failed" | "verification-success";
6
6
  verify: () => void;
7
7
  reset: () => void;
@@ -126,7 +126,6 @@ export function InstapayWithdrawal() {
126
126
  itemToValue: (item) => item.code,
127
127
  itemToString: (item) => item.name,
128
128
  }), [instapayBankList]);
129
- console.log(bankCollection.items);
130
129
  return (_jsxs("form", { onSubmit: handleSubmit, noValidate: true, autoComplete: "off", children: [_jsx(Controller, { control: form.control, name: "bankCode", render: (o) => (_jsxs(Field.Root, { invalid: !!form.formState.errors.bankCode, children: [_jsxs(Select.Root, { collection: bankCollection, positioning: {
131
130
  sameWidth: true,
132
131
  }, value: o.field.value ? [o.field.value] : [], onValueChange: (details) => {
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import Image from 'next/image';
3
+ import { useEffect } from 'react';
3
4
  import { useShallow } from 'zustand/shallow';
4
5
  import { useAccountQuery } from '../../client/hooks/useAccountQuery.js';
5
6
  import { useGlobalStore } from '../../client/hooks/useGlobalStore.js';
@@ -22,12 +23,18 @@ export function KYC(props) {
22
23
  const { data: account } = useAccountQuery();
23
24
  const { data: verification } = useMemberVerificationQuery();
24
25
  const isBasicInfoCompleted = account?.realName !== null && account?.birthDay !== null;
25
- const isUploadCompleted = verification &&
26
- verification.idFrontImage !== null &&
27
- verification.selfieImage !== null;
28
- const isRejected = Boolean(verification?.status === 'REJECTED');
29
- const hasMissingData = !isBasicInfoCompleted || !isUploadCompleted || isRejected;
26
+ const isUploadCompleted = Boolean(verification?.idFrontImage) && Boolean(verification?.selfieImage);
27
+ const isVerified = Boolean(verification?.status === 'VERIFIED') ||
28
+ Boolean(verification?.status === 'PENDING') ||
29
+ Boolean(verification?.status === 'APPROVED');
30
+ const hasMissingData = !isBasicInfoCompleted || !isUploadCompleted || !isVerified;
31
+ const steps = !isBasicInfoCompleted ? 1 : !isUploadCompleted ? 2 : 3;
32
+ useEffect(() => {
33
+ if (steps) {
34
+ kyc.setStep(steps);
35
+ }
36
+ }, [steps, kyc]);
30
37
  return (_jsxs(_Fragment, { children: [_jsx(KYCContext, { value: kyc, children: _jsx(Dialog.Root, { open: globalStore.kyc.open && hasMissingData, onOpenChange: (details) => {
31
38
  globalStore.kyc.setOpen(details.open);
32
- }, lazyMount: true, unmountOnExit: true, closeOnEscape: false, closeOnInteractOutside: false, onExitComplete: kyc.reset, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, {}), _jsx(Dialog.Positioner, { children: _jsxs(Dialog.Content, { className: "bg-bg-primary-alt lg:min-h-auto mx-auto min-h-full w-full lg:w-fit overflow-y-auto", children: [kyc.capturing === 'SELFIE' && _jsx(CaptureSelfie, {}), kyc.capturing === 'ID_DOCUMENT' && _jsx(CaptureIdDocument, {}), !kyc.capturing && (_jsxs("div", { className: "flex h-dvh w-full flex-col overflow-y-auto p-3xl sm:h-fit sm:overflow-auto lg:w-[400px]", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsx(Image, { src: props.logo, alt: "", width: 200, height: 100, className: "mx-auto mb-5 block h-7.5 w-auto", draggable: false }), _jsx(Indicator, {}), kyc.step === 1 && _jsx(BasicInformation, {}), kyc.step === 2 && _jsx(IdentityVerification, {}), kyc.step === 3 && _jsx(PersonalInformation, {})] }))] }) })] }) }) }), _jsx(KYCReminder, { ...props })] }));
39
+ }, lazyMount: true, unmountOnExit: true, closeOnEscape: false, closeOnInteractOutside: false, onExitComplete: kyc.reset, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, {}), _jsx(Dialog.Positioner, { children: _jsxs(Dialog.Content, { className: "bg-bg-primary-alt lg:min-h-auto mx-auto min-h-full w-full overflow-y-auto lg:w-fit", children: [kyc.capturing === 'SELFIE' && _jsx(CaptureSelfie, {}), kyc.capturing === 'ID_DOCUMENT' && _jsx(CaptureIdDocument, {}), !kyc.capturing && (_jsxs("div", { className: "flex h-dvh w-full flex-col overflow-y-auto p-3xl sm:h-fit sm:overflow-auto lg:w-[400px]", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsx(Image, { src: props.logo, alt: "", width: 200, height: 100, className: "mx-auto mb-5 block h-7.5 w-auto", draggable: false }), _jsx(Indicator, {}), kyc.step === 1 && _jsx(BasicInformation, {}), kyc.step === 2 && _jsx(IdentityVerification, {}), kyc.step === 3 && _jsx(PersonalInformation, {})] }))] }) })] }) }) }), _jsx(KYCReminder, { ...props })] }));
33
40
  }
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { GoogleAnalytics, GoogleTagManager } from '@next/third-parties/google';
3
3
  import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
4
4
  import { Suspense } from 'react';
5
+ import { INTERNALS__REPORT_WEB_VITALS } from '../../constants/index.js';
5
6
  import { prefetchAccountQuery } from '../../server/utils/prefetchAccountQuery.js';
6
7
  import { prefetchPlatformQuery } from '../../server/utils/prefetchPlatformQuery.js';
7
8
  import { prefetchSessionQuery } from '../../server/utils/prefetchSessionQuery.js';
@@ -20,5 +21,5 @@ export async function PortalProvider({ children, facebookPixelId, googleAnalytic
20
21
  prefetchAccountQuery(),
21
22
  prefetchWalletQuery(),
22
23
  ]);
23
- return (_jsxs(_Fragment, { children: [_jsx(WebVitals, {}), _jsx(Providers, { children: _jsxs(HydrationBoundary, { state: dehydrate(queryClient), children: [children, _jsx(Suspense, { fallback: null, children: _jsx(SetupDomain, {}) })] }) }), _jsx(Snackbar, {}), facebookPixelId && _jsx(FacebookPixel, { fbId: facebookPixelId }), googleAnalyticsId && _jsx(GoogleAnalytics, { gaId: googleAnalyticsId }), googleTagManagerId && _jsx(GoogleTagManager, { gtmId: googleTagManagerId }), googleRecaptchaSiteKey && (_jsx(GoogleRecaptcha, { siteKey: googleRecaptchaSiteKey }))] }));
24
+ return (_jsxs(_Fragment, { children: [INTERNALS__REPORT_WEB_VITALS && _jsx(WebVitals, {}), _jsx(Providers, { children: _jsxs(HydrationBoundary, { state: dehydrate(queryClient), children: [children, _jsx(Suspense, { fallback: null, children: _jsx(SetupDomain, {}) })] }) }), _jsx(Snackbar, {}), facebookPixelId && _jsx(FacebookPixel, { fbId: facebookPixelId }), googleAnalyticsId && _jsx(GoogleAnalytics, { gaId: googleAnalyticsId }), googleTagManagerId && _jsx(GoogleTagManager, { gtmId: googleTagManagerId }), googleRecaptchaSiteKey && (_jsx(GoogleRecaptcha, { siteKey: googleRecaptchaSiteKey }))] }));
24
25
  }
@@ -12,7 +12,7 @@ export interface SelfieFieldProps extends UseSelfieFieldProps {
12
12
  readOnly?: boolean;
13
13
  disabled?: boolean;
14
14
  required?: boolean;
15
- onError?: (error: ValidateFileError) => void;
15
+ onError?: (error: ValidateFileError<'FaceNotFoundError'>) => void;
16
16
  }
17
17
  export declare function SelfieField(props: SelfieFieldProps): import("react/jsx-runtime").JSX.Element;
18
18
  interface UseSelfieFieldProps {
@@ -29,22 +29,39 @@ import { Portal } from '../../ui/Portal/index.js';
29
29
  import { validateFile } from '../../utils/validateFile.js';
30
30
  export function SelfieField(props) {
31
31
  const context = useSelfieField(props);
32
- return (_jsxs(SelfieFieldProvider, { value: context, children: [_jsxs("div", { ref: props.ref, id: props.id, style: props.style, className: twMerge('group', 'border', 'border-border-primary', 'bg-bg-primary', 'relative', 'w-full', 'aspect-[352/180]', 'shrink-0', 'rounded-xl', 'p-xl', props.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: (e) => {
32
+ return (_jsxs(SelfieFieldProvider, { value: context, children: [_jsxs("div", { ref: props.ref, id: props.id, style: props.style, className: twMerge('group', 'border', 'border-border-primary', 'bg-bg-primary', 'relative', 'w-full', 'aspect-[352/180]', 'shrink-0', 'rounded-xl', 'p-xl', props.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: async (e) => {
33
33
  e.preventDefault();
34
34
  const file = e.dataTransfer.files?.[0];
35
35
  if (!file)
36
36
  return;
37
- const error = validateFile(file);
37
+ const face = await getFaceFromFile(file);
38
+ const error = validateFile(file, {
39
+ customValidator() {
40
+ if (!face) {
41
+ return {
42
+ name: 'FaceNotFoundError',
43
+ message: 'Please ensure your face is clearly visible in the photo.',
44
+ };
45
+ }
46
+ },
47
+ });
38
48
  if (error)
39
49
  return props.onError?.(error);
40
50
  context.mutation.mutate({ file });
41
- }, "aria-describedby": context.field?.ariaDescribedby, children: [_jsx("input", { id: context.field?.ids?.control, ref: context.inputRef, type: "file", accept: "image/png, image/jpeg", onChange: (e) => {
51
+ }, "aria-describedby": context.field?.ariaDescribedby, children: [_jsx("input", { id: context.field?.ids?.control, ref: context.inputRef, type: "file", accept: "image/png, image/jpeg", onChange: async (e) => {
42
52
  const file = e.target.files?.[0];
43
53
  if (!file)
44
54
  return;
45
55
  const error = validateFile(file);
46
56
  if (error)
47
57
  return props.onError?.(error);
58
+ const face = await getFaceFromFile(file);
59
+ if (!face) {
60
+ return props.onError?.({
61
+ name: 'FaceNotFoundError',
62
+ message: 'Please ensure your face is clearly visible in the photo.',
63
+ });
64
+ }
48
65
  context.mutation.mutate({ file });
49
66
  }, disabled: context.field?.disabled ||
50
67
  context.query.isLoading ||
@@ -202,7 +219,6 @@ function useSelfieField(props) {
202
219
  };
203
220
  }
204
221
  */
205
- return null;
206
222
  },
207
223
  });
208
224
  const disclosure = useDisclosure({
@@ -3,3 +3,4 @@ export declare const PLATFORM_ID: string;
3
3
  export declare const PLATFORM_CODE: string;
4
4
  export declare const ENVIRONMENT: "production" | "development";
5
5
  export declare const BYPASS_RECAPTCHA: boolean;
6
+ export declare const INTERNALS__REPORT_WEB_VITALS: boolean;
@@ -23,3 +23,8 @@ export const BYPASS_RECAPTCHA = z
23
23
  .transform(Boolean)
24
24
  .catch(false)
25
25
  .parse(process.env.NEXT_PUBLIC_BYPASS_RECAPTCHA);
26
+ export const INTERNALS__REPORT_WEB_VITALS = z
27
+ .any()
28
+ .transform(Boolean)
29
+ .catch(false)
30
+ .parse(process.env.NEXT_PUBLIC_REPORT_WEB_VITALS);
@@ -1,4 +1,4 @@
1
- export interface ValidateFileOptions {
1
+ export interface ValidateFileOptions<T extends string = never> {
2
2
  /**
3
3
  * @description Maximum file size in MB.
4
4
  * @default 10
@@ -19,10 +19,11 @@ export interface ValidateFileOptions {
19
19
  * ]
20
20
  */
21
21
  supportedFiles?: string[];
22
+ customValidator?(file: File): ValidateFileError<T> | null | undefined | void;
22
23
  }
23
- export type ValidateFileErrorName = 'FILE_NAME_TOO_LONG' | 'FILE_SIZE_TOO_LARGE' | 'FILE_TYPE_NOT_SUPPORTED';
24
- export interface ValidateFileError {
25
- name: ValidateFileErrorName;
24
+ export type KnownValidateFileErrorName = 'FileNameTooLongError' | 'FileSizeTooLargeError' | 'FileTypeNotSupportedError';
25
+ export interface ValidateFileError<T extends string = never> {
26
+ name: KnownValidateFileErrorName | T;
26
27
  message: string;
27
28
  }
28
- export declare function validateFile(file: File, options?: ValidateFileOptions): ValidateFileError | null;
29
+ export declare function validateFile<T extends string = never>(file: File, options?: ValidateFileOptions<T>): ValidateFileError<T> | null;
@@ -11,22 +11,27 @@ export function validateFile(file, options) {
11
11
  ];
12
12
  if (!supportedFiles.includes(file.type)) {
13
13
  return {
14
- name: 'FILE_TYPE_NOT_SUPPORTED',
14
+ name: 'FileTypeNotSupportedError',
15
15
  message: 'File type not supported.',
16
16
  };
17
17
  }
18
18
  if (mb(file.size) > maxFileSize) {
19
19
  return {
20
- name: 'FILE_SIZE_TOO_LARGE',
20
+ name: 'FileSizeTooLargeError',
21
21
  message: 'File size too large.',
22
22
  };
23
23
  }
24
24
  if (basename(file.name).length > maxFileName) {
25
25
  return {
26
- name: 'FILE_NAME_TOO_LONG',
26
+ name: 'FileNameTooLongError',
27
27
  message: 'File name is too long.',
28
28
  };
29
29
  }
30
+ if (options?.customValidator) {
31
+ const err = options.customValidator(file);
32
+ if (err)
33
+ return err;
34
+ }
30
35
  return null;
31
36
  }
32
37
  function mb(bytes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opexa/portal-components",
3
- "version": "0.0.238",
3
+ "version": "0.0.239",
4
4
  "exports": {
5
5
  "./ui/*": {
6
6
  "types": "./dist/ui/*/index.d.ts",