@opexa/portal-components 0.0.680 → 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/GameLaunch/GameLaunchTrigger.js +64 -11
- package/dist/components/KYC/KYCDefault/PersonalInformation.js +11 -1
- package/dist/components/KYC/KYCReminder.lazy.js +8 -10
- package/dist/components/KYC/KycOpenOnHomeMount.js +1 -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/shared/IdFrontImageField/IdFrontImageField.client.js +12 -2
- package/dist/components/shared/SelfieImageField/SelfieImageField.client.js +11 -2
- 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) {
|
|
@@ -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' ||
|
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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();
|
|
@@ -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) {
|
|
@@ -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 {
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { ark } from '@ark-ui/react/factory';
|
|
4
|
+
import { useState } from 'react';
|
|
4
5
|
import { useShallow } from 'zustand/shallow';
|
|
5
6
|
import { useGlobalStore } from '../../client/hooks/useGlobalStore.js';
|
|
6
7
|
import { getSession } from '../../client/services/getSession.js';
|
|
7
8
|
import { signIn } from '../../client/services/signIn.js';
|
|
8
|
-
import { BiometricAuthError, getBiometricCredentials, getBiometricInfo, hasSavedBiometry, performBiometricVerification, saveBiometricCredentials, } from '../../client/utils/biometric.js';
|
|
9
|
+
import { BiometricAuthError, deleteBiometricCredentials, getBiometricCredentials, getBiometricInfo, hasSavedBiometry, performBiometricVerification, saveBiometricCredentials, } from '../../client/utils/biometric.js';
|
|
9
10
|
import { toaster } from '../../client/utils/toaster.js';
|
|
10
11
|
import { createSingleUseToken } from '../../services/auth.js';
|
|
11
12
|
import { getQueryClient } from '../../utils/getQueryClient.js';
|
|
12
13
|
import { getSessionQueryKey } from '../../utils/queryKeys.js';
|
|
13
14
|
export function SignInTrigger(props) {
|
|
15
|
+
const [hasCancelledBiometric, setHasCancelledBiometric] = useState(false);
|
|
14
16
|
const globalStore = useGlobalStore(useShallow((ctx) => ({
|
|
15
17
|
signIn: ctx.signIn,
|
|
16
18
|
registerBiometrics: ctx.registerBiometrics,
|
|
17
19
|
})));
|
|
18
20
|
return (_jsx(ark.button, { type: "button", "aria-label": "Sign in", "data-state": globalStore.signIn.open ? 'open' : 'closed', ...props, onClick: async (e) => {
|
|
19
|
-
if (hasSavedBiometry()) {
|
|
21
|
+
if (hasSavedBiometry() && !hasCancelledBiometric) {
|
|
20
22
|
const ok = await performBiometricVerification({
|
|
21
23
|
reason: 'Login to your account',
|
|
22
24
|
title: 'Login',
|
|
@@ -28,7 +30,10 @@ export function SignInTrigger(props) {
|
|
|
28
30
|
const info = await getBiometricInfo();
|
|
29
31
|
if (info.errorCode === BiometricAuthError.APP_CANCEL ||
|
|
30
32
|
info.errorCode === BiometricAuthError.USER_CANCEL ||
|
|
31
|
-
info.errorCode === BiometricAuthError.SYSTEM_CANCEL
|
|
33
|
+
info.errorCode === BiometricAuthError.SYSTEM_CANCEL ||
|
|
34
|
+
info.errorCode === undefined ||
|
|
35
|
+
info.errorCode === null) {
|
|
36
|
+
setHasCancelledBiometric(true);
|
|
32
37
|
console.log('Biometric verification cancelled');
|
|
33
38
|
}
|
|
34
39
|
else {
|
|
@@ -43,10 +48,21 @@ export function SignInTrigger(props) {
|
|
|
43
48
|
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
44
49
|
return;
|
|
45
50
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
try {
|
|
52
|
+
await signIn({
|
|
53
|
+
type: 'SINGLE_USE_TOKEN',
|
|
54
|
+
token: credentials.password,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.log(err);
|
|
59
|
+
toaster.error({
|
|
60
|
+
title: 'Biometric removed or token expired.',
|
|
61
|
+
description: 'Please sign in with your mobile number or username to re-enable biometric login.',
|
|
62
|
+
});
|
|
63
|
+
deleteBiometricCredentials();
|
|
64
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
65
|
+
}
|
|
50
66
|
getQueryClient().invalidateQueries({
|
|
51
67
|
queryKey: getSessionQueryKey(),
|
|
52
68
|
});
|
|
@@ -75,6 +91,45 @@ export function SignInTrigger(props) {
|
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
else {
|
|
94
|
+
// still update biometric credentials if user has cancelled biometric once
|
|
95
|
+
if (hasCancelledBiometric) {
|
|
96
|
+
const credentials = await getBiometricCredentials();
|
|
97
|
+
if (!credentials) {
|
|
98
|
+
toaster.error({ description: 'Biometric verification failed' });
|
|
99
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await signIn({
|
|
103
|
+
type: 'SINGLE_USE_TOKEN',
|
|
104
|
+
token: credentials.password,
|
|
105
|
+
});
|
|
106
|
+
getQueryClient().invalidateQueries({
|
|
107
|
+
queryKey: getSessionQueryKey(),
|
|
108
|
+
});
|
|
109
|
+
const session = await getSession();
|
|
110
|
+
const r = await createSingleUseToken({
|
|
111
|
+
headers: {
|
|
112
|
+
Authorization: `Bearer ${session.token}`,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (r.token) {
|
|
116
|
+
const saved = await saveBiometricCredentials({
|
|
117
|
+
username: credentials.username,
|
|
118
|
+
password: r.token,
|
|
119
|
+
});
|
|
120
|
+
if (saved) {
|
|
121
|
+
console.info('Biometric credentials has been updated');
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.warn('Failed to updated biometric credentials');
|
|
125
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.error('Failed to create token');
|
|
130
|
+
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
78
133
|
globalStore.signIn.setOpen(!globalStore.signIn.open);
|
|
79
134
|
}
|
|
80
135
|
return props.onClick?.(e);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Capacitor } from '@capacitor/core';
|
|
2
3
|
import Image from 'next/image';
|
|
3
4
|
import { useRef } from 'react';
|
|
4
5
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -52,8 +53,16 @@ export function IdFrontImageField__client(props) {
|
|
|
52
53
|
context.query.isLoading ||
|
|
53
54
|
context.mutation.isPending ||
|
|
54
55
|
localProps.disabled ||
|
|
55
|
-
localProps.readOnly, className: "font-semibold text-button-secondary-fg 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
|
-
|
|
56
|
+
localProps.readOnly, className: "font-semibold text-button-secondary-fg 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: async () => {
|
|
57
|
+
if (Capacitor.isNativePlatform()) {
|
|
58
|
+
const data = await context.camera.openNativeCamera();
|
|
59
|
+
if (!data?.file)
|
|
60
|
+
return;
|
|
61
|
+
context.mutation.mutate({ file: data.file });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
context.disclosure.setOpen(true);
|
|
65
|
+
}
|
|
57
66
|
}, disabled: context.field?.disabled ||
|
|
58
67
|
context.field?.readOnly ||
|
|
59
68
|
context.query.isLoading ||
|
|
@@ -70,6 +79,7 @@ export function IdFrontImageField__client(props) {
|
|
|
70
79
|
*/
|
|
71
80
|
function Camera() {
|
|
72
81
|
const context = useIdFrontImageFieldContext();
|
|
82
|
+
console.log(context.camera, 'context.camera');
|
|
73
83
|
return (_jsx(Dialog.Root, { open: context.disclosure.open, onOpenChange: (details) => {
|
|
74
84
|
context.disclosure.setOpen(details.open);
|
|
75
85
|
}, closeOnEscape: false, closeOnInteractOutside: false, onExitComplete: context.camera.close, children: _jsxs(Portal, { children: [_jsx(Dialog.Backdrop, { className: "!z-[calc(var(--z-dialog)+1)]" }), _jsx(Dialog.Positioner, { className: "!z-[calc(var(--z-dialog)+2)] flex items-center justify-center overflow-y-auto py-4", children: _jsxs(Dialog.Content, { className: "mx-auto w-[calc(100dvw-1rem)] max-w-[calc(100dvw-1rem)] overflow-y-auto rounded-lg bg-bg-primary-alt 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: "mt-md text-center text-text-tertiary-600 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: [_jsx(Video, {}), context.camera.error && (_jsxs("div", { className: "flex aspect-[4/3] flex-col items-center justify-center rounded-md border border-border-disabled bg-black px-4 lg:aspect-video", children: [_jsx(CameraOffIcon, { className: "size-10 text-center text-text-placeholder-subtle lg:size-12" }), _jsx("h2", { className: "mt-3 font-semibold text-sm lg:mt-4 lg:text-base", children: context.camera.error.name }), _jsx("p", { className: "mt-0.5 text-center text-text-tertiary-600 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: () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Capacitor } from '@capacitor/core';
|
|
2
3
|
import Image from 'next/image';
|
|
3
4
|
import { useRef } from 'react';
|
|
4
5
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -52,8 +53,16 @@ export function SelfieImageField__client(props) {
|
|
|
52
53
|
context.query.isLoading ||
|
|
53
54
|
context.mutation.isPending ||
|
|
54
55
|
localProps.disabled ||
|
|
55
|
-
localProps.readOnly, className: "font-semibold text-button-secondary-fg 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
|
-
|
|
56
|
+
localProps.readOnly, className: "font-semibold text-button-secondary-fg 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: async () => {
|
|
57
|
+
if (Capacitor.isNativePlatform()) {
|
|
58
|
+
const data = await context.camera.openNativeCamera();
|
|
59
|
+
if (!data?.file)
|
|
60
|
+
return;
|
|
61
|
+
context.mutation.mutate({ file: data.file });
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
context.disclosure.setOpen(true);
|
|
65
|
+
}
|
|
57
66
|
}, disabled: context.field?.disabled ||
|
|
58
67
|
context.field?.readOnly ||
|
|
59
68
|
context.query.isLoading ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opexa/portal-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.681",
|
|
4
4
|
"exports": {
|
|
5
5
|
"./ui/*": {
|
|
6
6
|
"types": "./dist/ui/*/index.d.ts",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"@ark-ui/react": "^5.16.0",
|
|
80
80
|
"@capacitor/android": "^7.4.3",
|
|
81
81
|
"@capacitor/app": "^7.0.2",
|
|
82
|
+
"@capacitor/camera": "^7.0.2",
|
|
82
83
|
"@capacitor/cli": "^7.4.3",
|
|
83
84
|
"@capacitor/core": "^7.4.3",
|
|
84
85
|
"@capacitor/file-transfer": "^1.0.5",
|