@opexa/portal-components 0.0.679 → 0.0.681
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.
- package/dist/client/hooks/useCamera.d.ts +2 -1
- package/dist/client/hooks/useCamera.js +127 -21
- package/dist/client/utils/biometric.js +1 -1
- package/dist/components/Banner/Banner.client.d.ts +12 -0
- package/dist/components/Banner/Banner.client.js +49 -0
- package/dist/components/DepositWithdrawal/Deposit/QRPHDeposit/QRPHQRCode.js +1 -1
- package/dist/components/DigitainLauncher/Loading.d.ts +1 -0
- package/dist/components/DigitainLauncher/Loading.js +5 -0
- package/dist/components/GameLaunch/GameLaunchTrigger.js +64 -11
- package/dist/components/KYC/BasicInformation.d.ts +1 -0
- package/dist/components/KYC/BasicInformation.js +101 -0
- package/dist/components/KYC/IdentityVerification.d.ts +1 -0
- package/dist/components/KYC/IdentityVerification.js +120 -0
- package/dist/components/KYC/Indicator.d.ts +1 -0
- package/dist/components/KYC/Indicator.js +8 -0
- package/dist/components/KYC/KYC.lazy.d.ts +6 -0
- package/dist/components/KYC/KYC.lazy.js +45 -0
- package/dist/components/KYC/KYCContext.d.ts +6 -0
- package/dist/components/KYC/KYCContext.js +2 -0
- package/dist/components/KYC/KYCDefault/PersonalInformation.js +11 -1
- package/dist/components/KYC/KYCReminder.lazy.js +8 -10
- package/dist/components/KYC/KYCVerificationStatus.lazy.js +7 -4
- package/dist/components/KYC/KycOpenOnHomeMount.js +1 -0
- package/dist/components/KYC/PersonalInformation.d.ts +1 -0
- package/dist/components/KYC/PersonalInformation.js +122 -0
- package/dist/components/KYC/useKYC.d.ts +25 -0
- package/dist/components/KYC/useKYC.js +38 -0
- package/dist/components/PortalProvider/AndroidOnlyComponents.d.ts +1 -0
- package/dist/components/PortalProvider/AndroidOnlyComponents.js +12 -0
- package/dist/components/PortalProvider/CXDTokenObserver.d.ts +1 -0
- package/dist/components/PortalProvider/CXDTokenObserver.js +30 -0
- package/dist/components/RegisterBiometrics/RegisterBiometrics.js +3 -1
- package/dist/components/SignIn/SignInTrigger.d.ts +1 -1
- package/dist/components/SignIn/SignInTrigger.js +62 -7
- package/dist/components/SignIn/utils.d.ts +8 -0
- package/dist/components/SignIn/utils.js +26 -0
- package/dist/components/shared/IdFrontImageField/IdFrontImageField.client.js +12 -2
- package/dist/components/shared/SelfieImageField/SelfieImageField.client.js +11 -2
- package/dist/constants/Branches.d.ts +2 -0
- package/dist/constants/Branches.js +42 -0
- package/dist/images/responsible-gaming-yellow.png +0 -0
- package/dist/third-parties/FacebookPixel/FacebookPixel.d.ts +4 -0
- package/dist/third-parties/FacebookPixel/FacebookPixel.js +4 -0
- package/dist/third-parties/FacebookPixel/api.d.ts +0 -0
- package/dist/third-parties/FacebookPixel/api.js +1 -0
- package/dist/third-parties/FacebookPixel/index.d.ts +1 -0
- package/dist/third-parties/FacebookPixel/index.js +1 -0
- package/dist/third-parties/GoogleRecaptcha/GoogleRecaptcha.d.ts +4 -0
- package/dist/third-parties/GoogleRecaptcha/GoogleRecaptcha.js +4 -0
- package/dist/third-parties/GoogleRecaptcha/api.d.ts +0 -0
- package/dist/third-parties/GoogleRecaptcha/api.js +1 -0
- package/dist/third-parties/GoogleRecaptcha/index.d.ts +1 -0
- package/dist/third-parties/GoogleRecaptcha/index.js +1 -0
- package/dist/third-parties/index.d.ts +2 -0
- package/dist/third-parties/index.js +2 -0
- package/dist/ui/Carousel/Carousel.d.ts +45 -45
- package/dist/ui/Carousel/carousel.recipe.d.ts +5 -5
- package/dist/ui/Checkbox/Checkbox.d.ts +23 -23
- package/dist/ui/Checkbox/checkbox.recipe.d.ts +3 -3
- package/dist/ui/Clipboard/Clipboard.d.ts +18 -18
- package/dist/ui/Clipboard/clipboard.recipe.d.ts +3 -3
- package/dist/ui/Collapsible/Collapsible.d.ts +20 -20
- package/dist/ui/Collapsible/collapsible.recipe.d.ts +5 -5
- package/dist/ui/Combobox/Combobox.d.ts +42 -42
- package/dist/ui/Combobox/combobox.recipe.d.ts +3 -3
- package/dist/ui/DatePicker/DatePicker.d.ts +72 -72
- package/dist/ui/DatePicker/datePicker.recipe.d.ts +3 -3
- package/dist/ui/Menu/Menu.d.ts +306 -306
- package/dist/ui/Menu/menu.recipe.d.ts +17 -17
- package/dist/ui/PasswordInput/PasswordInput.d.ts +18 -18
- package/dist/ui/PasswordInput/passwordInput.recipe.d.ts +3 -3
- package/dist/ui/Popover/Popover.d.ts +55 -55
- package/dist/ui/Popover/popover.recipe.d.ts +5 -5
- package/dist/ui/Progress/Progress.d.ts +27 -27
- package/dist/ui/Progress/progress.recipe.d.ts +3 -3
- package/dist/ui/Select/Select.d.ts +45 -45
- package/dist/ui/Select/select.recipe.d.ts +3 -3
- package/dist/ui/Tabs/Tabs.d.ts +15 -15
- package/dist/ui/Tabs/tabs.recipe.d.ts +3 -3
- package/package.json +2 -1
|
@@ -11,8 +11,9 @@ export interface CameraData {
|
|
|
11
11
|
}
|
|
12
12
|
export interface UseCameraReturn<T extends string = never> {
|
|
13
13
|
open(): Promise<void>;
|
|
14
|
+
openNativeCamera(): Promise<CameraData | null>;
|
|
14
15
|
close(): Promise<void>;
|
|
15
|
-
snap(): CameraData | null
|
|
16
|
+
snap(): Promise<CameraData | null>;
|
|
16
17
|
reopen(): Promise<void>;
|
|
17
18
|
reset(): Promise<void>;
|
|
18
19
|
data: CameraData | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isBoolean } from 'lodash-es';
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
|
+
import invariant from 'tiny-invariant';
|
|
3
4
|
import { useMediaQuery } from 'usehooks-ts';
|
|
4
5
|
export function useCamera(options = {}) {
|
|
5
6
|
const videoRef = useRef(null);
|
|
@@ -78,6 +79,67 @@ export function useCamera(options = {}) {
|
|
|
78
79
|
setLoading(false);
|
|
79
80
|
}
|
|
80
81
|
}, [options, desktop]);
|
|
82
|
+
const openNativeCamera = useCallback(async () => {
|
|
83
|
+
setData(null);
|
|
84
|
+
setError(null);
|
|
85
|
+
setSnapping(true);
|
|
86
|
+
try {
|
|
87
|
+
// Dynamically import only when running native
|
|
88
|
+
const { Camera, CameraResultType } = await import('@capacitor/camera');
|
|
89
|
+
const photo = await Camera.getPhoto({
|
|
90
|
+
quality: 90,
|
|
91
|
+
resultType: CameraResultType.Uri,
|
|
92
|
+
saveToGallery: false,
|
|
93
|
+
allowEditing: false,
|
|
94
|
+
});
|
|
95
|
+
if (!photo.webPath) {
|
|
96
|
+
throw new Error('No photo returned from native camera');
|
|
97
|
+
}
|
|
98
|
+
console.log(photo, 'photo');
|
|
99
|
+
const response = await fetch(photo.webPath);
|
|
100
|
+
const blob = await response.blob();
|
|
101
|
+
const file = new File([blob], `${crypto.randomUUID()}.jpeg`, {
|
|
102
|
+
type: blob.type,
|
|
103
|
+
lastModified: Date.now(),
|
|
104
|
+
});
|
|
105
|
+
// Convert blob → base64 data URL
|
|
106
|
+
const url = await new Promise((resolve, reject) => {
|
|
107
|
+
const reader = new FileReader();
|
|
108
|
+
reader.onloadend = () => {
|
|
109
|
+
if (typeof reader.result === 'string')
|
|
110
|
+
resolve(reader.result);
|
|
111
|
+
else
|
|
112
|
+
reject(new Error('Failed to read image as data URL'));
|
|
113
|
+
};
|
|
114
|
+
reader.onerror = reject;
|
|
115
|
+
reader.readAsDataURL(blob);
|
|
116
|
+
});
|
|
117
|
+
const image = new Image();
|
|
118
|
+
image.src = url;
|
|
119
|
+
if (!image.complete || image.naturalWidth === 0) {
|
|
120
|
+
await new Promise((resolve, reject) => {
|
|
121
|
+
image.onload = () => resolve();
|
|
122
|
+
image.onerror = () => reject(new Error('Failed to load preview image'));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const data = {
|
|
126
|
+
url,
|
|
127
|
+
file,
|
|
128
|
+
image,
|
|
129
|
+
};
|
|
130
|
+
setData(data);
|
|
131
|
+
setSnapping(false);
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
setError({
|
|
136
|
+
name: 'CameraError',
|
|
137
|
+
message: e instanceof Error ? e.message : 'Failed to open native camera',
|
|
138
|
+
});
|
|
139
|
+
setSnapping(false);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}, [setData, setError, setSnapping]);
|
|
81
143
|
const close = useCallback(() => {
|
|
82
144
|
setData(null);
|
|
83
145
|
setError(null);
|
|
@@ -89,33 +151,76 @@ export function useCamera(options = {}) {
|
|
|
89
151
|
resolve();
|
|
90
152
|
});
|
|
91
153
|
}, []);
|
|
92
|
-
const snap = useCallback(() => {
|
|
154
|
+
const snap = useCallback(async () => {
|
|
155
|
+
setData(null);
|
|
156
|
+
setError(null);
|
|
157
|
+
setSnapping(true);
|
|
93
158
|
const video = videoRef.current;
|
|
94
|
-
if (!video)
|
|
95
|
-
return null;
|
|
96
159
|
const canvas = document.createElement('canvas');
|
|
97
160
|
const context = canvas.getContext('2d');
|
|
98
|
-
|
|
99
|
-
|
|
161
|
+
invariant(video, 'Could not find video element');
|
|
162
|
+
invariant(context, 'Could not get canvas context');
|
|
163
|
+
video.currentTime = 1;
|
|
100
164
|
canvas.width = video.videoWidth;
|
|
101
165
|
canvas.height = video.videoHeight;
|
|
102
|
-
context.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
context.imageSmoothingEnabled = true;
|
|
167
|
+
context.imageSmoothingQuality = 'high';
|
|
168
|
+
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
|
169
|
+
return new Promise((resolve) => {
|
|
170
|
+
canvas.toBlob(async (blob) => {
|
|
171
|
+
if (!blob) {
|
|
172
|
+
setSnapping(false);
|
|
173
|
+
resolve(null);
|
|
174
|
+
return setError({
|
|
175
|
+
name: 'CameraError',
|
|
176
|
+
message: "'canvas.toBlob' failed to create blob",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const url = canvas.toDataURL('image/jpeg', 1);
|
|
180
|
+
const file = new File([blob], `${crypto.randomUUID()}.jpeg`, {
|
|
181
|
+
type: 'image/jpeg',
|
|
182
|
+
endings: 'native',
|
|
183
|
+
lastModified: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
const image = new Image();
|
|
186
|
+
image.src = url;
|
|
187
|
+
image.alt = '';
|
|
188
|
+
image.width = canvas.width;
|
|
189
|
+
image.height = canvas.height;
|
|
190
|
+
if (!image.complete || image.naturalWidth === 0) {
|
|
191
|
+
await new Promise((resolve, reject) => {
|
|
192
|
+
image.onload = () => resolve();
|
|
193
|
+
image.onerror = () => reject();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const data = {
|
|
197
|
+
url,
|
|
198
|
+
file,
|
|
199
|
+
image,
|
|
200
|
+
};
|
|
201
|
+
if (!options.transform) {
|
|
202
|
+
setData(data);
|
|
203
|
+
setSnapping(false);
|
|
204
|
+
resolve(data);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const transformResult = await options.transform({
|
|
208
|
+
...data,
|
|
209
|
+
video,
|
|
210
|
+
canvas,
|
|
211
|
+
});
|
|
212
|
+
if (transformResult.ok) {
|
|
213
|
+
setData(transformResult.data);
|
|
214
|
+
resolve(transformResult.data);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
setError(transformResult.error);
|
|
218
|
+
resolve(null);
|
|
219
|
+
}
|
|
220
|
+
setSnapping(false);
|
|
221
|
+
}, 'image/jpeg', 1);
|
|
110
222
|
});
|
|
111
|
-
|
|
112
|
-
image.src = url;
|
|
113
|
-
image.width = canvas.width;
|
|
114
|
-
image.height = canvas.height;
|
|
115
|
-
const data = { url, file, image };
|
|
116
|
-
setData(data);
|
|
117
|
-
return data;
|
|
118
|
-
}, []);
|
|
223
|
+
}, [options]);
|
|
119
224
|
const reset = useCallback(() => {
|
|
120
225
|
setData(null);
|
|
121
226
|
setError(null);
|
|
@@ -157,6 +262,7 @@ export function useCamera(options = {}) {
|
|
|
157
262
|
return {
|
|
158
263
|
snap,
|
|
159
264
|
open,
|
|
265
|
+
openNativeCamera,
|
|
160
266
|
close,
|
|
161
267
|
reopen,
|
|
162
268
|
reset,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { addDays, isAfter } from 'date-fns';
|
|
2
|
-
const SERVER =
|
|
2
|
+
const SERVER = `${process.env.NEXT_PUBLIC_PLATFORM_CODE?.toLocaleLowerCase()}.app`;
|
|
3
3
|
export const BIOMETRIC_STORAGE_KEY = `${process.env.NEXT_PUBLIC_PLATFORM_CODE}__BiometricEnabled`;
|
|
4
4
|
export var BiometryType;
|
|
5
5
|
(function (BiometryType) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ImageProps } from 'next/image';
|
|
2
|
+
export interface BannerEntry {
|
|
3
|
+
src: ImageProps['src'];
|
|
4
|
+
redirectUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface BannerProps {
|
|
7
|
+
banners: BannerEntry[];
|
|
8
|
+
imageWidth?: number | [mobile: number, desktop: number];
|
|
9
|
+
imageHeight?: number | [mobile: number, desktop: number];
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function Banner__client(props: BannerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import emblaCarouselAutoplay from 'embla-carousel-autoplay';
|
|
4
|
+
import useEmblaCarousel, {} from 'embla-carousel-react';
|
|
5
|
+
import isMobile from 'is-mobile';
|
|
6
|
+
import Image from 'next/image';
|
|
7
|
+
import Link from 'next/link';
|
|
8
|
+
import { Fragment, useEffect, useState } from 'react';
|
|
9
|
+
import { twMerge } from 'tailwind-merge';
|
|
10
|
+
import { dataAttr } from '../../utils/dataAttr.js';
|
|
11
|
+
export function Banner__client(props) {
|
|
12
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({
|
|
13
|
+
loop: true,
|
|
14
|
+
align: 'start',
|
|
15
|
+
}, [
|
|
16
|
+
emblaCarouselAutoplay({
|
|
17
|
+
playOnInit: true,
|
|
18
|
+
delay: 5000,
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
21
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
function handler(detail) {
|
|
24
|
+
setCurrentIndex(detail?.selectedScrollSnap() ?? 0);
|
|
25
|
+
}
|
|
26
|
+
emblaApi?.on('scroll', handler);
|
|
27
|
+
emblaApi?.on('init', handler);
|
|
28
|
+
return () => {
|
|
29
|
+
emblaApi?.on('init', handler);
|
|
30
|
+
emblaApi?.off('scroll', handler);
|
|
31
|
+
};
|
|
32
|
+
}, [emblaApi]);
|
|
33
|
+
const imageWidth = !props.imageWidth
|
|
34
|
+
? [400, 1200]
|
|
35
|
+
: Array.isArray(props.imageWidth)
|
|
36
|
+
? props.imageWidth
|
|
37
|
+
: [props.imageWidth, props.imageWidth];
|
|
38
|
+
const imageHeight = !props.imageHeight
|
|
39
|
+
? [225, 300]
|
|
40
|
+
: Array.isArray(props.imageHeight)
|
|
41
|
+
? props.imageHeight
|
|
42
|
+
: [props.imageHeight, props.imageHeight];
|
|
43
|
+
return (_jsxs("div", { className: twMerge('relative', props.className), children: [_jsx("div", { ref: emblaRef, className: "overflow-hidden", children: _jsx("div", { className: "flex gap-2", children: props.banners.map((banner, index) => {
|
|
44
|
+
const img = (_jsx(Image, { src: banner.src, alt: "", width: isMobile() ? imageWidth[0] : imageWidth[1], height: isMobile() ? imageHeight[0] : imageHeight[1], className: "block h-auto w-full", priority: index === 0 }));
|
|
45
|
+
return (_jsx(Fragment, { children: !banner.redirectUrl ? (_jsx("div", { className: "w-full shrink-0", children: img })) : (_jsx(Link, { href: banner.redirectUrl, className: "block w-full shrink-0", "aria-label": `Open ${banner.redirectUrl}`, children: img })) }, index));
|
|
46
|
+
}) }) }), _jsx("div", { className: "absolute bottom-lg left-1/2 flex w-fit -translate-x-1/2 gap-2", children: props.banners.map((_, index) => (_jsx("button", { type: "button", className: "ui-active:bg-brand-500 bg-gray-300 size-3 shrink-0 rounded-full border border-white transition-all duration-300 ui-active:w-9", onClick: () => {
|
|
47
|
+
emblaApi?.scrollTo(index);
|
|
48
|
+
}, "aria-label": `Go to slide ${index + 1}`, "data-active": dataAttr(index === currentIndex) }, index))) })] }));
|
|
49
|
+
}
|
|
@@ -32,5 +32,5 @@ export function QRPHQRCode(props) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
return (_jsxs(QrCode.Root, { value: props.qrCodeString, className: "mx-auto mt-6 w-[12.5rem] rounded-lg border border-border-primary bg-bg-brand-secondary-alt p-5 lg:w-[13rem] lg:p-6", children: [_jsxs("div", { className: "relative", children: [_jsx(QrCode.Frame, {
|
|
35
|
+
return (_jsxs(QrCode.Root, { value: props.qrCodeString, className: "mx-auto mt-6 w-[12.5rem] rounded-lg border border-border-primary bg-bg-brand-secondary-alt p-5 lg:w-[13rem] lg:p-6", children: [_jsxs("div", { className: "relative", children: [_jsx(QrCode.Frame, { className: "mx-auto size-[10rem] rounded-[0.25rem] border border-border-primary bg-white", children: _jsx(QrCode.Pattern, {}) }), _jsx(QrCode.Overlay, { className: "bg-white p-0.5", children: _jsx(Image, { src: qrphIcon, alt: "", className: "size-8", width: 40, height: 40 }) })] }), Capacitor.isNativePlatform() ? (_jsx("button", { type: "button", onClick: handleNativeDownload, className: "mt-5 block w-full text-center font-semibold text-sm text-text-secondary-700", children: "Download QR Code to Device" })) : (_jsx(QrCode.DownloadTrigger, { fileName: `qrcode-${Date.now()}`, mimeType: "image/png", className: "mt-5 block w-full text-center font-semibold text-sm text-text-secondary-700", children: "Download QR Code" }))] }));
|
|
36
36
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Loading(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
export function Loading() {
|
|
4
|
+
return (_jsx("div", { className: "flex h-[calc(100vh-400px)] w-full items-center justify-center px-4", children: _jsxs("div", { className: "flex flex-col items-center space-y-4", children: [_jsx("div", { className: "h-10 w-10 animate-spin rounded-full border-4 border-blue-500 border-t-transparent" }), _jsx("p", { className: "font-medium text-gray-700 text-lg", children: "Loading Sports Book\u2026" })] }) }));
|
|
5
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { ark } from '@ark-ui/react/factory';
|
|
4
4
|
import { BiometricAuthError } from 'capacitor-native-biometric';
|
|
5
|
+
import { useState } from 'react';
|
|
5
6
|
import { useShallow } from 'zustand/shallow';
|
|
6
7
|
import { useCreateGameSessionMutation } from '../../client/hooks/useCreateGameSessionMutation.js';
|
|
7
8
|
import { useGlobalStore } from '../../client/hooks/useGlobalStore.js';
|
|
@@ -9,7 +10,7 @@ import { useMemberVerificationQuery } from '../../client/hooks/useMemberVerifica
|
|
|
9
10
|
import { useSessionQuery } from '../../client/hooks/useSessionQuery.js';
|
|
10
11
|
import { getSession } from '../../client/services/getSession.js';
|
|
11
12
|
import { signIn } from '../../client/services/signIn.js';
|
|
12
|
-
import { getBiometricCredentials, getBiometricInfo, hasSavedBiometry, performBiometricVerification, saveBiometricCredentials, } from '../../client/utils/biometric.js';
|
|
13
|
+
import { deleteBiometricCredentials, getBiometricCredentials, getBiometricInfo, hasSavedBiometry, performBiometricVerification, saveBiometricCredentials, } from '../../client/utils/biometric.js';
|
|
13
14
|
import { toaster } from '../../client/utils/toaster.js';
|
|
14
15
|
import { createSingleUseToken } from '../../services/auth.js';
|
|
15
16
|
import { getQueryClient } from '../../utils/getQueryClient.js';
|
|
@@ -23,6 +24,7 @@ export function GameLaunchTrigger(props) {
|
|
|
23
24
|
gameLaunch: ctx.gameLaunch,
|
|
24
25
|
kycVerificationStatus: ctx.kycVerificationStatus,
|
|
25
26
|
})));
|
|
27
|
+
const [hasCancelledBiometric, setHasCancelledBiometric] = useState(false);
|
|
26
28
|
const verificationStatus = verificationQuery.data?.status ?? 'UNVERIFIED';
|
|
27
29
|
const currentHour = new Date().getHours();
|
|
28
30
|
const between3amAnd3pm = currentHour >= 15 || currentHour < 3;
|
|
@@ -44,7 +46,7 @@ export function GameLaunchTrigger(props) {
|
|
|
44
46
|
: 'open', ...props, disabled: disabled, onClick: async (e) => {
|
|
45
47
|
props.onClick?.(e);
|
|
46
48
|
if (sessionQuery.data?.status === 'unauthenticated') {
|
|
47
|
-
if (hasSavedBiometry()) {
|
|
49
|
+
if (hasSavedBiometry() && !hasCancelledBiometric) {
|
|
48
50
|
const ok = await performBiometricVerification({
|
|
49
51
|
reason: 'Login to your account',
|
|
50
52
|
title: 'Login',
|
|
@@ -56,7 +58,10 @@ export function GameLaunchTrigger(props) {
|
|
|
56
58
|
const info = await getBiometricInfo();
|
|
57
59
|
if (info.errorCode === BiometricAuthError.APP_CANCEL ||
|
|
58
60
|
info.errorCode === BiometricAuthError.USER_CANCEL ||
|
|
59
|
-
info.errorCode === BiometricAuthError.SYSTEM_CANCEL
|
|
61
|
+
info.errorCode === BiometricAuthError.SYSTEM_CANCEL ||
|
|
62
|
+
info.errorCode === undefined ||
|
|
63
|
+
info.errorCode === null) {
|
|
64
|
+
setHasCancelledBiometric(true);
|
|
60
65
|
console.log('Biometric verification cancelled');
|
|
61
66
|
}
|
|
62
67
|
else {
|
|
@@ -71,10 +76,21 @@ export function GameLaunchTrigger(props) {
|
|
|
71
76
|
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
72
77
|
return;
|
|
73
78
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
try {
|
|
80
|
+
console.log('Signing in using biometric credentials');
|
|
81
|
+
await signIn({
|
|
82
|
+
type: 'SINGLE_USE_TOKEN',
|
|
83
|
+
token: credentials.password,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
toaster.error({
|
|
88
|
+
title: 'Biometric sign-in token has expired.',
|
|
89
|
+
description: 'Please sign in with your mobile number or username to re-enable biometric login.',
|
|
90
|
+
});
|
|
91
|
+
deleteBiometricCredentials();
|
|
92
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
93
|
+
}
|
|
78
94
|
getQueryClient().invalidateQueries({
|
|
79
95
|
queryKey: getSessionQueryKey(),
|
|
80
96
|
});
|
|
@@ -95,19 +111,56 @@ export function GameLaunchTrigger(props) {
|
|
|
95
111
|
else {
|
|
96
112
|
console.warn('Failed to updated biometric credentials');
|
|
97
113
|
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
98
|
-
return;
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
116
|
else {
|
|
102
117
|
console.error('Failed to create token');
|
|
103
118
|
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
104
|
-
return;
|
|
105
119
|
}
|
|
106
120
|
}
|
|
107
121
|
else {
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
// still update biometric credentials if user has cancelled biometric once
|
|
123
|
+
if (hasCancelledBiometric) {
|
|
124
|
+
const credentials = await getBiometricCredentials();
|
|
125
|
+
if (!credentials) {
|
|
126
|
+
toaster.error({ description: 'Biometric verification failed' });
|
|
127
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
await signIn({
|
|
131
|
+
type: 'SINGLE_USE_TOKEN',
|
|
132
|
+
token: credentials.password,
|
|
133
|
+
});
|
|
134
|
+
getQueryClient().invalidateQueries({
|
|
135
|
+
queryKey: getSessionQueryKey(),
|
|
136
|
+
});
|
|
137
|
+
const session = await getSession();
|
|
138
|
+
const r = await createSingleUseToken({
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Bearer ${session.token}`,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
if (r.token) {
|
|
144
|
+
const saved = await saveBiometricCredentials({
|
|
145
|
+
username: credentials.username,
|
|
146
|
+
password: r.token,
|
|
147
|
+
});
|
|
148
|
+
if (saved) {
|
|
149
|
+
console.info('Biometric credentials has been updated');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
console.warn('Failed to updated biometric credentials');
|
|
153
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.error('Failed to create token');
|
|
158
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
110
162
|
}
|
|
163
|
+
return props.onClick?.(e);
|
|
111
164
|
}
|
|
112
165
|
//handle new kyc process to play only on verified members only
|
|
113
166
|
if (verificationStatus === 'PENDING' ||
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function BasicInformation(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
3
|
+
import { differenceInYears, format, isSameDay, isValid, parse } from 'date-fns';
|
|
4
|
+
import { isNil, omitBy, size } from 'lodash-es';
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
import { useForm } from 'react-hook-form';
|
|
7
|
+
import invariant from 'tiny-invariant';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { useAccountQuery } from '../../client/hooks/useAccountQuery.js';
|
|
10
|
+
import { useUpdateAccountMutation } from '../../client/hooks/useUpdateAccountMutation.js';
|
|
11
|
+
import { toaster } from '../../client/utils/toaster.js';
|
|
12
|
+
import { Button } from '../../ui/Button/index.js';
|
|
13
|
+
import { Dialog } from '../../ui/Dialog/index.js';
|
|
14
|
+
import { Field } from '../../ui/Field/index.js';
|
|
15
|
+
import { useKYCContext } from './KYCContext.js';
|
|
16
|
+
const definition = z.object({
|
|
17
|
+
realName: z
|
|
18
|
+
.string()
|
|
19
|
+
.min(3, 'Name must be 3 or more characters')
|
|
20
|
+
.max(50, 'Name must not be more than 50 characters')
|
|
21
|
+
.regex(/^[a-z0-9 ]+$/gi, 'Name must not contain special characters')
|
|
22
|
+
.trim(),
|
|
23
|
+
birthDay: z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1, 'Date of birth is required')
|
|
26
|
+
.superRefine((value, ctx) => {
|
|
27
|
+
const dob = parse(value, 'yyyy-MM-dd', new Date());
|
|
28
|
+
if (!isValid(dob)) {
|
|
29
|
+
return ctx.addIssue({
|
|
30
|
+
code: z.ZodIssueCode.invalid_date,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const age = differenceInYears(now, dob);
|
|
35
|
+
if (age < 21) {
|
|
36
|
+
return ctx.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
message: 'You must be at least 21 years old',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}),
|
|
42
|
+
branchCode: z.string().min(4).max(10).optional().or(z.literal('')),
|
|
43
|
+
});
|
|
44
|
+
export function BasicInformation() {
|
|
45
|
+
const kyc = useKYCContext();
|
|
46
|
+
const form = useForm({
|
|
47
|
+
resolver: zodResolver(definition),
|
|
48
|
+
mode: 'all',
|
|
49
|
+
defaultValues: {
|
|
50
|
+
birthDay: '',
|
|
51
|
+
branchCode: '',
|
|
52
|
+
realName: '',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const accountQuery = useAccountQuery();
|
|
56
|
+
const account = accountQuery.data;
|
|
57
|
+
const stepCompleted = account != null && account.realName != null && account.birthDay != null;
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (stepCompleted)
|
|
60
|
+
kyc.setStep(2);
|
|
61
|
+
}, [stepCompleted, kyc]);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (account) {
|
|
64
|
+
form.reset({
|
|
65
|
+
realName: account.realName ?? '',
|
|
66
|
+
birthDay: account.birthDay
|
|
67
|
+
? format(account.birthDay, 'yyyy-MM-dd')
|
|
68
|
+
: '',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}, [account, form]);
|
|
72
|
+
const updateAccountMutation = useUpdateAccountMutation({
|
|
73
|
+
onError(error) {
|
|
74
|
+
toaster.error({
|
|
75
|
+
title: 'Error',
|
|
76
|
+
description: error.message,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
onSuccess() {
|
|
80
|
+
kyc.setStep(2);
|
|
81
|
+
toaster.success({
|
|
82
|
+
title: 'Success',
|
|
83
|
+
description: 'Basic information has been set successfully.',
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return (_jsxs("div", { children: [_jsx(Dialog.Title, { className: "text-center font-semibold text-lg", children: "Basic Information" }), _jsx(Dialog.Description, { className: "mt-xs text-center text-sm text-text-secondary-700", children: "Enter your basic details for identification and communication." }), _jsxs("form", { className: "mt-3", onSubmit: form.handleSubmit((data) => {
|
|
88
|
+
invariant(account);
|
|
89
|
+
const input = omitBy({
|
|
90
|
+
realName: account.realName === data.realName ? undefined : data.realName,
|
|
91
|
+
birthDay: account.birthDay && isSameDay(data.birthDay, account.birthDay)
|
|
92
|
+
? undefined
|
|
93
|
+
: format(data.birthDay, 'yyyy-MM-dd'),
|
|
94
|
+
}, isNil);
|
|
95
|
+
if (size(input) === 0)
|
|
96
|
+
return kyc.setStep(2);
|
|
97
|
+
updateAccountMutation.mutate(input);
|
|
98
|
+
}), children: [_jsxs(Field.Root, { invalid: !!form.formState.errors.realName, readOnly: !!accountQuery.data?.realName, children: [_jsx(Field.Label, { children: "Real Name" }), _jsx(Field.Input, { placeholder: "Enter your real name", ...form.register('realName') }), _jsx(Field.ErrorText, { children: form.formState.errors.realName?.message })] }), _jsxs(Field.Root, { className: "mt-3", invalid: !!form.formState.errors.branchCode, readOnly: !!accountQuery.data?.birthDay, children: [_jsx(Field.Label, { children: "Date of Birth" }), _jsx(Field.Input, { type: "date", ...form.register('birthDay') }), _jsx(Field.ErrorText, { children: form.formState.errors.birthDay?.message })] }), _jsx(Button, { type: "submit", className: "mt-8", disabled: accountQuery.isLoading ||
|
|
99
|
+
updateAccountMutation.isPending ||
|
|
100
|
+
stepCompleted, children: "Continue" })] })] }));
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function IdentityVerification(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,120 @@
|
|
|
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 { Controller, useForm } from 'react-hook-form';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { useShallow } from 'zustand/shallow';
|
|
8
|
+
import { useCreateMemberVerificationMutation } from '../../client/hooks/useCreateMemberVerificationMutation.js';
|
|
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';
|
|
13
|
+
import { Button } from '../../ui/Button/index.js';
|
|
14
|
+
import { Dialog } from '../../ui/Dialog/index.js';
|
|
15
|
+
import { Field } from '../../ui/Field/index.js';
|
|
16
|
+
import { getQueryClient } from '../../utils/getQueryClient.js';
|
|
17
|
+
import { getMemberVerificationQueryKey } from '../../utils/queryKeys.js';
|
|
18
|
+
import { IdFrontImageField } from '../shared/IdFrontImageField/index.js';
|
|
19
|
+
import { SelfieImageField } from '../shared/SelfieImageField/index.js';
|
|
20
|
+
import { useKYCContext } from './KYCContext.js';
|
|
21
|
+
const formSchema = z.object({
|
|
22
|
+
selfieImage: z.string().min(1, 'Selfie image is required.'),
|
|
23
|
+
idFrontImage: z.string().min(1, 'Front image of ID is required.'),
|
|
24
|
+
});
|
|
25
|
+
export function IdentityVerification() {
|
|
26
|
+
const kyc = useKYCContext();
|
|
27
|
+
const { mutate: createMemberVerification, isPending: createPending } = useCreateMemberVerificationMutation({
|
|
28
|
+
onSuccess: () => {
|
|
29
|
+
toaster.success({
|
|
30
|
+
title: 'ID Front Image & Selfie Image uploaded successfully',
|
|
31
|
+
});
|
|
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
|
+
const queryClient = getQueryClient();
|
|
43
|
+
toaster.success({
|
|
44
|
+
title: 'ID Front Image & Selfie Image updated successfully',
|
|
45
|
+
});
|
|
46
|
+
queryClient.setQueryData(getMemberVerificationQueryKey(), (prev) => {
|
|
47
|
+
if (!prev)
|
|
48
|
+
return prev;
|
|
49
|
+
return {
|
|
50
|
+
...prev,
|
|
51
|
+
status: 'CREATED',
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
onError: (error) => {
|
|
56
|
+
toaster.error({
|
|
57
|
+
title: 'Failed to upload ID Front Image & Selfie Image',
|
|
58
|
+
description: error.message,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const memberVerification = useMemberVerificationQuery();
|
|
63
|
+
const memberId = memberVerification.data?.id;
|
|
64
|
+
const globalStore = useGlobalStore(useShallow((ctx) => ({
|
|
65
|
+
kyc: ctx.kyc,
|
|
66
|
+
})));
|
|
67
|
+
const form = useForm({
|
|
68
|
+
resolver: zodResolver(formSchema),
|
|
69
|
+
defaultValues: {
|
|
70
|
+
idFrontImage: '',
|
|
71
|
+
selfieImage: '',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
function onSubmit(values) {
|
|
75
|
+
if (!memberId) {
|
|
76
|
+
createMemberVerification({
|
|
77
|
+
selfieImage: values.selfieImage,
|
|
78
|
+
idFrontImage: values.idFrontImage,
|
|
79
|
+
address: '',
|
|
80
|
+
nationality: '',
|
|
81
|
+
natureOfWork: '',
|
|
82
|
+
permanentAddress: '',
|
|
83
|
+
placeOfBirth: '',
|
|
84
|
+
sourceOfIncome: '',
|
|
85
|
+
});
|
|
86
|
+
kyc.setStep(3);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
updateMemberVerification({
|
|
90
|
+
id: memberId,
|
|
91
|
+
data: {
|
|
92
|
+
selfieImage: values.selfieImage,
|
|
93
|
+
idFrontImage: values.idFrontImage,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
kyc.setStep(3);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (kyc.selfieImageId)
|
|
101
|
+
form.setValue('selfieImage', kyc.selfieImageId);
|
|
102
|
+
if (kyc.idFrontImageId)
|
|
103
|
+
form.setValue('idFrontImage', kyc.idFrontImageId);
|
|
104
|
+
}, [form, kyc.idFrontImageId, kyc.selfieImageId]);
|
|
105
|
+
return (_jsxs("div", { children: [_jsx(Dialog.Title, { className: "text-center font-semibold text-lg", children: "Verify your identity" }), _jsx(Dialog.Description, { className: "mt-xs text-center text-sm text-text-secondary-700", children: "Verify your details to confirm your identity and secure your access." }), _jsxs("div", { className: "mt-7 rounded-xl border border-border-primary bg-bg-primary p-lg text-text-placeholder 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: [_jsx(Controller, { control: form.control, name: "idFrontImage", render: (o) => (_jsxs(Field.Root, { invalid: o.fieldState.invalid, children: [_jsx(Field.Label, { children: "Front of your ID" }), _jsx(IdFrontImageField, { value: o.field.value, onChange: o.field.onChange, onError: (error) => {
|
|
106
|
+
form.setValue('idFrontImage', '');
|
|
107
|
+
form.setError('idFrontImage', {
|
|
108
|
+
type: 'validate',
|
|
109
|
+
message: error.message,
|
|
110
|
+
});
|
|
111
|
+
} }), _jsx(Field.ErrorText, { children: o.fieldState.error?.message })] })) }), _jsx(Controller, { control: form.control, name: "selfieImage", render: (o) => (_jsxs(Field.Root, { invalid: o.fieldState.invalid, className: "mt-3", children: [_jsx(Field.Label, { children: "Selfie holding your ID" }), _jsx(SelfieImageField, { value: o.field.value, onChange: o.field.onChange, onError: (error) => {
|
|
112
|
+
form.setValue('selfieImage', '');
|
|
113
|
+
form.setError('selfieImage', {
|
|
114
|
+
type: 'validate',
|
|
115
|
+
message: error.message,
|
|
116
|
+
});
|
|
117
|
+
} }), _jsx(Field.ErrorText, { children: o.fieldState.error?.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: () => {
|
|
118
|
+
globalStore.kyc.setOpen(false);
|
|
119
|
+
}, children: "Skip for now" })] })] }));
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Indicator(): import("react/jsx-runtime").JSX.Element;
|