@opexa/portal-components 0.0.680 → 0.0.682

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 (67) hide show
  1. package/dist/client/hooks/useCamera.d.ts +2 -1
  2. package/dist/client/hooks/useCamera.js +128 -21
  3. package/dist/client/hooks/useSignOutMutation.js +15 -0
  4. package/dist/client/utils/biometric.js +1 -1
  5. package/dist/components/AccountInfo/GoogleDisconnect.d.ts +7 -0
  6. package/dist/components/AccountInfo/GoogleDisconnect.js +11 -0
  7. package/dist/components/DepositWithdrawal/AiOPaymentMethods.js +4 -4
  8. package/dist/components/DepositWithdrawal/PaymentMethods.js +4 -3
  9. package/dist/components/GameLaunch/GameLaunchTrigger.js +64 -11
  10. package/dist/components/KYC/KYCDefault/PersonalInformation.js +11 -1
  11. package/dist/components/KYC/KYCReminder.lazy.js +8 -10
  12. package/dist/components/KYC/KYCVerificationStatus.lazy.js +4 -7
  13. package/dist/components/KYC/KycOpenOnHomeMount.js +1 -0
  14. package/dist/components/PortalProvider/CXDTokenObserver.js +11 -11
  15. package/dist/components/RegisterBiometrics/RegisterBiometrics.js +3 -1
  16. package/dist/components/SignIn/MobileNumberSignIn.js +13 -1
  17. package/dist/components/SignIn/NameAndPasswordSignIn.js +23 -3
  18. package/dist/components/SignIn/SignInTrigger.d.ts +1 -1
  19. package/dist/components/SignIn/SignInTrigger.js +62 -7
  20. package/dist/components/TopWins/TopWins.client.js +1 -1
  21. package/dist/components/shared/IdFrontImageField/IdFrontImageField.client.js +12 -2
  22. package/dist/components/shared/SelfieImageField/SelfieImageField.client.js +11 -2
  23. package/dist/icons/LinkBrokenIcon.d.ts +2 -0
  24. package/dist/icons/LinkBrokenIcon.js +4 -0
  25. package/dist/ui/AlertDialog/AlertDialog.d.ts +88 -88
  26. package/dist/ui/AlertDialog/alertDialog.recipe.d.ts +8 -8
  27. package/dist/ui/Checkbox/Checkbox.d.ts +23 -23
  28. package/dist/ui/Checkbox/checkbox.recipe.d.ts +3 -3
  29. package/dist/ui/Combobox/Combobox.d.ts +42 -42
  30. package/dist/ui/Combobox/combobox.recipe.d.ts +3 -3
  31. package/dist/ui/DatePicker/DatePicker.d.ts +72 -72
  32. package/dist/ui/DatePicker/datePicker.recipe.d.ts +3 -3
  33. package/dist/ui/Dialog/Dialog.d.ts +33 -33
  34. package/dist/ui/Dialog/dialog.recipe.d.ts +3 -3
  35. package/dist/ui/Drawer/Drawer.d.ts +33 -33
  36. package/dist/ui/Drawer/drawer.recipe.d.ts +3 -3
  37. package/dist/ui/Menu/Menu.d.ts +252 -252
  38. package/dist/ui/Menu/menu.recipe.d.ts +14 -14
  39. package/dist/ui/Popover/Popover.d.ts +88 -88
  40. package/dist/ui/Popover/popover.recipe.d.ts +8 -8
  41. package/dist/ui/Select/Select.d.ts +45 -45
  42. package/dist/ui/Select/select.recipe.d.ts +3 -3
  43. package/dist/ui/Tooltip/Tooltip.d.ts +30 -30
  44. package/dist/ui/Tooltip/tooltip.recipe.d.ts +5 -5
  45. package/package.json +2 -1
  46. package/dist/components/Banner/Banner.client.d.ts +0 -12
  47. package/dist/components/Banner/Banner.client.js +0 -49
  48. package/dist/components/PortalProvider/AndroidOnlyComponents.d.ts +0 -1
  49. package/dist/components/PortalProvider/AndroidOnlyComponents.js +0 -12
  50. package/dist/components/SignIn/utils.d.ts +0 -8
  51. package/dist/components/SignIn/utils.js +0 -26
  52. package/dist/constants/Branches.d.ts +0 -2
  53. package/dist/constants/Branches.js +0 -42
  54. package/dist/third-parties/FacebookPixel/FacebookPixel.d.ts +0 -4
  55. package/dist/third-parties/FacebookPixel/FacebookPixel.js +0 -4
  56. package/dist/third-parties/FacebookPixel/api.d.ts +0 -0
  57. package/dist/third-parties/FacebookPixel/api.js +0 -1
  58. package/dist/third-parties/FacebookPixel/index.d.ts +0 -1
  59. package/dist/third-parties/FacebookPixel/index.js +0 -1
  60. package/dist/third-parties/GoogleRecaptcha/GoogleRecaptcha.d.ts +0 -4
  61. package/dist/third-parties/GoogleRecaptcha/GoogleRecaptcha.js +0 -4
  62. package/dist/third-parties/GoogleRecaptcha/api.d.ts +0 -0
  63. package/dist/third-parties/GoogleRecaptcha/api.js +0 -1
  64. package/dist/third-parties/GoogleRecaptcha/index.d.ts +0 -1
  65. package/dist/third-parties/GoogleRecaptcha/index.js +0 -1
  66. package/dist/third-parties/index.d.ts +0 -2
  67. package/dist/third-parties/index.js +0 -2
@@ -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,68 @@ export function useCamera(options = {}) {
78
79
  setLoading(false);
79
80
  }
80
81
  }, [options, desktop]);
82
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intentional empty deps: we need a stable openNativeCamera reference.
83
+ const openNativeCamera = useCallback(async () => {
84
+ setData(null);
85
+ setError(null);
86
+ setSnapping(true);
87
+ try {
88
+ // Dynamically import only when running native
89
+ const { Camera, CameraResultType } = await import('@capacitor/camera');
90
+ const photo = await Camera.getPhoto({
91
+ quality: 90,
92
+ resultType: CameraResultType.Uri,
93
+ saveToGallery: false,
94
+ allowEditing: false,
95
+ });
96
+ if (!photo.webPath) {
97
+ throw new Error('No photo returned from native camera');
98
+ }
99
+ console.log(photo, 'photo');
100
+ const response = await fetch(photo.webPath);
101
+ const blob = await response.blob();
102
+ const file = new File([blob], `${crypto.randomUUID()}.jpeg`, {
103
+ type: blob.type,
104
+ lastModified: Date.now(),
105
+ });
106
+ // Convert blob → base64 data URL
107
+ const url = await new Promise((resolve, reject) => {
108
+ const reader = new FileReader();
109
+ reader.onloadend = () => {
110
+ if (typeof reader.result === 'string')
111
+ resolve(reader.result);
112
+ else
113
+ reject(new Error('Failed to read image as data URL'));
114
+ };
115
+ reader.onerror = reject;
116
+ reader.readAsDataURL(blob);
117
+ });
118
+ const image = new Image();
119
+ image.src = url;
120
+ if (!image.complete || image.naturalWidth === 0) {
121
+ await new Promise((resolve, reject) => {
122
+ image.onload = () => resolve();
123
+ image.onerror = () => reject(new Error('Failed to load preview image'));
124
+ });
125
+ }
126
+ const data = {
127
+ url,
128
+ file,
129
+ image,
130
+ };
131
+ setData(data);
132
+ setSnapping(false);
133
+ return data;
134
+ }
135
+ catch (e) {
136
+ setError({
137
+ name: 'CameraError',
138
+ message: e instanceof Error ? e.message : 'Failed to open native camera',
139
+ });
140
+ setSnapping(false);
141
+ return null;
142
+ }
143
+ }, [setData, setError, setSnapping]);
81
144
  const close = useCallback(() => {
82
145
  setData(null);
83
146
  setError(null);
@@ -89,33 +152,76 @@ export function useCamera(options = {}) {
89
152
  resolve();
90
153
  });
91
154
  }, []);
92
- const snap = useCallback(() => {
155
+ const snap = useCallback(async () => {
156
+ setData(null);
157
+ setError(null);
158
+ setSnapping(true);
93
159
  const video = videoRef.current;
94
- if (!video)
95
- return null;
96
160
  const canvas = document.createElement('canvas');
97
161
  const context = canvas.getContext('2d');
98
- if (!context)
99
- return null;
162
+ invariant(video, 'Could not find video element');
163
+ invariant(context, 'Could not get canvas context');
164
+ video.currentTime = 1;
100
165
  canvas.width = video.videoWidth;
101
166
  canvas.height = video.videoHeight;
102
- context.drawImage(video, 0, 0, canvas.width, canvas.height);
103
- const url = canvas.toDataURL('image/jpeg', 0.9);
104
- const arr = atob(url.split(',')[1]);
105
- const u8arr = new Uint8Array(arr.length);
106
- for (let i = 0; i < arr.length; i++)
107
- u8arr[i] = arr.charCodeAt(i);
108
- const file = new File([u8arr], `${crypto.randomUUID()}.jpeg`, {
109
- type: 'image/jpeg',
167
+ context.imageSmoothingEnabled = true;
168
+ context.imageSmoothingQuality = 'high';
169
+ context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
170
+ return new Promise((resolve) => {
171
+ canvas.toBlob(async (blob) => {
172
+ if (!blob) {
173
+ setSnapping(false);
174
+ resolve(null);
175
+ return setError({
176
+ name: 'CameraError',
177
+ message: "'canvas.toBlob' failed to create blob",
178
+ });
179
+ }
180
+ const url = canvas.toDataURL('image/jpeg', 1);
181
+ const file = new File([blob], `${crypto.randomUUID()}.jpeg`, {
182
+ type: 'image/jpeg',
183
+ endings: 'native',
184
+ lastModified: Date.now(),
185
+ });
186
+ const image = new Image();
187
+ image.src = url;
188
+ image.alt = '';
189
+ image.width = canvas.width;
190
+ image.height = canvas.height;
191
+ if (!image.complete || image.naturalWidth === 0) {
192
+ await new Promise((resolve, reject) => {
193
+ image.onload = () => resolve();
194
+ image.onerror = () => reject();
195
+ });
196
+ }
197
+ const data = {
198
+ url,
199
+ file,
200
+ image,
201
+ };
202
+ if (!options.transform) {
203
+ setData(data);
204
+ setSnapping(false);
205
+ resolve(data);
206
+ return;
207
+ }
208
+ const transformResult = await options.transform({
209
+ ...data,
210
+ video,
211
+ canvas,
212
+ });
213
+ if (transformResult.ok) {
214
+ setData(transformResult.data);
215
+ resolve(transformResult.data);
216
+ }
217
+ else {
218
+ setError(transformResult.error);
219
+ resolve(null);
220
+ }
221
+ setSnapping(false);
222
+ }, 'image/jpeg', 1);
110
223
  });
111
- const image = new Image();
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
- }, []);
224
+ }, [options]);
119
225
  const reset = useCallback(() => {
120
226
  setData(null);
121
227
  setError(null);
@@ -157,6 +263,7 @@ export function useCamera(options = {}) {
157
263
  return {
158
264
  snap,
159
265
  open,
266
+ openNativeCamera,
160
267
  close,
161
268
  reopen,
162
269
  reset,
@@ -1,7 +1,10 @@
1
1
  import { useMutation } from '@tanstack/react-query';
2
+ import invariant from 'tiny-invariant';
3
+ import { unregisterFCMDevice } from '../../services/trigger.js';
2
4
  import { getQueryClient } from '../../utils/getQueryClient.js';
3
5
  import { getSignOutMutationKey } from '../../utils/mutationKeys.js';
4
6
  import { getSessionQueryKey } from '../../utils/queryKeys.js';
7
+ import { getSession } from '../services/getSession.js';
5
8
  import { signOut } from '../services/signOut.js';
6
9
  const IDLE_TIMESTAMP_KEY = 'idle-logout-timestamp';
7
10
  export const useSignOutMutation = (config) => {
@@ -10,6 +13,18 @@ export const useSignOutMutation = (config) => {
10
13
  ...config,
11
14
  mutationKey: getSignOutMutationKey(),
12
15
  mutationFn: async () => {
16
+ const session = await getQueryClient().fetchQuery({
17
+ queryKey: getSessionQueryKey(),
18
+ queryFn: async () => getSession(),
19
+ });
20
+ invariant(session.status === 'authenticated');
21
+ await unregisterFCMDevice({
22
+ type: ['IOS', 'ANDROID'],
23
+ }, {
24
+ headers: {
25
+ Authorization: `Bearer ${session.token}`,
26
+ },
27
+ });
13
28
  await signOut();
14
29
  await queryClient.invalidateQueries({ queryKey: getSessionQueryKey() });
15
30
  queryClient.removeQueries();
@@ -1,5 +1,5 @@
1
1
  import { addDays, isAfter } from 'date-fns';
2
- const SERVER = `com.${process.env.NEXT_PUBLIC_PLATFORM_CODE}.app`;
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,7 @@
1
+ import { type UseDisclosureReturn } from '../../client/hooks/useDisclosure';
2
+ interface GoogleDisconnectProps {
3
+ onConfirmAction?: (ctx: UseDisclosureReturn) => React.ReactNode;
4
+ children?: (ctx: UseDisclosureReturn) => React.ReactNode;
5
+ }
6
+ export declare function GoogleDisconnect(props: GoogleDisconnectProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useDisclosure, } from '../../client/hooks/useDisclosure.js';
4
+ import { LinkBrokenIcon } from '../../icons/LinkBrokenIcon.js';
5
+ import { XIcon } from '../../icons/XIcon.js';
6
+ import { Dialog } from '../../ui/Dialog/index.js';
7
+ import { Portal } from '../../ui/Portal/index.js';
8
+ export function GoogleDisconnect(props) {
9
+ const disclosure = useDisclosure();
10
+ return (_jsxs(_Fragment, { children: [props.children?.(disclosure), _jsx(Dialog.Root, { lazyMount: true, unmountOnExit: true, open: disclosure.open, onOpenChange: (details) => disclosure.setOpen(details.open), closeOnEscape: false, closeOnInteractOutside: false, 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", children: _jsxs(Dialog.Content, { className: "mx-auto min-h-auto max-w-[25rem] overflow-y-auto rounded-xl p-6", children: [_jsx(Dialog.CloseTrigger, { children: _jsx(XIcon, {}) }), _jsxs("div", { className: "flex flex-col ", children: [_jsx("div", { className: "mx-auto flex size-12 items-center justify-center rounded-full bg-bg-brand-secondary text-text-brand", children: _jsx(LinkBrokenIcon, {}) }), _jsx("h2", { className: "mb-1 text-center font-semibold text-lg xl:mt-xl", children: "Disconnect Google Account" }), _jsx("p", { className: "text-center text-sm text-text-tertiary-600 leading-2xl", children: "Are you sure you want to disconnect your Google account? This may affect your ability to log in or sync data." })] }), _jsx("div", { className: "pt-6", children: props.onConfirmAction?.(disclosure) })] }) })] }) })] }));
11
+ }
@@ -4,12 +4,12 @@ import { twMerge } from 'tailwind-merge';
4
4
  import { useControllableState } from '../../client/hooks/useControllableState.js';
5
5
  import { CheckIcon } from '../../icons/CheckIcon.js';
6
6
  import gcash from '../../images/gcash.png';
7
- import maya from '../../images/maya.png';
8
7
  import grabPay from '../../images/grabpay.png';
8
+ import maya from '../../images/maya.png';
9
9
  import palawanPay from '../../images/palawanpay.png';
10
10
  import { Checkbox } from '../../ui/Checkbox/index.js';
11
11
  import { Field } from '../../ui/Field/index.js';
12
- import { AiOeWalletPaymentMethodDefinition } from './utils.js';
12
+ import { AiOeWalletPaymentMethodDefinition, } from './utils.js';
13
13
  const AIO_EWALLET_OPTIONS = [
14
14
  {
15
15
  value: 'AIO_GCASH',
@@ -50,6 +50,6 @@ export function AiOPaymentMethods(props) {
50
50
  return;
51
51
  setValue(parseValue(lastValue));
52
52
  }, className: "grid grid-cols-2 gap-x-4 gap-y-3", children: options.map((option) => (_jsxs(Checkbox.Root, { value: option.value, className: "flex cursor-pointer items-center justify-between rounded-xl border border-border-secondary ui-checked:border-border-brand-solid p-lg", children: [_jsx("div", { className: twMerge('rounded-xs', option.value === 'AIO_GRAB_PAY'
53
- ? 'bg-transparent py-0 px-0'
54
- : 'bg-white py-[0.688rem] px-sm', option.value === 'AIO_GCASH' && 'bg-[#017EFF]', option.value === 'AIO_PALAWAN_PAY' && 'bg-[#026308]', option.value === 'AIO_PAY_MAYA' && 'bg-black'), children: _jsx(Image, { src: option.image, alt: "", width: 200, height: 40, className: twMerge('w-auto h-[1.063rem]', option.value === 'AIO_GRAB_PAY' && 'h-[3rem] rounded-[4px]', option.value === 'AIO_PALAWAN_PAY' && 'h-[2rem]'), draggable: false }) }), _jsx(Checkbox.Control, { className: "shrink-0", children: _jsx(Checkbox.Indicator, { asChild: true, children: _jsx(CheckIcon, {}) }) }), _jsx(Checkbox.HiddenInput, {})] }, option.value))) })] }));
53
+ ? 'bg-transparent px-0 py-0'
54
+ : 'bg-white px-sm py-[0.688rem]', option.value === 'AIO_GCASH' && 'bg-[#017EFF]', option.value === 'AIO_PALAWAN_PAY' && 'bg-[#026308]', option.value === 'AIO_PAY_MAYA' && 'bg-black'), children: _jsx(Image, { src: option.image, alt: "", width: 200, height: 40, className: twMerge('h-[1.063rem] w-auto', option.value === 'AIO_GRAB_PAY' && 'h-[3rem] rounded-[4px]', option.value === 'AIO_PALAWAN_PAY' && 'h-[2rem]'), draggable: false }) }), _jsx(Checkbox.Control, { className: "shrink-0", children: _jsx(Checkbox.Indicator, { asChild: true, children: _jsx(CheckIcon, {}) }) }), _jsx(Checkbox.HiddenInput, {})] }, option.value))) })] }));
55
55
  }
@@ -3,13 +3,13 @@ import Image from 'next/image';
3
3
  import { twMerge } from 'tailwind-merge';
4
4
  import { useControllableState } from '../../client/hooks/useControllableState.js';
5
5
  import { CheckIcon } from '../../icons/CheckIcon.js';
6
- import qrph from '../../images/QRPH.png';
7
6
  import gcash from '../../images/gcash.png';
8
7
  import instapay from '../../images/instapay.png';
9
8
  import libangan from '../../images/libangan.png';
10
9
  import maya from '../../images/maya.png';
11
10
  import onlineBank from '../../images/online-bank.png';
12
11
  import pisoPay from '../../images/piso-pay.png';
12
+ import qrph from '../../images/QRPH.png';
13
13
  import wallet from '../../images/wallet.png';
14
14
  import { Checkbox } from '../../ui/Checkbox/index.js';
15
15
  import { Field } from '../../ui/Field/index.js';
@@ -80,8 +80,9 @@ export function PaymentMethods(props) {
80
80
  if (!lastValue)
81
81
  return;
82
82
  setValue(PaymentMethodDefinition.parse(lastValue));
83
- }, className: "grid grid-cols-2 gap-x-4 gap-y-3", children: options.map((option) => (_jsxs(Checkbox.Root, { value: option.value, className: "flex cursor-pointer items-center justify-between rounded-xl border border-border-secondary ui-checked:border-border-brand-solid p-lg", children: [_jsxs("div", { className: twMerge('rounded-xs bg-white px-sm py-[0.688rem]', option.value === 'GCASH' && 'bg-[#017EFF]', option.value === 'AIO_EWALLET' && 'bg-bg-secondary flex items-center space-x-sm'), children: [_jsx(Image, { src: option.image, alt: "", width: 200, height: 40, className: twMerge('w-auto', option.value === 'LIBANGAN_PAY_IN' ||
83
+ }, className: "grid grid-cols-2 gap-x-4 gap-y-3", children: options.map((option) => (_jsxs(Checkbox.Root, { value: option.value, className: "flex cursor-pointer items-center justify-between rounded-xl border border-border-secondary ui-checked:border-border-brand-solid p-lg", children: [_jsxs("div", { className: twMerge('rounded-xs bg-white px-sm py-[0.688rem]', option.value === 'GCASH' && 'bg-[#017EFF]', option.value === 'AIO_EWALLET' &&
84
+ 'flex items-center space-x-sm bg-bg-secondary'), children: [_jsx(Image, { src: option.image, alt: "", width: 200, height: 40, className: twMerge('w-auto', option.value === 'LIBANGAN_PAY_IN' ||
84
85
  option.value === 'VENTAJA_DISBURSEMENT'
85
86
  ? 'h-[2.5rem]'
86
- : 'h-[1.063rem]'), draggable: false }), option.value === 'AIO_EWALLET' && _jsx("p", { className: 'text-text-secondary-700 text-xs leading-tight', children: "AIO eWallet" })] }), _jsx(Checkbox.Control, { className: "shrink-0", children: _jsx(Checkbox.Indicator, { asChild: true, children: _jsx(CheckIcon, {}) }) }), _jsx(Checkbox.HiddenInput, {})] }, option.value))) })] }));
87
+ : 'h-[1.063rem]'), draggable: false }), option.value === 'AIO_EWALLET' && (_jsx("p", { className: "text-text-secondary-700 text-xs leading-tight", children: "AIO eWallet" }))] }), _jsx(Checkbox.Control, { className: "shrink-0", children: _jsx(Checkbox.Indicator, { asChild: true, children: _jsx(CheckIcon, {}) }) }), _jsx(Checkbox.HiddenInput, {})] }, option.value))) })] }));
87
88
  }
@@ -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
- await signIn({
75
- type: 'SINGLE_USE_TOKEN',
76
- token: credentials.password,
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 {
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
- globalStore.signIn.setOpen(true);
109
- return;
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' ||
@@ -10,6 +10,7 @@ import { useGlobalStore } from '../../../client/hooks/useGlobalStore.js';
10
10
  import { useMemberVerificationQuery } from '../../../client/hooks/useMemberVerificationQuery.js';
11
11
  import { useSignOutMutation } from '../../../client/hooks/useSignOutMutation.js';
12
12
  import { useUpdateMemberVerificationMutation } from '../../../client/hooks/useUpdateMemberVerificationMutation.js';
13
+ import { BIOMETRIC_STORAGE_KEY } from '../../../client/utils/biometric.js';
13
14
  import { toaster } from '../../../client/utils/toaster.js';
14
15
  import { CheckIcon } from '../../../icons/CheckIcon.js';
15
16
  import { Button } from '../../../ui/Button/index.js';
@@ -38,7 +39,16 @@ export function PersonalInformation() {
38
39
  const router = useRouter();
39
40
  const signOutMutation = useSignOutMutation({
40
41
  onSuccess() {
41
- localStorage.clear();
42
+ const keep = new Set([BIOMETRIC_STORAGE_KEY]);
43
+ for (let i = 0; i < localStorage.length;) {
44
+ const key = localStorage.key(i);
45
+ if (key && !keep.has(key)) {
46
+ localStorage.removeItem(key);
47
+ }
48
+ else {
49
+ i++;
50
+ }
51
+ }
42
52
  sessionStorage.clear();
43
53
  router.replace('/');
44
54
  },
@@ -28,16 +28,14 @@ export function KYCReminder(props) {
28
28
  const signOutMutation = useSignOutMutation({
29
29
  onSuccess() {
30
30
  // Clear everything except the 'biometric' entry
31
- {
32
- const keep = new Set([BIOMETRIC_STORAGE_KEY]);
33
- for (let i = 0; i < localStorage.length;) {
34
- const key = localStorage.key(i);
35
- if (key && !keep.has(key)) {
36
- localStorage.removeItem(key);
37
- }
38
- else {
39
- i++;
40
- }
31
+ const keep = new Set([BIOMETRIC_STORAGE_KEY]);
32
+ for (let i = 0; i < localStorage.length;) {
33
+ const key = localStorage.key(i);
34
+ if (key && !keep.has(key)) {
35
+ localStorage.removeItem(key);
36
+ }
37
+ else {
38
+ i++;
41
39
  }
42
40
  }
43
41
  sessionStorage.clear();
@@ -19,18 +19,15 @@ export function KYCVerificationStatus() {
19
19
  const icons = status === 'PENDING' ? bellIcon : alertIcon;
20
20
  return (_jsx(Dialog.Root, { open: globalStore.kycVerificationStatus.open, onOpenChange: (details) => {
21
21
  globalStore.kycVerificationStatus.setOpen(details.open);
22
- }, closeOnEscape: false, closeOnInteractOutside: false, lazyMount: true, unmountOnExit: true, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+3)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+4)] flex items-center justify-center", children: _jsx(Dialog.Content, { className: "mx-auto h-fit w-[450px] overflow-y-auto rounded-lg bg-bg-primary", children: _jsxs("div", { className: "p-3xl text-center", children: [_jsx("div", { className: "mb-3xl grid h-[200px] w-full place-items-center rounded-xl bg-radial from-40% from-button-primary-bg to-bg-brand-solid", children: _jsx(Image, { src: icons, alt: "icon", className: "w-60 object-contain", draggable: false, width: 120, height: 120 }) }), _jsxs("h1", { className: "font-semibold text-lg text-white", children: [status === 'PENDING' && 'Verification in Progress', status === 'REJECTED' && 'Verification Rejected', status === 'UNVERIFIED' ||
23
- (status === 'CREATED' && 'Verification Required')] }), _jsxs("p", { className: "mb-4xl text-[#94969C] text-base", children: [status === 'PENDING' &&
22
+ }, closeOnEscape: false, closeOnInteractOutside: false, lazyMount: true, unmountOnExit: true, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+3)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+4)] flex items-center justify-center", children: _jsx(Dialog.Content, { className: "mx-auto h-fit w-[450px] overflow-y-auto rounded-lg bg-bg-primary", children: _jsxs("div", { className: "p-3xl text-center", children: [_jsx("div", { className: "mb-3xl grid h-[200px] w-full place-items-center rounded-xl bg-radial from-40% from-button-primary-bg to-bg-brand-solid", children: _jsx(Image, { src: icons, alt: "icon", className: "w-60 object-contain", draggable: false, width: 120, height: 120 }) }), _jsxs("h1", { className: "font-semibold text-lg text-white", children: [status === 'PENDING' && 'Verification in Progress', status === 'REJECTED' && 'Verification Rejected', status === 'UNVERIFIED' && 'Verification Required'] }), _jsxs("p", { className: "mb-4xl text-[#94969C] text-base", children: [status === 'PENDING' &&
24
23
  `Your account verification is still under review. Please wait
25
24
  until it's approved before you can continue playing or
26
25
  depositing.`, status === 'REJECTED' &&
27
- 'Your account verification was not approved. Please resubmit your verification to regain full access.', status === 'UNVERIFIED' ||
28
- (status === 'CREATED' &&
29
- 'Your account is not yet verified. Please complete the verification process to continue playing or depositing.')] }), _jsxs(Button, { variant: "solid", className: twMerge('mb-2 w-full', status === 'PENDING' && 'hidden'), onClick: () => {
26
+ 'Your account verification was not approved. Please resubmit your verification to regain full access.', status === 'UNVERIFIED' &&
27
+ 'Your account is not yet verified. Please complete the verification process to continue playing or depositing.'] }), _jsxs(Button, { variant: "solid", className: twMerge('mb-2 w-full', status === 'PENDING' && 'hidden'), onClick: () => {
30
28
  globalStore.kycVerificationStatus.setOpen(false);
31
29
  globalStore.kyc.setOpen(true);
32
- }, children: [status === 'REJECTED' && 'Resubmit Verification', status === 'UNVERIFIED' ||
33
- (status === 'CREATED' && 'Verify Now')] }), _jsx(Button, { type: "button", variant: "outline", onClick: () => {
30
+ }, children: [status === 'REJECTED' && 'Resubmit Verification', status === 'UNVERIFIED' && 'Verify Now'] }), _jsx(Button, { type: "button", variant: "outline", onClick: () => {
34
31
  globalStore.kycVerificationStatus.setOpen(false);
35
32
  }, children: "Close" })] }) }) })] }) }));
36
33
  }
@@ -33,6 +33,7 @@ export function KycOpenOnHomeMount(props) {
33
33
  !verification?.placeOfBirth ||
34
34
  !verification?.address;
35
35
  useEffect(() => {
36
+ console.log(hasntSubmittedCompliantDocs, hasntCompletedKYC);
36
37
  if (!verificationLoading && !accountLoading) {
37
38
  // Handle pending case with feature flag
38
39
  if (isPending) {
@@ -1,30 +1,30 @@
1
1
  'use client';
2
2
  import { addHours } from 'date-fns';
3
3
  import { clamp } from 'lodash-es';
4
+ import { useSearchParams } from 'next/navigation';
4
5
  import { useLocalStorage, useTimeout } from 'usehooks-ts';
5
6
  import { useAccountQuery } from '../../client/hooks/useAccountQuery.js';
6
7
  export function CXDTokenObserver() {
7
- const { data: account } = useAccountQuery();
8
- const accountCxd = {
9
- cxd: account?.cellxpertDetails?.cxd,
10
- };
11
- const [cxd, setCxd, removeCxd] = useLocalStorage('cxd', null);
8
+ const searchParams = useSearchParams();
9
+ const cxdToken = searchParams.get('cxd');
10
+ const accountQuery = useAccountQuery();
11
+ const account = accountQuery.data;
12
+ const [cxd, setCxd, removeCxd] = useLocalStorage('WebPortalCellxpertCxd', null);
12
13
  const now = new Date();
14
+ const shouldTimeoutRun = cxdToken && account;
13
15
  const removeCxdUntilInMs = cxd?.timestamp
14
16
  ? clamp(cxd.timestamp - now.getTime(), 0, Infinity)
15
17
  : 0;
16
18
  useTimeout(() => {
17
- const isSame = cxd?.cxd === accountCxd.cxd;
19
+ const isSame = cxd?.cxd === cxdToken;
18
20
  if (!isSame) {
19
21
  const extendedTimestamp = addHours(new Date(), 6).getTime();
20
22
  setCxd({
21
- cxd: accountCxd.cxd,
23
+ cxd: cxdToken,
22
24
  timestamp: extendedTimestamp,
23
25
  });
24
26
  }
25
- }, account ? 100 : null);
26
- useTimeout(() => {
27
- removeCxd();
28
- }, account ? removeCxdUntilInMs : null);
27
+ }, shouldTimeoutRun ? 100 : null);
28
+ useTimeout(() => removeCxd(), shouldTimeoutRun ? removeCxdUntilInMs : null);
29
29
  return null;
30
30
  }
@@ -45,7 +45,9 @@ export function RegisterBiometrics() {
45
45
  const info = await getBiometricInfo();
46
46
  if (info.errorCode === BiometricAuthError.APP_CANCEL ||
47
47
  info.errorCode === BiometricAuthError.USER_CANCEL ||
48
- info.errorCode === BiometricAuthError.SYSTEM_CANCEL) {
48
+ info.errorCode === BiometricAuthError.SYSTEM_CANCEL ||
49
+ info.errorCode === undefined ||
50
+ info.errorCode === null) {
49
51
  return;
50
52
  }
51
53
  else {
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation';
6
6
  import { useRef } from 'react';
7
7
  import { Controller, useForm } from 'react-hook-form';
8
8
  import { twMerge } from 'tailwind-merge';
9
+ import invariant from 'tiny-invariant';
9
10
  import { z } from 'zod';
10
11
  import { useShallow } from 'zustand/shallow';
11
12
  import { useCooldown } from '../../client/hooks/useCooldown.js';
@@ -14,12 +15,14 @@ import { useLocaleInfo } from '../../client/hooks/useLocaleInfo.js';
14
15
  import { useMobileNumberParser } from '../../client/hooks/useMobileNumberParser.js';
15
16
  import { useSendVerificationCodeMutation } from '../../client/hooks/useSendVerificationCodeMutation.js';
16
17
  import { useSignInMutation } from '../../client/hooks/useSignInMutation.js';
18
+ import { getSession } from '../../client/services/getSession.js';
17
19
  import { hasSavedBiometry } from '../../client/utils/biometric.js';
18
20
  import { toaster } from '../../client/utils/toaster.js';
19
21
  import { ArrowLeftIcon } from '../../icons/ArrowLeftIcon.js';
20
22
  import { CheckIcon } from '../../icons/CheckIcon.js';
21
23
  import pagcorLogo from '../../images/pagcor-round-icon.png';
22
24
  import responsibleGamingLogo from '../../images/responsible-gaming-gold.png';
25
+ import { unregisterFCMDevice } from '../../services/trigger.js';
23
26
  import { Button } from '../../ui/Button/index.js';
24
27
  import { Checkbox } from '../../ui/Checkbox/index.js';
25
28
  import { Field } from '../../ui/Field/index.js';
@@ -45,11 +48,20 @@ export function MobileNumberSignIn() {
45
48
  registerBiometrics: ctx.registerBiometrics,
46
49
  })));
47
50
  const signInMutation = useSignInMutation({
48
- onSuccess: () => {
51
+ onSuccess: async () => {
49
52
  step1Form.reset();
50
53
  step2Form.reset();
51
54
  context.setStep(1);
52
55
  globalStore.signIn.setOpen(false);
56
+ const session = await getSession();
57
+ invariant(session.status === 'authenticated');
58
+ await unregisterFCMDevice({
59
+ type: ['IOS', 'ANDROID'],
60
+ }, {
61
+ headers: {
62
+ Authorization: `Bearer ${session.token}`,
63
+ },
64
+ });
53
65
  if (signInProps.shouldShowResponsibleGamingReminder) {
54
66
  globalStore.responsibleGamingReminder.setOpen(true);
55
67
  }