@opexa/portal-components 0.0.242 → 0.0.243

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.
Files changed (32) hide show
  1. package/dist/client/hooks/useCamera.d.ts +6 -1
  2. package/dist/client/hooks/useCamera.js +57 -12
  3. package/dist/client/hooks/useFeatureFlag.d.ts +21 -5
  4. package/dist/client/hooks/useFeatureFlag.js +49 -9
  5. package/dist/components/PortalProvider/FeatureFlag.d.ts +1 -0
  6. package/dist/components/PortalProvider/FeatureFlag.js +26 -0
  7. package/dist/components/PortalProvider/PortalProvider.js +2 -1
  8. package/dist/components/SignIn/MobileNumberSignIn.js +4 -2
  9. package/dist/components/SignIn/NameAndPasswordSignIn.js +3 -2
  10. package/dist/components/shared/IdDocumentField.js +18 -27
  11. package/dist/components/shared/SelfieField.d.ts +1 -3
  12. package/dist/components/shared/SelfieField.js +120 -123
  13. package/dist/constants/StorageKey.d.ts +1 -0
  14. package/dist/constants/StorageKey.js +1 -0
  15. package/dist/services/httpRequest.js +25 -4
  16. package/dist/ui/AlertDialog/AlertDialog.d.ts +88 -88
  17. package/dist/ui/AlertDialog/alertDialog.recipe.d.ts +8 -8
  18. package/dist/ui/Combobox/Combobox.d.ts +42 -42
  19. package/dist/ui/Combobox/combobox.recipe.d.ts +3 -3
  20. package/dist/ui/DatePicker/DatePicker.d.ts +72 -72
  21. package/dist/ui/DatePicker/datePicker.recipe.d.ts +3 -3
  22. package/dist/ui/Dialog/Dialog.d.ts +88 -88
  23. package/dist/ui/Dialog/dialog.recipe.d.ts +8 -8
  24. package/dist/ui/Drawer/Drawer.d.ts +33 -33
  25. package/dist/ui/Drawer/drawer.recipe.d.ts +3 -3
  26. package/dist/ui/Field/Field.d.ts +21 -21
  27. package/dist/ui/Field/field.recipe.d.ts +3 -3
  28. package/dist/ui/Popover/Popover.d.ts +55 -55
  29. package/dist/ui/Popover/popover.recipe.d.ts +5 -5
  30. package/dist/ui/Select/Select.d.ts +45 -45
  31. package/dist/ui/Select/select.recipe.d.ts +3 -3
  32. package/package.json +2 -6
@@ -1,12 +1,9 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import '@mediapipe/face_detection';
4
- import * as faceDetection from '@tensorflow-models/face-detection';
5
- import '@tensorflow/tfjs-backend-webgl';
6
- import '@tensorflow/tfjs-core';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
3
  import { useFieldContext } from '@ark-ui/react';
4
+ import * as faceapi from 'face-api.js';
8
5
  import Image from 'next/image';
9
- import { useId, useLayoutEffect, useRef, useState, } from 'react';
6
+ import { cache, useEffect, useId, useRef, useState, } from 'react';
10
7
  import { twMerge } from 'tailwind-merge';
11
8
  import { useInterval } from 'usehooks-ts';
12
9
  import { useCamera } from '../../client/hooks/useCamera.js';
@@ -34,17 +31,7 @@ export function SelfieField(props) {
34
31
  const file = e.dataTransfer.files?.[0];
35
32
  if (!file)
36
33
  return;
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
- });
34
+ const error = validateFile(file);
48
35
  if (error)
49
36
  return props.onError?.(error);
50
37
  context.mutation.mutate({ file });
@@ -55,13 +42,6 @@ export function SelfieField(props) {
55
42
  const error = validateFile(file);
56
43
  if (error)
57
44
  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
- }
65
45
  context.mutation.mutate({ file });
66
46
  }, disabled: context.field?.disabled ||
67
47
  context.query.isLoading ||
@@ -93,41 +73,31 @@ function Camera() {
93
73
  const context = useSelfieFieldContext();
94
74
  return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
95
75
  context.disclosure.setOpen(details.open);
96
- }, onExitComplete: context.camera.close, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+1)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+2)] flex items-center justify-center overflow-y-auto py-4", children: _jsxs(Dialog.Content, { className: "bg-bg-primary-alt mx-auto w-[calc(100dvw-1rem)] max-w-[calc(100dvw-1rem)] overflow-y-auto rounded-lg px-4 py-5 lg:w-[747px] lg:max-w-[747px] lg:px-3xl lg:py-4xl", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsx(Dialog.Title, { className: "text-center font-semibold lg:text-xl", children: "Take a Selfie Holding Your ID" }), _jsx(Dialog.Description, { className: "text-text-tertiary-600 mt-md text-center text-xs lg:text-base", children: "Make sure your face and ID are clearly visible, with no glare\u00A0or\u00A0blur." }), _jsxs("div", { className: "relative mt-5 lg:mt-10 lg:px-3xl", children: [_jsxs("div", { className: twMerge('border-border-disabled relative w-full overflow-hidden rounded-md border bg-black lg:rounded-lg', !context.camera.data &&
97
- !context.camera.error &&
98
- !context.camera.loading
99
- ? 'block'
100
- : 'hidden'), children: [_jsx("video", { ...context.camera.videoProps }), _jsx(Mask, {})] }), context.camera.error && (_jsxs("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: [_jsx(CameraOffIcon, { className: "text-text-placeholder-subtle size-10 text-center lg:size-12" }), _jsx("h2", { className: "mt-3 text-sm font-semibold lg:mt-4 lg:text-base", children: context.camera.error.name }), _jsx("p", { className: "text-text-tertiary-600 mt-0.5 text-center text-xs lg:mt-1 lg:text-sm", children: context.camera.error.message }), _jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", fullWidth: false, className: "mt-4 lg:mt-5", onClick: () => {
76
+ }, closeOnEscape: false, closeOnInteractOutside: false, onExitComplete: context.camera.close, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+1)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+2)] flex items-center justify-center overflow-y-auto py-4", children: _jsxs(Dialog.Content, { className: "bg-bg-primary-alt mx-auto w-[calc(100dvw-1rem)] max-w-[calc(100dvw-1rem)] overflow-y-auto rounded-lg px-4 py-5 lg:w-[747px] lg:max-w-[747px] lg:px-3xl lg:py-4xl", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsx(Dialog.Title, { className: "text-center font-semibold lg:text-xl", children: "Take a Selfie Holding Your ID" }), _jsx(Dialog.Description, { className: "text-text-tertiary-600 mt-md text-center text-xs lg:text-base", children: "Make sure your face and ID are clearly visible, with no glare\u00A0or\u00A0blur." }), _jsxs("div", { className: "relative mt-5 lg:mt-10 lg:px-3xl", children: [_jsx(Video, {}), context.camera.error && (_jsxs("div", { className: "border-border-disabled flex aspect-[4/3] flex-col items-center justify-center rounded-md border bg-black px-4 lg:aspect-video", children: [_jsx(CameraOffIcon, { className: "text-text-placeholder-subtle size-10 text-center lg:size-12" }), _jsx("h2", { className: "mt-3 text-sm font-semibold lg:mt-4 lg:text-base", children: context.camera.error.name }), _jsx("p", { className: "text-text-tertiary-600 mt-0.5 text-center text-xs lg:mt-1 lg:text-sm", children: context.camera.error.message }), _jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", fullWidth: false, className: "mt-4 lg:mt-5", onClick: () => {
101
77
  context.camera.reopen();
102
- }, children: "Try again" })] })), context.camera.loading && (_jsx("div", { className: "border-border-disabled flex aspect-[85.60/53.98] flex-col items-center justify-center rounded-md border bg-black px-4 lg:rounded-lg", children: _jsx(Spinner02Icon, { className: "text-text-placeholder size-8" }) })), context.camera.data && (_jsx("div", { className: "border-border-disabled h-auto w-full overflow-hidden rounded-md border bg-black lg:rounded-lg", children: _jsx(Image, { src: context.camera.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "block h-auto w-full" }) }))] }), !context.camera.data &&
103
- !context.camera.error &&
104
- !context.camera.loading && (_jsx("div", { className: "mt-5 lg:mt-10", children: _jsxs(Button, { className: "disabled:bg-bg-disabled disabled:text-text-disabled mx-auto w-full px-xl lg:w-auto", onClick: () => {
105
- const rect = context.guideRef.current?.getBoundingClientRect();
106
- const crop = !rect
107
- ? undefined
108
- : {
109
- y: rect.y,
110
- x: rect.x,
111
- width: rect.width,
112
- height: rect.height,
113
- };
114
- context.camera.snap({ crop });
115
- }, disabled: !context.faceFound, children: [_jsx(Camera01Icon, { className: "size-5" }), "Capture"] }) })), context.camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10", children: [_jsxs(Button, { onClick: () => {
78
+ }, children: "Try again" })] })), context.camera.loading && (_jsx("div", { className: "border-border-disabled flex aspect-[4/3] flex-col items-center justify-center rounded-md border bg-black px-4 lg:aspect-video", children: _jsx(Spinner02Icon, { className: "text-text-placeholder size-8" }) })), context.camera.data && (_jsx("div", { className: "border-border-disabled aspect-[4/3] w-full overflow-hidden rounded-md border bg-black lg:aspect-video", children: _jsx(Image, { src: context.camera.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "mx-auto mt-2 block h-[calc(100%-1rem)] w-auto" }) }))] }), !context.camera.data && (_jsx("div", { className: "mt-5 lg:mt-10", children: _jsxs(Button, { className: "disabled:bg-bg-disabled disabled:text-text-disabled mx-auto w-full px-xl py-lg lg:w-[10rem]", onClick: () => {
79
+ context.camera.snap({
80
+ crop: context.guideRef.current?.getBoundingClientRect(),
81
+ });
82
+ }, disabled: !context.faceFound ||
83
+ context.camera.loading ||
84
+ context.camera.snapping ||
85
+ !!context.camera.error, children: [context.camera.snapping ? (_jsx(Spinner02Icon, { className: "size-5" })) : (_jsx(Camera01Icon, { className: "size-5" })), "Capture"] }) })), context.camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10 lg:justify-center", children: [_jsxs(Button, { onClick: () => {
116
86
  const file = context.camera.data?.file;
117
87
  if (!file)
118
88
  return;
119
89
  context.mutation.mutate({ file });
120
90
  context.disclosure.setOpen(false);
121
- }, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
91
+ }, className: "w-full lg:w-[10rem]", children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
122
92
  context.camera.reset();
123
- }, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
93
+ }, className: "w-full lg:w-[10rem]", children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
124
94
  }
125
95
  /*
126
96
  * -------------------------------------
127
- * Mask
97
+ * Video
128
98
  * -------------------------------------
129
99
  */
130
- function Mask() {
100
+ function Video() {
131
101
  const id = useId();
132
102
  const context = useSelfieFieldContext();
133
103
  const [maskBox, setMaskBox] = useState({
@@ -136,7 +106,7 @@ function Mask() {
136
106
  width: 0,
137
107
  height: 0,
138
108
  });
139
- useLayoutEffect(() => {
109
+ useEffect(() => {
140
110
  const update = () => {
141
111
  const guide = context.guideRef.current;
142
112
  const mask = context.maskRef.current;
@@ -165,12 +135,43 @@ function Mask() {
165
135
  requestAnimationFrame(update);
166
136
  return () => observer.disconnect();
167
137
  }, [context.guideRef, context.maskRef]);
168
- return (_jsxs(_Fragment, { children: [_jsx("svg", { className: "pointer-events-none absolute h-full w-full", children: _jsx("defs", { children: _jsxs("mask", { id: id, maskUnits: "userSpaceOnUse", children: [_jsx("rect", { x: "0", y: "0", width: "100%", height: "100%", fill: "white" }), _jsx("rect", { x: maskBox.x, y: maskBox.y, width: maskBox.width, height: maskBox.height, fill: "black", rx: "8" /* change to 100% for circle */, ry: "8" /* change to 100% for circle */ })] }) }) }), _jsx("div", { ref: context.maskRef, className: "absolute inset-0 bg-black/60 backdrop-blur-sm", style: {
138
+ const [videoSize, setVideoSize] = useState({
139
+ width: 0,
140
+ height: 0,
141
+ });
142
+ useEffect(() => {
143
+ const video = context.camera.videoRef.current;
144
+ if (!video)
145
+ return;
146
+ const updateVideoWidth = () => {
147
+ const rect = video.getBoundingClientRect();
148
+ setVideoSize({
149
+ width: rect.width,
150
+ height: rect.height,
151
+ });
152
+ };
153
+ video.addEventListener('loadedmetadata', updateVideoWidth);
154
+ video.addEventListener('resize', updateVideoWidth);
155
+ return () => {
156
+ video.removeEventListener('loadedmetadata', updateVideoWidth);
157
+ video.removeEventListener('resize', updateVideoWidth);
158
+ };
159
+ }, [context.camera.videoRef]);
160
+ const guideWidth = videoSize.width <= 0 || videoSize.height <= 0
161
+ ? 0
162
+ : videoSize.width > videoSize.height
163
+ ? videoSize.height
164
+ : videoSize.width;
165
+ return (_jsxs("div", { className: twMerge('relative h-auto w-full overflow-hidden', !context.camera.data && !context.camera.error && !context.camera.loading
166
+ ? 'block'
167
+ : 'hidden'), children: [_jsx("video", { ...context.camera.videoProps }), _jsx("svg", { className: "pointer-events-none absolute -inset-4 size-[200%]", children: _jsx("defs", { children: _jsxs("mask", { id: id, maskUnits: "userSpaceOnUse", children: [_jsx("rect", { x: "0", y: "0", width: "200%", height: "200%", fill: "white" }), _jsx("rect", { x: maskBox.x, y: maskBox.y, width: maskBox.width, height: maskBox.height, fill: "black", rx: "8", ry: "8" })] }) }) }), _jsx("div", { ref: context.maskRef, className: "absolute -inset-4 size-[200%] bg-black/60 backdrop-blur-sm", style: {
169
168
  mask: `url(#${id})`,
170
169
  WebkitMask: `url(#${id})`,
171
170
  maskRepeat: 'no-repeat',
172
171
  WebkitMaskRepeat: 'no-repeat',
173
- } }), _jsx("div", { ref: context.guideRef, className: twMerge('border-utility-warning-700 absolute left-1/2 top-1/2 z-20 aspect-square w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-md border-2 border-dashed transition-all duration-200 lg:w-[24.688rem]', context.faceFound && 'border-utility-success-700 border-solid') })] }));
172
+ } }), _jsx("div", { ref: context.guideRef, className: twMerge('border-utility-warning-700 absolute left-1/2 top-1/2 z-20 aspect-square -translate-x-1/2 -translate-y-1/2 rounded-md border-2 border-dashed', context.faceFound && 'border-utility-success-700', guideWidth <= 0 ? 'hidden' : 'block'), style: {
173
+ width: `calc(${guideWidth}px - 2rem)`,
174
+ } })] }));
174
175
  }
175
176
  function useSelfieField(props) {
176
177
  const field = useFieldContext();
@@ -208,17 +209,18 @@ function useSelfieField(props) {
208
209
  const guideRef = useRef(null);
209
210
  const camera = useCamera({
210
211
  async validate(data) {
211
- const face = await getFaceFromFile(data.file);
212
- console.log(face);
213
212
  /*
214
- ⛏️ FIXME
215
- if (!face) {
216
- return {
217
- name: 'FaceNotFoundError',
218
- message: 'Please ensure your face is clearly visible in the photo.',
219
- };
220
- }
213
+ * 💁‍♂️
214
+ * Check if we still have the face.
215
+ * User could have moved away from the camera
221
216
  */
217
+ const ok = await validateFaceFromImage(data.image);
218
+ if (!ok) {
219
+ return {
220
+ name: 'FaceNotFoundError',
221
+ message: 'Please ensure your face is clearly visible in the photo.',
222
+ };
223
+ }
222
224
  },
223
225
  });
224
226
  const disclosure = useDisclosure({
@@ -230,32 +232,10 @@ function useSelfieField(props) {
230
232
  });
231
233
  const [faceFound, setFaceFound] = useState(false);
232
234
  useInterval(async () => {
233
- if (!camera.videoRef.current)
235
+ if (!camera.videoRef.current || !guideRef.current) {
234
236
  return setFaceFound(false);
235
- if (!guideRef.current)
236
- return setFaceFound(false);
237
- const guide = guideRef.current;
238
- const video = camera.videoRef.current;
239
- const guideRect = guide.getBoundingClientRect();
240
- const videoRect = video.getBoundingClientRect();
241
- const face = await getFace(video);
242
- if (!face)
243
- return setFaceFound(false);
244
- /* ⛏️ FIXME */
245
- const scaleX = videoRect.width / video.videoWidth;
246
- const scaleY = videoRect.height / video.videoHeight;
247
- const offsetX = videoRect.left;
248
- const offsetY = videoRect.top;
249
- const faceBoxX = offsetX + face.box.xMin * video.videoWidth * scaleX;
250
- const faceBoxY = offsetY + face.box.yMin * video.videoHeight * scaleY;
251
- const faceBoxWidth = (face.box.xMax - face.box.xMin) * video.videoWidth * scaleX;
252
- const faceBoxHeight = (face.box.yMax - face.box.yMin) * video.videoHeight * scaleY;
253
- const faceIsInsideGuide = faceBoxX >= guideRect.left &&
254
- faceBoxY >= guideRect.top &&
255
- faceBoxX + faceBoxWidth <= guideRect.right &&
256
- faceBoxY + faceBoxHeight <= guideRect.bottom;
257
- console.log({ faceIsInsideGuide });
258
- setFaceFound(true);
237
+ }
238
+ setFaceFound(await validateFace(camera.videoRef.current, guideRef.current));
259
239
  }, disclosure.open && !camera.data && !camera.error && !camera.loading
260
240
  ? 1000
261
241
  : null);
@@ -279,44 +259,61 @@ const [SelfieFieldProvider, useSelfieFieldContext] = createContext();
279
259
  * Utils
280
260
  * -------------------------------------
281
261
  */
282
- let __face_detector__;
283
- async function getFaceDetector() {
284
- if (__face_detector__)
285
- return __face_detector__;
286
- __face_detector__ = await faceDetection.createDetector(faceDetection.SupportedModels.MediaPipeFaceDetector, {
287
- runtime: 'tfjs',
288
- maxFaces: 1,
289
- });
290
- return __face_detector__;
291
- }
292
- async function getFace(video) {
293
- if (!video || video.readyState < 2)
294
- return null;
295
- try {
296
- const detector = await getFaceDetector();
297
- const faces = await detector.estimateFaces(video);
298
- return faces.at(0) ?? null;
299
- }
300
- catch {
301
- return null;
302
- }
262
+ let __faceapi_models_loaded__ = false;
263
+ async function loadFaceApiModels() {
264
+ if (__faceapi_models_loaded__)
265
+ return;
266
+ await Promise.all([
267
+ faceapi.nets.tinyFaceDetector.loadFromUri('/models'),
268
+ faceapi.nets.faceLandmark68Net.loadFromUri('/models'),
269
+ faceapi.nets.faceRecognitionNet.loadFromUri('/models'),
270
+ ]);
271
+ __faceapi_models_loaded__ = true;
303
272
  }
304
- function fileToImage(file) {
305
- return new Promise((resolve, reject) => {
306
- const img = new globalThis.Image();
307
- img.src = URL.createObjectURL(file);
308
- img.onload = () => resolve(img);
309
- img.onerror = () => reject('Failed to load image');
310
- });
311
- }
312
- async function getFaceFromFile(file) {
313
- try {
314
- const detector = await getFaceDetector();
315
- const image = await fileToImage(file);
316
- const faces = await detector.estimateFaces(image);
317
- return faces.at(0) ?? null;
318
- }
319
- catch {
320
- return null;
321
- }
273
+ const validateFace = cache(async (video, guide) => {
274
+ await loadFaceApiModels();
275
+ const videoRect = video.getBoundingClientRect();
276
+ const guideRect = guide.getBoundingClientRect();
277
+ const canvas = document.createElement('canvas');
278
+ canvas.width = videoRect.width;
279
+ canvas.height = videoRect.height;
280
+ const ctx = canvas.getContext('2d');
281
+ if (!ctx)
282
+ return false;
283
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
284
+ const detection = await faceapi.detectSingleFace(canvas, new faceapi.TinyFaceDetectorOptions({
285
+ inputSize: 224,
286
+ scoreThreshold: 0.75,
287
+ }));
288
+ if (!detection)
289
+ return false;
290
+ const { x, y, width, height } = detection.box;
291
+ const faceBox = {
292
+ left: x,
293
+ right: x + width,
294
+ top: y,
295
+ bottom: y + height,
296
+ };
297
+ const guideBox = {
298
+ left: guideRect.left - videoRect.left,
299
+ right: guideRect.right - videoRect.left,
300
+ top: guideRect.top - videoRect.top,
301
+ bottom: guideRect.bottom - videoRect.top,
302
+ };
303
+ const overlapWidth = Math.max(0, Math.min(faceBox.right, guideBox.right) -
304
+ Math.max(faceBox.left, guideBox.left));
305
+ const overlapHeight = Math.max(0, Math.min(faceBox.bottom, guideBox.bottom) -
306
+ Math.max(faceBox.top, guideBox.top));
307
+ const overlapArea = overlapWidth * overlapHeight;
308
+ const faceArea = width * height;
309
+ const overlapRatio = overlapArea / faceArea;
310
+ return overlapRatio >= 0.6;
311
+ });
312
+ export async function validateFaceFromImage(img) {
313
+ await loadFaceApiModels();
314
+ const detection = await faceapi.detectSingleFace(img, new faceapi.TinyFaceDetectorOptions({
315
+ inputSize: 224,
316
+ scoreThreshold: 0.5,
317
+ }));
318
+ return detection != null;
322
319
  }
@@ -1,3 +1,4 @@
1
1
  export declare const ACCESS_TOKEN_COOKIE_NAME: string;
2
2
  export declare const REFRESH_TOKEN_COOKIE_NAME: string;
3
3
  export declare const DOMAIN_COOKIE_NAME: string;
4
+ export declare const FEATURE_FLAG_LOCAL_STORAGE_KEY: string;
@@ -1,3 +1,4 @@
1
1
  export const ACCESS_TOKEN_COOKIE_NAME = 'WebPortalAuthAccessToken';
2
2
  export const REFRESH_TOKEN_COOKIE_NAME = 'WebPortalAuthRefreshToken';
3
3
  export const DOMAIN_COOKIE_NAME = 'WebPortalDomain';
4
+ export const FEATURE_FLAG_LOCAL_STORAGE_KEY = 'WebPortalFeatureFlag';
@@ -72,16 +72,35 @@ const KnownErrorSchema = z
72
72
  .object({
73
73
  code: z.string().optional(),
74
74
  message: z.string().optional(),
75
+ __error: z
76
+ .object({
77
+ name: z
78
+ .union([z.string(), z.object({ code: z.string().optional() })])
79
+ .optional(),
80
+ message: z.string().optional(),
81
+ })
82
+ .optional(),
75
83
  })
76
84
  .nullable()
77
85
  .catch(null)
78
- .transform((v) => v?.code ?? v?.message ?? null);
86
+ .transform((v) => {
87
+ if (v?.__error) {
88
+ return ((typeof v.__error.name === 'string'
89
+ ? v.__error.name
90
+ : v.__error.name?.code) ??
91
+ v.__error.message ??
92
+ v.code ??
93
+ v.message ??
94
+ null);
95
+ }
96
+ return v?.code ?? v?.message ?? null;
97
+ });
79
98
  function getErrorMessage(code) {
80
99
  switch (code) {
81
- case 'ACCOUNT_BLACKLISTED':
82
- return 'Account is blacklisted';
100
+ case 'MEMBER_ACCOUNT_BLACKLISTED':
101
+ return 'Your account has been blacklisted due to a serious violation of our policies. For more information or to appeal, please contac Chat Support team.';
83
102
  case 'MEMBER_ACCOUNT_SUSPENDED':
84
- return 'Account is suspended';
103
+ return 'Your account has been temporarily suspended. Please reach out to the Chat Support team to learn more and get help with resolving the issue.';
85
104
  case 'INVALID_RECAPTCHA_RESPONSE':
86
105
  return 'Invalid reCAPTCHA response';
87
106
  case 'MEMBER_NOT_FOUND':
@@ -90,6 +109,8 @@ function getErrorMessage(code) {
90
109
  return 'Rate limit exceeded';
91
110
  case 'NOT_READY_TO_SEND_VERIFICATION_ERROR':
92
111
  return 'Not ready to send verification code';
112
+ case 'Forbidden':
113
+ return 'Invalid username or password. Please try again.';
93
114
  default:
94
115
  return 'Something went wrong. Please try again later.';
95
116
  }