@opexa/portal-components 0.0.236 → 0.0.237

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 (72) hide show
  1. package/dist/client/hooks/useCamera.d.ts +8 -7
  2. package/dist/client/hooks/useCamera.js +6 -6
  3. package/dist/components/Bonuses/ClaimSpotBonus.lazy.js +1 -1
  4. package/dist/components/DepositWithdrawal/Deposit/OnlineBankDeposit/OnlineBankDepositContext.d.ts +2 -2
  5. package/dist/components/DepositWithdrawal/Deposit/OnlineBankDeposit/OnlineBankDepositDetails.js +1 -1
  6. package/dist/components/DepositWithdrawal/Deposit/OnlineBankDeposit/useOnlineBankDeposit.d.ts +1 -1
  7. package/dist/components/DepositWithdrawal/Deposit/QRPHDeposit/QRPHDepositContext.d.ts +2 -2
  8. package/dist/components/DepositWithdrawal/Deposit/QRPHDeposit/QRPHDepositDetails.js +1 -1
  9. package/dist/components/DepositWithdrawal/Deposit/QRPHDeposit/useQRPHDeposit.d.ts +1 -1
  10. package/dist/components/DepositWithdrawal/DepositWithdrawal.lazy.js +1 -1
  11. package/dist/components/Disclaimer/DisclaimerV1.js +1 -1
  12. package/dist/components/Disclaimer/DisclaimerV2.js +1 -1
  13. package/dist/components/Disclaimer/ResponsibleGaming.js +3 -3
  14. package/dist/components/Disclaimer/TermsOfUse.js +4 -4
  15. package/dist/components/GameLaunch/GameLaunch.lazy.js +1 -1
  16. package/dist/components/Jackpots/JackpotsList/JackpotsListItemRules.js +1 -1
  17. package/dist/components/KYC/KYC.lazy.js +1 -1
  18. package/dist/components/KYC/KYCReminder.lazy.js +1 -1
  19. package/dist/components/Messages/Messages.lazy.js +1 -1
  20. package/dist/components/Messages/MessagesPopup.js +1 -1
  21. package/dist/components/Quests/DailyCheckInQuest.js +1 -1
  22. package/dist/components/Quests/OnboardingQuest.js +1 -1
  23. package/dist/components/Quests/WageringQuest.js +1 -1
  24. package/dist/components/Search/Search.lazy.js +1 -1
  25. package/dist/components/SignIn/SignIn.lazy.js +1 -1
  26. package/dist/components/SignUp/SignUpDefault/SignUp.lazy.js +1 -1
  27. package/dist/components/SignUp/SignUpKYC/CaptureIdDocument.js +1 -1
  28. package/dist/components/SignUp/SignUpKYC/CaptureSelfie.js +1 -1
  29. package/dist/components/SignUp/SignUpKYC/SignUpKYC.lazy.js +1 -1
  30. package/dist/components/Tournaments/TournamentsList/TournamentsListItemRules.js +1 -1
  31. package/dist/components/shared/IdDocumentField.d.ts +14 -6
  32. package/dist/components/shared/IdDocumentField.js +69 -58
  33. package/dist/components/shared/SelfieField.d.ts +12 -6
  34. package/dist/components/shared/SelfieField.js +188 -72
  35. package/dist/next.config.js +8 -0
  36. package/dist/ui/AlertDialog/AlertDialog.d.ts +88 -88
  37. package/dist/ui/AlertDialog/alertDialog.recipe.d.ts +11 -11
  38. package/dist/ui/Avatar/avatar.recipe.d.ts +3 -3
  39. package/dist/ui/Badge/Badge.d.ts +12 -12
  40. package/dist/ui/Carousel/Carousel.d.ts +45 -45
  41. package/dist/ui/Carousel/carousel.recipe.d.ts +5 -5
  42. package/dist/ui/Checkbox/Checkbox.d.ts +23 -23
  43. package/dist/ui/Checkbox/checkbox.recipe.d.ts +3 -3
  44. package/dist/ui/Clipboard/Clipboard.d.ts +18 -18
  45. package/dist/ui/Collapsible/Collapsible.d.ts +32 -32
  46. package/dist/ui/Collapsible/collapsible.recipe.d.ts +5 -5
  47. package/dist/ui/Combobox/Combobox.d.ts +42 -42
  48. package/dist/ui/Combobox/combobox.recipe.d.ts +3 -3
  49. package/dist/ui/DatePicker/DatePicker.d.ts +72 -72
  50. package/dist/ui/DatePicker/datePicker.recipe.d.ts +3 -3
  51. package/dist/ui/Dialog/Dialog.d.ts +88 -88
  52. package/dist/ui/Dialog/dialog.recipe.d.ts +11 -11
  53. package/dist/ui/Drawer/Drawer.d.ts +33 -33
  54. package/dist/ui/Drawer/drawer.recipe.d.ts +3 -3
  55. package/dist/ui/Menu/Menu.d.ts +144 -144
  56. package/dist/ui/Menu/menu.recipe.d.ts +20 -20
  57. package/dist/ui/PasswordInput/PasswordInput.d.ts +18 -18
  58. package/dist/ui/Popover/Popover.d.ts +55 -55
  59. package/dist/ui/Popover/popover.recipe.d.ts +8 -8
  60. package/dist/ui/Progress/Progress.d.ts +27 -27
  61. package/dist/ui/Progress/progress.recipe.d.ts +3 -3
  62. package/dist/ui/QrCode/qrCode.recipe.d.ts +5 -5
  63. package/dist/ui/SegmentGroup/SegmentGroup.d.ts +18 -18
  64. package/dist/ui/SegmentGroup/segmentGroup.recipe.d.ts +3 -3
  65. package/dist/ui/Select/Select.d.ts +45 -45
  66. package/dist/ui/Select/select.recipe.d.ts +3 -3
  67. package/dist/ui/Table/Table.d.ts +21 -21
  68. package/dist/ui/Table/table.anatomy.d.ts +1 -1
  69. package/dist/ui/Table/table.recipe.d.ts +3 -3
  70. package/dist/ui/Tabs/Tabs.d.ts +15 -15
  71. package/dist/ui/Tabs/tabs.recipe.d.ts +3 -3
  72. package/package.json +5 -3
@@ -22,11 +22,9 @@ import { XIcon } from '../../icons/XIcon.js';
22
22
  import { Button } from '../../ui/Button/index.js';
23
23
  import { Dialog } from '../../ui/Dialog/index.js';
24
24
  import { Portal } from '../../ui/Portal/index.js';
25
- import { dataAttr } from '../../utils/dataAttr.js';
26
25
  import { validateFile } from '../../utils/validateFile.js';
27
26
  export function IdDocumentField(props) {
28
27
  const context = useIdDocumentField(props);
29
- const { field, setValue, query, mutation, inputRef, guideRef, camera, disclosure, } = context;
30
28
  return (_jsxs(IdDocumentFieldProvider, { value: context, children: [_jsxs("div", { ref: props.ref, id: props.id, style: props.style, className: twMerge('border-border-primary bg-bg-primary group relative aspect-[352/180] w-full shrink-0 rounded-xl border p-xl', props.className), onDragOver: (e) => e.preventDefault(), onDragEnter: (e) => e.preventDefault(), onDragLeave: (e) => e.preventDefault(), onDrop: (e) => {
31
29
  e.preventDefault();
32
30
  const file = e.dataTransfer.files?.[0];
@@ -35,60 +33,73 @@ export function IdDocumentField(props) {
35
33
  const error = validateFile(file);
36
34
  if (error)
37
35
  return props.onError?.(error);
38
- mutation.mutate({ file });
39
- }, "data-invalid": dataAttr(props.invalid ?? field?.invalid), "data-readonly": dataAttr(props.readOnly ?? field?.readOnly), "data-disabled": dataAttr(props.disabled ?? field?.disabled), "data-required": dataAttr(props.required ?? field?.required), "data-loading": dataAttr(query.isLoading || mutation.isPending), "aria-describedby": field?.ariaDescribedby, children: [_jsx("input", { id: field?.ids?.control, ref: inputRef, type: "file", accept: "image/png, image/jpeg", onChange: (e) => {
36
+ context.mutation.mutate({ file });
37
+ }, "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) => {
40
38
  const file = e.target.files?.[0];
41
39
  if (!file)
42
40
  return;
43
41
  const error = validateFile(file);
44
42
  if (error)
45
43
  return props.onError?.(error);
46
- mutation.mutate({ file });
47
- }, disabled: field?.disabled ||
48
- props.disabled ||
49
- query.isLoading ||
50
- mutation.isPending, readOnly: field?.readOnly || props.readOnly, required: field?.required || props.required, className: "sr-only" }), !query.data?.url && (_jsxs("div", { className: "flex size-full flex-col justify-center", 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-placeholder-subtle size-4" }) }), _jsxs("h2", { className: "text-text-tertiary-600 mt-lg", children: [_jsxs("span", { className: "block text-center text-sm", children: [_jsx("button", { type: "button", onClick: () => {
51
- inputRef.current?.click();
52
- }, disabled: field?.disabled ||
53
- field?.readOnly ||
44
+ context.mutation.mutate({ file });
45
+ }, disabled: context.field?.disabled ||
46
+ context.query.isLoading ||
47
+ context.mutation.isPending ||
48
+ props.disabled, readOnly: context.field?.readOnly || props.readOnly, required: context.field?.required || props.required, className: "sr-only" }), !context.query.data?.url && (_jsxs("div", { className: "flex size-full flex-col justify-center", 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-placeholder-subtle size-4" }) }), _jsxs("h2", { className: "text-text-tertiary-600 mt-lg", children: [_jsxs("span", { className: "block text-center text-sm", children: [_jsx("button", { type: "button", onClick: () => {
49
+ context.inputRef.current?.click();
50
+ }, disabled: context.field?.disabled ||
51
+ context.field?.readOnly ||
52
+ context.query.isLoading ||
53
+ context.mutation.isPending ||
54
54
  props.disabled ||
55
- props.readOnly ||
56
- query.isLoading ||
57
- mutation.isPending, "data-required": dataAttr(props.required ?? field?.required), "data-readonly": dataAttr(props.readOnly ?? field?.readOnly), "data-loading": dataAttr(query.isLoading || mutation.isPending), "data-invalid": dataAttr(props.invalid ?? field?.invalid), className: "text-button-secondary-fg font-semibold disabled:opacity-60", children: "Click to upload" }), ' ', "or drag and drop"] }), _jsx("span", { className: "mt-xs block text-center text-xs", children: "PNG, JPG or JPEG (max. 10mb)" }), _jsx("span", { className: "m-txs block text-center text-xs", children: "or" })] }), _jsx(Button, { size: "sm", variant: "outline", className: "mx-auto mt-md w-auto", onClick: () => {
58
- disclosure.setOpen(true);
59
- }, disabled: field?.disabled ||
60
- field?.readOnly ||
55
+ props.readOnly, className: "text-button-secondary-fg font-semibold disabled:opacity-60", children: "Click to upload" }), ' ', "or drag and drop"] }), _jsx("span", { className: "mt-xs block text-center text-xs", children: "PNG, JPG or JPEG (max. 10mb)" }), _jsx("span", { className: "m-txs block text-center text-xs", children: "or" })] }), _jsx(Button, { size: "sm", variant: "outline", className: "mx-auto mt-md w-auto", onClick: () => {
56
+ context.disclosure.setOpen(true);
57
+ }, disabled: context.field?.disabled ||
58
+ context.field?.readOnly ||
59
+ context.query.isLoading ||
60
+ context.mutation.isPending ||
61
61
  props.disabled ||
62
- props.readOnly ||
63
- query.isLoading ||
64
- mutation.isPending, "data-required": dataAttr(props.required ?? field?.required), "data-readonly": dataAttr(props.readOnly ?? field?.readOnly), "data-loading": dataAttr(query.isLoading || mutation.isPending), "data-invalid": dataAttr(props.invalid ?? field?.invalid), children: "Take a photo" })] })), query.data?.url && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: query.data.url, alt: "", width: 600, height: 378.08, unoptimized: true, className: "h-auto w-full rounded-md" }), _jsx("button", { type: "button", className: "backdrop-blur-xs absolute right-2 top-2 rounded-full bg-white/10 p-1", onClick: () => {
65
- setValue('');
66
- }, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Dialog.Root, { open: disclosure.open, onOpenChange: (details) => {
67
- disclosure.setOpen(details.open);
68
- }, onExitComplete: 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)] 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 Picture of Your Front ID" }), _jsxs(Dialog.Description, { className: "text-text-tertiary-600 mt-md text-center text-xs lg:text-base", children: ["Make sure your ID is clearly visible, well-lit, and not blurry.", ' ', _jsx("br", { className: "hidden lg:block" }), "Avoid glare or reflections, and ensure all corners are within\u00A0the\u00A0frame."] }), _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', !camera.data && !camera.error && !camera.loading
69
- ? 'block'
70
- : 'hidden'), children: [_jsx("video", { ...camera.videoProps }), _jsx(Mask, {})] }), 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: camera.error.name }), _jsx("p", { className: "text-text-tertiary-600 mt-0.5 text-center text-xs lg:mt-1 lg:text-sm", children: camera.error.message }), _jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", fullWidth: false, className: "mt-4 lg:mt-5", onClick: () => {
71
- camera.reopen();
72
- }, children: "Try again" })] })), 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" }) })), 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: camera.data.url, alt: "", width: 600, height: 378.08, unoptimized: true, className: "block h-auto w-full" }) }))] }), !camera.data && !camera.error && !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: () => {
73
- const rect = guideRef.current?.getBoundingClientRect();
74
- const crop = !rect
75
- ? undefined
76
- : {
77
- y: rect.y,
78
- x: rect.x,
79
- width: rect.width,
80
- height: rect.height,
81
- };
82
- camera.snap({ crop });
83
- }, children: [_jsx(Camera01Icon, { className: "size-5" }), "Capture"] }) })), camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10", children: [_jsxs(Button, { onClick: () => {
84
- const file = camera.data?.file;
85
- if (!file)
86
- return;
87
- mutation.mutate({ file });
88
- disclosure.setOpen(false);
89
- }, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
90
- camera.reset();
91
- }, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) })] }));
62
+ props.readOnly, children: "Take a photo" })] })), context.query.data?.url && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: context.query.data.url, alt: "", width: 600, height: 378.08, unoptimized: true, className: "h-auto w-full rounded-md" }), _jsx("button", { type: "button", className: "backdrop-blur-xs absolute right-2 top-2 rounded-full bg-white/10 p-1", onClick: () => {
63
+ context.setValue('');
64
+ }, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Camera, {})] }));
65
+ }
66
+ /**
67
+ * -------------------------------------
68
+ * Camera
69
+ * -------------------------------------
70
+ */
71
+ function Camera() {
72
+ const context = useIdDocumentFieldContext();
73
+ return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
74
+ context.disclosure.setOpen(details.open);
75
+ }, 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 Picture of Your Front ID" }), _jsxs(Dialog.Description, { className: "text-text-tertiary-600 mt-md text-center text-xs lg:text-base", children: ["Make sure your ID is clearly visible, well-lit, and not blurry.", ' ', _jsx("br", { className: "hidden lg:block" }), "Avoid glare or reflections, and ensure all corners are within\u00A0the\u00A0frame."] }), _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 &&
76
+ !context.camera.error &&
77
+ !context.camera.loading
78
+ ? 'block'
79
+ : '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: () => {
80
+ context.camera.reopen();
81
+ }, 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: 378.08, unoptimized: true, className: "block h-auto w-full" }) }))] }), !context.camera.data &&
82
+ !context.camera.error &&
83
+ !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: () => {
84
+ const rect = context.guideRef.current?.getBoundingClientRect();
85
+ const crop = !rect
86
+ ? undefined
87
+ : {
88
+ y: rect.y,
89
+ x: rect.x,
90
+ width: rect.width,
91
+ height: rect.height,
92
+ };
93
+ context.camera.snap({ crop });
94
+ }, 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: () => {
95
+ const file = context.camera.data?.file;
96
+ if (!file)
97
+ return;
98
+ context.mutation.mutate({ file });
99
+ context.disclosure.setOpen(false);
100
+ }, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
101
+ context.camera.reset();
102
+ }, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
92
103
  }
93
104
  /**
94
105
  * -------------------------------------
@@ -97,7 +108,7 @@ export function IdDocumentField(props) {
97
108
  */
98
109
  function Mask() {
99
110
  const id = useId();
100
- const { guideRef, maskRef } = useIdDocumentFieldContext();
111
+ const context = useIdDocumentFieldContext();
101
112
  const [maskBox, setMaskBox] = useState({
102
113
  x: 0,
103
114
  y: 0,
@@ -106,8 +117,8 @@ function Mask() {
106
117
  });
107
118
  useLayoutEffect(() => {
108
119
  const update = () => {
109
- const guide = guideRef.current;
110
- const mask = maskRef.current;
120
+ const guide = context.guideRef.current;
121
+ const mask = context.maskRef.current;
111
122
  if (!guide || !mask)
112
123
  return;
113
124
  const guideRect = guide.getBoundingClientRect();
@@ -126,19 +137,19 @@ function Mask() {
126
137
  }
127
138
  };
128
139
  const observer = new ResizeObserver(update);
129
- if (guideRef.current)
130
- observer.observe(guideRef.current);
131
- if (maskRef.current)
132
- observer.observe(maskRef.current);
140
+ if (context.guideRef.current)
141
+ observer.observe(context.guideRef.current);
142
+ if (context.maskRef.current)
143
+ observer.observe(context.maskRef.current);
133
144
  requestAnimationFrame(update);
134
145
  return () => observer.disconnect();
135
- }, [guideRef, maskRef]);
136
- 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", ry: "8" })] }) }) }), _jsx("div", { ref: maskRef, className: "absolute inset-0 bg-black/60 backdrop-blur-sm", style: {
146
+ }, [context.guideRef, context.maskRef]);
147
+ 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", ry: "8" })] }) }) }), _jsx("div", { ref: context.maskRef, className: "absolute inset-0 bg-black/60 backdrop-blur-sm", style: {
137
148
  mask: `url(#${id})`,
138
149
  WebkitMask: `url(#${id})`,
139
150
  maskRepeat: 'no-repeat',
140
151
  WebkitMaskRepeat: 'no-repeat',
141
- } }), _jsx("div", { ref: guideRef, className: "border-utility-warning-700 pointer-events-none absolute left-1/2 top-1/2 z-10 aspect-[85.6/53.98] w-[calc(100%-1rem)] max-w-[24.688rem] -translate-x-1/2 -translate-y-1/2 rounded-md border-2 border-dashed" })] }));
152
+ } }), _jsx("div", { ref: context.guideRef, className: "border-utility-warning-700 pointer-events-none absolute left-1/2 top-1/2 z-10 aspect-[85.6/53.98] w-[calc(100%-1rem)] max-w-[24.688rem] -translate-x-1/2 -translate-y-1/2 rounded-md border-2 border-dashed transition-all duration-200" })] }));
142
153
  }
143
154
  function useIdDocumentField(props) {
144
155
  const field = useFieldContext();
@@ -1,13 +1,13 @@
1
- import { type CSSProperties, type Ref } from 'react';
1
+ import '@mediapipe/face_detection';
2
+ import '@tensorflow/tfjs-backend-webgl';
3
+ import '@tensorflow/tfjs-core';
4
+ import { type CSSProperties, type RefObject } from 'react';
2
5
  import { type ValidateFileError } from '../../utils/validateFile';
3
- export interface SelfieFieldProps {
4
- ref?: Ref<HTMLDivElement>;
6
+ export interface SelfieFieldProps extends UseSelfieFieldProps {
7
+ ref?: RefObject<HTMLDivElement | null>;
5
8
  id?: string;
6
9
  style?: CSSProperties;
7
10
  className?: string;
8
- value?: string;
9
- onChange?: (value: string) => void;
10
- defaultValue?: string;
11
11
  invalid?: boolean;
12
12
  readOnly?: boolean;
13
13
  disabled?: boolean;
@@ -15,3 +15,9 @@ export interface SelfieFieldProps {
15
15
  onError?: (error: ValidateFileError) => void;
16
16
  }
17
17
  export declare function SelfieField(props: SelfieFieldProps): import("react/jsx-runtime").JSX.Element;
18
+ interface UseSelfieFieldProps {
19
+ value?: string;
20
+ onChange?: (value: string) => void;
21
+ defaultValue?: string;
22
+ }
23
+ export {};
@@ -1,9 +1,14 @@
1
1
  'use client';
2
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';
3
7
  import { useFieldContext } from '@ark-ui/react';
4
8
  import Image from 'next/image';
5
9
  import { useId, useLayoutEffect, useRef, useState, } from 'react';
6
10
  import { twMerge } from 'tailwind-merge';
11
+ import { useInterval } from 'usehooks-ts';
7
12
  import { useCamera } from '../../client/hooks/useCamera.js';
8
13
  import { useControllableState } from '../../client/hooks/useControllableState.js';
9
14
  import { useDisclosure } from '../../client/hooks/useDisclosure.js';
@@ -21,12 +26,10 @@ import { XIcon } from '../../icons/XIcon.js';
21
26
  import { Button } from '../../ui/Button/index.js';
22
27
  import { Dialog } from '../../ui/Dialog/index.js';
23
28
  import { Portal } from '../../ui/Portal/index.js';
24
- import { dataAttr } from '../../utils/dataAttr.js';
25
29
  import { validateFile } from '../../utils/validateFile.js';
26
30
  export function SelfieField(props) {
27
31
  const context = useSelfieField(props);
28
- const { field, setValue, query, mutation, camera, disclosure, inputRef, guideRef, } = context;
29
- return (_jsxs(SelfieFieldProvider, { value: context, children: [_jsxs("div", { ref: props.ref, id: props.id, style: props.style, className: twMerge('border-border-primary bg-bg-primary group relative aspect-[352/180] w-full shrink-0 rounded-xl border 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: (e) => {
30
33
  e.preventDefault();
31
34
  const file = e.dataTransfer.files?.[0];
32
35
  if (!file)
@@ -34,103 +37,123 @@ export function SelfieField(props) {
34
37
  const error = validateFile(file);
35
38
  if (error)
36
39
  return props.onError?.(error);
37
- mutation.mutate({ file });
38
- }, "data-invalid": dataAttr(props.invalid ?? field?.invalid), "data-readonly": dataAttr(props.readOnly ?? field?.readOnly), "data-disabled": dataAttr(props.disabled ?? field?.disabled), "data-required": dataAttr(props.required ?? field?.required), "data-loading": dataAttr(query.isLoading || mutation.isPending), "aria-describedby": field?.ariaDescribedby, children: [_jsx("input", { id: field?.ids?.control, ref: inputRef, type: "file", accept: "image/png, image/jpeg", onChange: (e) => {
40
+ 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) => {
39
42
  const file = e.target.files?.[0];
40
43
  if (!file)
41
44
  return;
42
45
  const error = validateFile(file);
43
46
  if (error)
44
47
  return props.onError?.(error);
45
- mutation.mutate({ file });
46
- }, disabled: field?.disabled ||
47
- props.disabled ||
48
- query.isLoading ||
49
- mutation.isPending, readOnly: field?.readOnly || props.readOnly, required: field?.required || props.required, className: "sr-only" }), !query.data?.url && (_jsxs("div", { className: "flex size-full flex-col justify-center", 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-placeholder-subtle size-4" }) }), _jsxs("h2", { className: "text-text-tertiary-600 mt-lg", children: [_jsxs("span", { className: "block text-center text-sm", children: [_jsx("button", { type: "button", onClick: () => {
50
- inputRef.current?.click();
51
- }, disabled: field?.disabled ||
52
- field?.readOnly ||
48
+ context.mutation.mutate({ file });
49
+ }, disabled: context.field?.disabled ||
50
+ context.query.isLoading ||
51
+ context.mutation.isPending ||
52
+ props.disabled, readOnly: context.field?.readOnly || props.readOnly, required: context.field?.required || props.required, className: "sr-only" }), !context.query.data?.url && (_jsxs("div", { className: "flex size-full flex-col justify-center", 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-placeholder-subtle size-4" }) }), _jsxs("h2", { className: "text-text-tertiary-600 mt-lg", children: [_jsxs("span", { className: "block text-center text-sm", children: [_jsx("button", { type: "button", onClick: () => {
53
+ context.inputRef.current?.click();
54
+ }, disabled: context.field?.disabled ||
55
+ context.field?.readOnly ||
56
+ context.query.isLoading ||
57
+ context.mutation.isPending ||
53
58
  props.disabled ||
54
- props.readOnly ||
55
- query.isLoading ||
56
- mutation.isPending, "data-required": dataAttr(props.required ?? field?.required), "data-readonly": dataAttr(props.readOnly ?? field?.readOnly), "data-loading": dataAttr(query.isLoading || mutation.isPending), "data-invalid": dataAttr(props.invalid ?? field?.invalid), className: "text-button-secondary-fg font-semibold disabled:opacity-60", children: "Click to upload" }), ' ', "or drag and drop"] }), _jsx("span", { className: "mt-xs block text-center text-xs", children: "PNG, JPG or JPEG (max. 10mb)" }), _jsx("span", { className: "m-txs block text-center text-xs", children: "or" })] }), _jsx(Button, { size: "sm", variant: "outline", className: "mx-auto mt-md w-auto", onClick: () => {
57
- disclosure.setOpen(true);
58
- }, disabled: field?.disabled ||
59
- field?.readOnly ||
59
+ props.readOnly, className: "text-button-secondary-fg font-semibold disabled:opacity-60", children: "Click to upload" }), ' ', "or drag and drop"] }), _jsx("span", { className: "mt-xs block text-center text-xs", children: "PNG, JPG or JPEG (max. 10mb)" }), _jsx("span", { className: "m-txs block text-center text-xs", children: "or" })] }), _jsx(Button, { size: "sm", variant: "outline", className: "mx-auto mt-md w-auto", onClick: () => {
60
+ context.disclosure.setOpen(true);
61
+ }, disabled: context.field?.disabled ||
62
+ context.field?.readOnly ||
63
+ context.query.isLoading ||
64
+ context.mutation.isPending ||
60
65
  props.disabled ||
61
- props.readOnly ||
62
- query.isLoading ||
63
- mutation.isPending, "data-required": dataAttr(props.required ?? field?.required), "data-readonly": dataAttr(props.readOnly ?? field?.readOnly), "data-loading": dataAttr(query.isLoading || mutation.isPending), "data-invalid": dataAttr(props.invalid ?? field?.invalid), children: "Take a photo" })] })), query.data?.url && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: query.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "h-auto w-full rounded-md" }), _jsx("button", { type: "button", className: "backdrop-blur-xs absolute right-2 top-2 rounded-full bg-white/10 p-1", onClick: () => {
64
- setValue('');
65
- }, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Dialog.Root, { open: disclosure.open, onOpenChange: (details) => {
66
- disclosure.setOpen(details.open);
67
- }, onExitComplete: 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)] 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', !camera.data && !camera.error && !camera.loading
68
- ? 'block'
69
- : 'hidden'), children: [_jsx("video", { ...camera.videoProps }), _jsx(Mask, {})] }), 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: camera.error.name }), _jsx("p", { className: "text-text-tertiary-600 mt-0.5 text-center text-xs lg:mt-1 lg:text-sm", children: camera.error.message }), _jsx(Button, { size: "xs", variant: "outline", colorScheme: "gray", fullWidth: false, className: "mt-4 lg:mt-5", onClick: () => {
70
- camera.reopen();
71
- }, children: "Try again" })] })), 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" }) })), 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: camera.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "block h-auto w-full" }) }))] }), !camera.data && !camera.error && !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: () => {
72
- const rect = guideRef.current?.getBoundingClientRect();
73
- const crop = !rect
74
- ? undefined
75
- : {
76
- y: rect.y,
77
- x: rect.x,
78
- width: rect.width,
79
- height: rect.height,
80
- };
81
- camera.snap({ crop });
82
- }, children: [_jsx(Camera01Icon, { className: "size-5" }), "Capture"] }) })), camera.data && (_jsxs("div", { className: "mt-5 flex gap-3 lg:mt-10", children: [_jsxs(Button, { onClick: () => {
83
- const file = camera.data?.file;
84
- if (!file)
85
- return;
86
- mutation.mutate({ file });
87
- disclosure.setOpen(false);
88
- }, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
89
- camera.reset();
90
- }, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) })] }));
66
+ props.readOnly, children: "Take a photo" })] })), context.query.data?.url && (_jsxs("div", { className: "relative", children: [_jsx(Image, { src: context.query.data.url, alt: "", width: 600, height: 600, unoptimized: true, className: "h-auto w-full rounded-md" }), _jsx("button", { type: "button", className: "backdrop-blur-xs absolute right-2 top-2 rounded-full bg-white/10 p-1", onClick: () => {
67
+ context.setValue('');
68
+ }, children: _jsx(XIcon, { className: "size-5" }) })] }))] }), _jsx(Camera, {})] }));
91
69
  }
92
70
  /**
71
+ * -------------------------------------
72
+ * Camera
73
+ * -------------------------------------
74
+ */
75
+ function Camera() {
76
+ const context = useSelfieFieldContext();
77
+ return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
78
+ context.disclosure.setOpen(details.open);
79
+ }, 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 &&
80
+ !context.camera.error &&
81
+ !context.camera.loading
82
+ ? 'block'
83
+ : '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: () => {
84
+ context.camera.reopen();
85
+ }, 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 &&
86
+ !context.camera.error &&
87
+ !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: () => {
88
+ const rect = context.guideRef.current?.getBoundingClientRect();
89
+ const crop = !rect
90
+ ? undefined
91
+ : {
92
+ y: rect.y,
93
+ x: rect.x,
94
+ width: rect.width,
95
+ height: rect.height,
96
+ };
97
+ context.camera.snap({ crop });
98
+ }, 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: () => {
99
+ const file = context.camera.data?.file;
100
+ if (!file)
101
+ return;
102
+ context.mutation.mutate({ file });
103
+ context.disclosure.setOpen(false);
104
+ }, children: [_jsx(CheckCircleIcon, { className: "size-5" }), "Use Photo"] }), _jsxs(Button, { variant: "outline", colorScheme: "gray", onClick: () => {
105
+ context.camera.reset();
106
+ }, children: [_jsx(RefreshCcw01Icon, { className: "size-5" }), " Retake"] })] }))] }) })] }) }));
107
+ }
108
+ /*
93
109
  * -------------------------------------
94
110
  * Mask
95
111
  * -------------------------------------
96
112
  */
97
113
  function Mask() {
98
114
  const id = useId();
99
- const { guideRef, maskRef } = useSelfieFieldContext();
115
+ const context = useSelfieFieldContext();
100
116
  const [maskBox, setMaskBox] = useState({
101
- cx: 0,
102
- cy: 0,
103
- r: 0,
117
+ x: 0,
118
+ y: 0,
119
+ width: 0,
120
+ height: 0,
104
121
  });
105
122
  useLayoutEffect(() => {
106
123
  const update = () => {
107
- const guide = guideRef.current;
108
- const mask = maskRef.current;
124
+ const guide = context.guideRef.current;
125
+ const mask = context.maskRef.current;
109
126
  if (!guide || !mask)
110
127
  return;
111
128
  const guideRect = guide.getBoundingClientRect();
112
129
  const maskRect = mask.getBoundingClientRect();
113
- const cx = guideRect.left + guideRect.width / 2 - maskRect.left;
114
- const cy = guideRect.top + guideRect.height / 2 - maskRect.top;
115
- const r = guideRect.width / 2;
116
- setMaskBox({
117
- cx,
118
- cy,
119
- r,
120
- });
130
+ const x = guideRect.left - maskRect.left;
131
+ const y = guideRect.top - maskRect.top;
132
+ const width = guideRect.width;
133
+ const height = guideRect.height;
134
+ if (width && height) {
135
+ setMaskBox({
136
+ x,
137
+ y,
138
+ width,
139
+ height,
140
+ });
141
+ }
121
142
  };
122
- update();
123
143
  const observer = new ResizeObserver(update);
124
- if (guideRef.current)
125
- observer.observe(guideRef.current);
126
- if (maskRef.current)
127
- observer.observe(maskRef.current);
144
+ if (context.guideRef.current)
145
+ observer.observe(context.guideRef.current);
146
+ if (context.maskRef.current)
147
+ observer.observe(context.maskRef.current);
148
+ requestAnimationFrame(update);
128
149
  return () => observer.disconnect();
129
- }, [guideRef, maskRef]);
130
- return (_jsxs(_Fragment, { children: [_jsx("svg", { className: "absolute h-full w-full", children: _jsxs("mask", { id: id, maskUnits: "userSpaceOnUse", children: [_jsx("rect", { width: "100%", height: "100%", fill: "white" }), _jsx("circle", { cx: maskBox.cx, cy: maskBox.cy, r: maskBox.r, fill: "black" })] }) }), _jsx("div", { ref: maskRef, className: "absolute inset-0 bg-black/60 backdrop-blur-sm", style: {
150
+ }, [context.guideRef, context.maskRef]);
151
+ 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: {
131
152
  mask: `url(#${id})`,
132
153
  WebkitMask: `url(#${id})`,
133
- } }), _jsx("div", { ref: guideRef, className: "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-full border-2 border-dashed lg:w-[29.688rem]" })] }));
154
+ maskRepeat: 'no-repeat',
155
+ WebkitMaskRepeat: 'no-repeat',
156
+ } }), _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') })] }));
134
157
  }
135
158
  function useSelfieField(props) {
136
159
  const field = useFieldContext();
@@ -166,7 +189,22 @@ function useSelfieField(props) {
166
189
  const inputRef = useRef(null);
167
190
  const maskRef = useRef(null);
168
191
  const guideRef = useRef(null);
169
- const camera = useCamera();
192
+ const camera = useCamera({
193
+ async validate(data) {
194
+ const face = await getFaceFromFile(data.file);
195
+ console.log(face);
196
+ /*
197
+ ⛏️ FIXME
198
+ if (!face) {
199
+ return {
200
+ name: 'FaceNotFoundError',
201
+ message: 'Please ensure your face is clearly visible in the photo.',
202
+ };
203
+ }
204
+ */
205
+ return null;
206
+ },
207
+ });
170
208
  const disclosure = useDisclosure({
171
209
  onOpenChange(open) {
172
210
  if (open) {
@@ -174,6 +212,37 @@ function useSelfieField(props) {
174
212
  }
175
213
  },
176
214
  });
215
+ const [faceFound, setFaceFound] = useState(false);
216
+ useInterval(async () => {
217
+ if (!camera.videoRef.current)
218
+ return setFaceFound(false);
219
+ if (!guideRef.current)
220
+ return setFaceFound(false);
221
+ const guide = guideRef.current;
222
+ const video = camera.videoRef.current;
223
+ const guideRect = guide.getBoundingClientRect();
224
+ const videoRect = video.getBoundingClientRect();
225
+ const face = await getFace(video);
226
+ if (!face)
227
+ return setFaceFound(false);
228
+ /* ⛏️ FIXME */
229
+ const scaleX = videoRect.width / video.videoWidth;
230
+ const scaleY = videoRect.height / video.videoHeight;
231
+ const offsetX = videoRect.left;
232
+ const offsetY = videoRect.top;
233
+ const faceBoxX = offsetX + face.box.xMin * video.videoWidth * scaleX;
234
+ const faceBoxY = offsetY + face.box.yMin * video.videoHeight * scaleY;
235
+ const faceBoxWidth = (face.box.xMax - face.box.xMin) * video.videoWidth * scaleX;
236
+ const faceBoxHeight = (face.box.yMax - face.box.yMin) * video.videoHeight * scaleY;
237
+ const faceIsInsideGuide = faceBoxX >= guideRect.left &&
238
+ faceBoxY >= guideRect.top &&
239
+ faceBoxX + faceBoxWidth <= guideRect.right &&
240
+ faceBoxY + faceBoxHeight <= guideRect.bottom;
241
+ console.log({ faceIsInsideGuide });
242
+ setFaceFound(true);
243
+ }, disclosure.open && !camera.data && !camera.error && !camera.loading
244
+ ? 1000
245
+ : null);
177
246
  return {
178
247
  field,
179
248
  value,
@@ -185,6 +254,53 @@ function useSelfieField(props) {
185
254
  inputRef,
186
255
  guideRef,
187
256
  maskRef,
257
+ faceFound,
188
258
  };
189
259
  }
190
260
  const [SelfieFieldProvider, useSelfieFieldContext] = createContext();
261
+ /*
262
+ * -------------------------------------
263
+ * Utils
264
+ * -------------------------------------
265
+ */
266
+ let __face_detector__;
267
+ async function getFaceDetector() {
268
+ if (__face_detector__)
269
+ return __face_detector__;
270
+ __face_detector__ = await faceDetection.createDetector(faceDetection.SupportedModels.MediaPipeFaceDetector, {
271
+ runtime: 'tfjs',
272
+ maxFaces: 1,
273
+ });
274
+ return __face_detector__;
275
+ }
276
+ async function getFace(video) {
277
+ if (!video || video.readyState < 2)
278
+ return null;
279
+ try {
280
+ const detector = await getFaceDetector();
281
+ const faces = await detector.estimateFaces(video);
282
+ return faces.at(0) ?? null;
283
+ }
284
+ catch {
285
+ return null;
286
+ }
287
+ }
288
+ function fileToImage(file) {
289
+ return new Promise((resolve, reject) => {
290
+ const img = new globalThis.Image();
291
+ img.src = URL.createObjectURL(file);
292
+ img.onload = () => resolve(img);
293
+ img.onerror = () => reject('Failed to load image');
294
+ });
295
+ }
296
+ async function getFaceFromFile(file) {
297
+ try {
298
+ const detector = await getFaceDetector();
299
+ const image = await fileToImage(file);
300
+ const faces = await detector.estimateFaces(image);
301
+ return faces.at(0) ?? null;
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ }
@@ -48,6 +48,14 @@ export const config = {
48
48
  compiler: {
49
49
  removeConsole: process.env.NODE_ENV === 'production',
50
50
  },
51
+ webpack(config) {
52
+ config.resolve.fallback = {
53
+ fs: false,
54
+ path: false,
55
+ crypto: false,
56
+ };
57
+ return config;
58
+ },
51
59
  };
52
60
  export function extendConfig(userConfig) {
53
61
  return {