@trustchex/react-native-sdk 1.250.0 → 1.266.0
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/README.md +43 -2
- package/android/src/main/java/com/trustchex/reactnativesdk/DeviceBrightnessModule.kt +66 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
- package/ios/DeviceBrightnessModule.h +4 -0
- package/ios/DeviceBrightnessModule.m +27 -0
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +25 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +19 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +19 -0
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +18 -5
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +10 -2
- package/lib/module/Screens/Static/ResultScreen.js +52 -3
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +82 -72
- package/lib/module/Shared/Components/EIDScanner.js +63 -3
- package/lib/module/Shared/Components/FaceCamera.js +73 -6
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +9 -4
- package/lib/module/Shared/Components/LanguageSelector.js +14 -10
- package/lib/module/Shared/Components/NavigationManager.js +4 -2
- package/lib/module/Shared/Components/QrCodeScannerCamera.js +6 -1
- package/lib/module/Shared/Components/StyledButton.js +108 -9
- package/lib/module/Shared/Components/StyledTextInput.js +87 -0
- package/lib/module/Shared/Contexts/AppContext.js +3 -1
- package/lib/module/Shared/Contexts/ThemeContext.js +40 -0
- package/lib/module/Shared/Libs/analytics.utils.js +430 -0
- package/lib/module/Shared/Libs/camera.utils.js +58 -2
- package/lib/module/Shared/Libs/deeplink.utils.js +8 -0
- package/lib/module/Shared/Libs/http-client.js +89 -28
- package/lib/module/Shared/Services/AnalyticsService.js +404 -0
- package/lib/module/Shared/Types/analytics.types.js +111 -0
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +1 -0
- package/lib/module/Translation/index.js +17 -5
- package/lib/module/Trustchex.js +52 -16
- package/lib/module/index.js +3 -0
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +7 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/LanguageSelector.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/StyledButton.d.ts +12 -2
- package/lib/typescript/src/Shared/Components/StyledButton.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/StyledTextInput.d.ts +15 -0
- package/lib/typescript/src/Shared/Components/StyledTextInput.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +2 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Contexts/ThemeContext.d.ts +26 -0
- package/lib/typescript/src/Shared/Contexts/ThemeContext.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts +98 -0
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +19 -1
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts +86 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts +146 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -0
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Translation/index.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts +1 -0
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +6 -7
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +35 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +30 -0
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +30 -0
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +30 -4
- package/src/Screens/Static/QrCodeScanningScreen.tsx +12 -2
- package/src/Screens/Static/ResultScreen.tsx +79 -4
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +113 -90
- package/src/Shared/Components/EIDScanner.tsx +132 -3
- package/src/Shared/Components/FaceCamera.tsx +81 -4
- package/src/Shared/Components/IdentityDocumentCamera.tsx +8 -6
- package/src/Shared/Components/LanguageSelector.tsx +12 -11
- package/src/Shared/Components/NavigationManager.tsx +5 -3
- package/src/Shared/Components/QrCodeScannerCamera.tsx +5 -1
- package/src/Shared/Components/StyledButton.tsx +141 -10
- package/src/Shared/Components/StyledTextInput.tsx +128 -0
- package/src/Shared/Contexts/AppContext.ts +4 -0
- package/src/Shared/Contexts/ThemeContext.tsx +67 -0
- package/src/Shared/Libs/analytics.utils.ts +644 -0
- package/src/Shared/Libs/camera.utils.ts +74 -2
- package/src/Shared/Libs/deeplink.utils.ts +5 -0
- package/src/Shared/Libs/http-client.ts +105 -31
- package/src/Shared/Services/AnalyticsService.ts +470 -0
- package/src/Shared/Types/analytics.types.ts +179 -0
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +1 -0
- package/src/Translation/Resources/tr.ts +2 -1
- package/src/Translation/index.ts +21 -10
- package/src/Trustchex.tsx +65 -20
- package/src/index.tsx +33 -0
|
@@ -4,16 +4,19 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState }
|
|
|
4
4
|
import { SafeAreaView, Text, StyleSheet, Alert, View, Image, Dimensions, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
|
|
5
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
6
|
import AppContext from "../../Shared/Contexts/AppContext.js";
|
|
7
|
-
import httpClient, { NotFoundError } from "../../Shared/Libs/http-client.js";
|
|
7
|
+
import httpClient, { BadRequestError, NotFoundError } from "../../Shared/Libs/http-client.js";
|
|
8
8
|
import { useTranslation } from 'react-i18next';
|
|
9
9
|
import LanguageSelector from "../../Shared/Components/LanguageSelector.js";
|
|
10
10
|
import NavigationManager from "../../Shared/Components/NavigationManager.js";
|
|
11
|
-
import { TextInput, useTheme } from 'react-native-paper';
|
|
12
11
|
import StyledButton from "../../Shared/Components/StyledButton.js";
|
|
12
|
+
import StyledTextInput from "../../Shared/Components/StyledTextInput.js";
|
|
13
|
+
import { analyticsService } from "../../Shared/Services/AnalyticsService.js";
|
|
14
|
+
import { useTheme } from "../../Shared/Contexts/ThemeContext.js";
|
|
13
15
|
import LottieView from 'lottie-react-native';
|
|
14
16
|
import { getSimulatedDemoData, isDemoSession } from "../../Shared/Libs/demo.utils.js";
|
|
15
17
|
import { useNavigation } from '@react-navigation/native';
|
|
16
18
|
import { SESSION_CODE_CHARSET_PATTERN, SESSION_CODE_VALIDATION_PATTERN } from "../../Shared/Constants/validation.constants.js";
|
|
19
|
+
import { trackError, trackFunnelStep, useScreenTracking } from "../../Shared/Libs/analytics.utils.js";
|
|
17
20
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
21
|
const VerificationSessionCheckScreen = () => {
|
|
19
22
|
const [sessionCode, setSessionCode] = useState('');
|
|
@@ -28,9 +31,13 @@ const VerificationSessionCheckScreen = () => {
|
|
|
28
31
|
} = useTranslation();
|
|
29
32
|
const navigationManagerRef = React.useRef(null);
|
|
30
33
|
const navigation = useNavigation();
|
|
31
|
-
const theme = useTheme();
|
|
32
34
|
const initialized = useRef(false);
|
|
33
35
|
const insets = useSafeAreaInsets();
|
|
36
|
+
const theme = useTheme();
|
|
37
|
+
const primaryColor = theme.colors.primary;
|
|
38
|
+
|
|
39
|
+
// Track screen view and exit
|
|
40
|
+
useScreenTracking('verification_session_check');
|
|
34
41
|
const apiUrl = useMemo(() => `${appContext.baseUrl}/api/app/mobile`, [appContext.baseUrl]);
|
|
35
42
|
const validateSessionCode = useCallback(value => {
|
|
36
43
|
return SESSION_CODE_VALIDATION_PATTERN.test(value.toUpperCase());
|
|
@@ -41,21 +48,37 @@ const VerificationSessionCheckScreen = () => {
|
|
|
41
48
|
return response;
|
|
42
49
|
} catch (error) {
|
|
43
50
|
if (error instanceof NotFoundError) {
|
|
51
|
+
// User entered invalid session ID - expected user behavior, not actionable
|
|
44
52
|
Alert.alert(t('general.error'), t('verificationSessionCheckScreen.noVerificationSessionFound'));
|
|
53
|
+
} else {
|
|
54
|
+
trackError('SESSION_CHECK_ERROR', error instanceof Error ? error.message : 'Unknown error', 'verification_session_check', 'high', {
|
|
55
|
+
recoverable: false,
|
|
56
|
+
userAction: 'check_session'
|
|
57
|
+
});
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
60
|
}, [apiUrl, appContext.isDemoSession, t]);
|
|
48
61
|
const getSessionByCode = useCallback(async inputCode => {
|
|
49
62
|
try {
|
|
50
63
|
appContext.isDemoSession = isDemoSession(inputCode);
|
|
64
|
+
analyticsService.setDemoSession(appContext.isDemoSession);
|
|
51
65
|
const response = await httpClient.get(`${apiUrl}/verification-sessions?sessionCode=${inputCode.toUpperCase()}`, appContext.isDemoSession ? getSimulatedDemoData('GET_SESSION_BY_CODE') : undefined);
|
|
52
66
|
if (!response || !response.id) {
|
|
53
67
|
throw new NotFoundError();
|
|
54
68
|
}
|
|
69
|
+
|
|
70
|
+
// Track successful session start as funnel step
|
|
71
|
+
trackFunnelStep('Session Started', 1, 5, undefined, true);
|
|
55
72
|
return response;
|
|
56
73
|
} catch (error) {
|
|
57
74
|
if (error instanceof NotFoundError) {
|
|
75
|
+
// User entered invalid session code - expected user behavior, not actionable
|
|
58
76
|
Alert.alert(t('general.error'), t('verificationSessionCheckScreen.noVerificationSessionFound'));
|
|
77
|
+
} else {
|
|
78
|
+
trackError('SESSION_CODE_CHECK_ERROR', error instanceof Error ? error.message : 'Unknown error', 'verification_session_check', 'high', {
|
|
79
|
+
recoverable: false,
|
|
80
|
+
userAction: 'check_session_code'
|
|
81
|
+
});
|
|
59
82
|
}
|
|
60
83
|
}
|
|
61
84
|
}, [apiUrl, appContext, t]);
|
|
@@ -65,8 +88,13 @@ const VerificationSessionCheckScreen = () => {
|
|
|
65
88
|
return true;
|
|
66
89
|
} catch (error) {
|
|
67
90
|
if (error instanceof NotFoundError) {
|
|
91
|
+
// Session expired or invalid - expected behavior, not actionable
|
|
68
92
|
Alert.alert(t('general.error'), t('verificationSessionCheckScreen.noVerificationSessionFound'));
|
|
69
93
|
} else {
|
|
94
|
+
trackError('VERIFICATION_CODE_SEND_FAILED', 'Failed to send verification code', 'verification_session_check', 'high', {
|
|
95
|
+
recoverable: true,
|
|
96
|
+
userAction: 'send_verification_code'
|
|
97
|
+
});
|
|
70
98
|
Alert.alert(t('general.error'), t('verificationSessionCheckScreen.cannotSendVerificationCode'));
|
|
71
99
|
}
|
|
72
100
|
}
|
|
@@ -82,7 +110,15 @@ const VerificationSessionCheckScreen = () => {
|
|
|
82
110
|
return response;
|
|
83
111
|
} catch (error) {
|
|
84
112
|
if (error instanceof NotFoundError) {
|
|
113
|
+
// Session expired or invalid - expected behavior, not actionable
|
|
85
114
|
Alert.alert(t('general.error'), t('verificationSessionCheckScreen.noVerificationSessionFound'));
|
|
115
|
+
} else if (error instanceof BadRequestError) {
|
|
116
|
+
// Wrong OTP code - expected user behavior, not actionable
|
|
117
|
+
} else {
|
|
118
|
+
trackError('VERIFIED_SESSION_CHECK_ERROR', error instanceof Error ? error.message : 'Unknown error', 'verification_session_check', 'high', {
|
|
119
|
+
recoverable: false,
|
|
120
|
+
userAction: 'check_verified_session'
|
|
121
|
+
});
|
|
86
122
|
}
|
|
87
123
|
}
|
|
88
124
|
}, [apiUrl, appContext.isDemoSession, t]);
|
|
@@ -109,9 +145,6 @@ const VerificationSessionCheckScreen = () => {
|
|
|
109
145
|
appContext.workflowSteps = session?.workflowSteps;
|
|
110
146
|
if (session?.branding) {
|
|
111
147
|
appContext.branding = appContext.branding || session.branding;
|
|
112
|
-
theme.colors.primary = appContext.branding.primaryColor;
|
|
113
|
-
theme.colors.secondary = appContext.branding.secondaryColor;
|
|
114
|
-
theme.colors.tertiary = appContext.branding.tertiaryColor;
|
|
115
148
|
}
|
|
116
149
|
if (session?.sendOTP) {
|
|
117
150
|
return sendOTPCode(session.id);
|
|
@@ -125,7 +158,7 @@ const VerificationSessionCheckScreen = () => {
|
|
|
125
158
|
}, 1000);
|
|
126
159
|
});
|
|
127
160
|
}
|
|
128
|
-
}, [appContext, appContext.identificationInfo.sessionId, getSession, sendOTPCode
|
|
161
|
+
}, [appContext, appContext.identificationInfo.sessionId, getSession, sendOTPCode]);
|
|
129
162
|
return /*#__PURE__*/_jsx(SafeAreaView, {
|
|
130
163
|
style: styles.safeAreaContainer,
|
|
131
164
|
children: /*#__PURE__*/_jsx(KeyboardAvoidingView, {
|
|
@@ -160,57 +193,54 @@ const VerificationSessionCheckScreen = () => {
|
|
|
160
193
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
161
194
|
style: styles.mainText,
|
|
162
195
|
children: t('verificationSessionCheckScreen.mainText')
|
|
163
|
-
}), /*#__PURE__*/
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (alphanumericText.length
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
appContext.identificationInfo.sessionId = session.id;
|
|
196
|
+
}), /*#__PURE__*/_jsx(StyledTextInput, {
|
|
197
|
+
autoCapitalize: "characters",
|
|
198
|
+
placeholder: t('verificationSessionCheckScreen.enterSessionCode'),
|
|
199
|
+
borderColor: primaryColor,
|
|
200
|
+
focusedBorderColor: primaryColor,
|
|
201
|
+
inputStyle: styles.sessionCodeTextInput,
|
|
202
|
+
placeholderStyle: styles.sessionCodeTextInputPlaceholder,
|
|
203
|
+
value: sessionCode.toUpperCase(),
|
|
204
|
+
onChangeText: async text => {
|
|
205
|
+
const alphanumericText = text.toUpperCase().replace(SESSION_CODE_CHARSET_PATTERN, '');
|
|
206
|
+
if (alphanumericText.length <= 8) {
|
|
207
|
+
setSessionCode(alphanumericText);
|
|
208
|
+
if (validateSessionCode(alphanumericText) && alphanumericText.length === 8) {
|
|
209
|
+
try {
|
|
210
|
+
setIsCheckingSession(true);
|
|
211
|
+
const session = await getSessionByCode(alphanumericText);
|
|
212
|
+
if (session?.id) {
|
|
213
|
+
if (appContext.setSessionId) {
|
|
214
|
+
appContext.setSessionId(session.id);
|
|
183
215
|
} else {
|
|
184
|
-
|
|
216
|
+
appContext.identificationInfo.sessionId = session.id;
|
|
185
217
|
}
|
|
186
|
-
}
|
|
218
|
+
} else {
|
|
187
219
|
setSessionCode('');
|
|
188
|
-
} finally {
|
|
189
|
-
setTimeout(() => {
|
|
190
|
-
setIsCheckingSession(false);
|
|
191
|
-
}, 1000);
|
|
192
220
|
}
|
|
221
|
+
} catch {
|
|
222
|
+
setSessionCode('');
|
|
223
|
+
} finally {
|
|
224
|
+
setTimeout(() => {
|
|
225
|
+
setIsCheckingSession(false);
|
|
226
|
+
}, 1000);
|
|
193
227
|
}
|
|
194
228
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}), !sessionCode && /*#__PURE__*/_jsx(Text, {
|
|
199
|
-
style: styles.placeholderText,
|
|
200
|
-
children: t('verificationSessionCheckScreen.enterSessionCode')
|
|
201
|
-
})]
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
maxLength: 8
|
|
202
232
|
}), /*#__PURE__*/_jsxs(View, {
|
|
203
233
|
style: styles.horizontalLineContainer,
|
|
204
234
|
children: [/*#__PURE__*/_jsx(View, {
|
|
205
235
|
style: [styles.horizontalLine, {
|
|
206
|
-
backgroundColor:
|
|
236
|
+
backgroundColor: primaryColor
|
|
207
237
|
}]
|
|
208
238
|
}), /*#__PURE__*/_jsx(Text, {
|
|
209
239
|
style: styles.mainText,
|
|
210
240
|
children: t('verificationSessionCheckScreen.or')
|
|
211
241
|
}), /*#__PURE__*/_jsx(View, {
|
|
212
242
|
style: [styles.horizontalLine, {
|
|
213
|
-
backgroundColor:
|
|
243
|
+
backgroundColor: primaryColor
|
|
214
244
|
}]
|
|
215
245
|
})]
|
|
216
246
|
}), /*#__PURE__*/_jsx(StyledButton, {
|
|
@@ -224,14 +254,12 @@ const VerificationSessionCheckScreen = () => {
|
|
|
224
254
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
225
255
|
style: styles.mainText,
|
|
226
256
|
children: t('verificationSessionCheckScreen.codeText')
|
|
227
|
-
}), /*#__PURE__*/_jsx(
|
|
228
|
-
mode: "outlined",
|
|
257
|
+
}), /*#__PURE__*/_jsx(StyledTextInput, {
|
|
229
258
|
autoFocus: true,
|
|
230
259
|
placeholder: "",
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
contentStyle: styles.otpCodeInputWithText,
|
|
260
|
+
borderColor: primaryColor,
|
|
261
|
+
focusedBorderColor: primaryColor,
|
|
262
|
+
inputStyle: styles.otpCodeTextInput,
|
|
235
263
|
keyboardType: "number-pad",
|
|
236
264
|
textContentType: "oneTimeCode",
|
|
237
265
|
autoComplete: "sms-otp",
|
|
@@ -250,6 +278,7 @@ const VerificationSessionCheckScreen = () => {
|
|
|
250
278
|
setIsCodeSent(false);
|
|
251
279
|
navigationManagerRef.current?.navigateToNextStep();
|
|
252
280
|
} else {
|
|
281
|
+
// User entered wrong OTP code - expected behavior, not actionable
|
|
253
282
|
appContext.onError?.('Invalid OTP code');
|
|
254
283
|
Alert.alert(t('general.error'), t('verificationSessionCheckScreen.codeError'));
|
|
255
284
|
setCode('');
|
|
@@ -313,7 +342,7 @@ const styles = StyleSheet.create({
|
|
|
313
342
|
},
|
|
314
343
|
horizontalLine: {
|
|
315
344
|
flex: 1,
|
|
316
|
-
height:
|
|
345
|
+
height: 1
|
|
317
346
|
},
|
|
318
347
|
loadingAnimation: {
|
|
319
348
|
width: '100%',
|
|
@@ -382,40 +411,21 @@ const styles = StyleSheet.create({
|
|
|
382
411
|
alignItems: 'center',
|
|
383
412
|
position: 'absolute'
|
|
384
413
|
},
|
|
385
|
-
inputContainer: {
|
|
386
|
-
position: 'relative',
|
|
387
|
-
width: '100%'
|
|
388
|
-
},
|
|
389
414
|
sessionCodeTextInput: {
|
|
390
|
-
backgroundColor: 'white'
|
|
391
|
-
},
|
|
392
|
-
sessionCodeInputWithText: {
|
|
393
415
|
textTransform: 'uppercase',
|
|
394
416
|
fontWeight: '800',
|
|
395
417
|
fontSize: 28,
|
|
396
418
|
textAlign: 'center',
|
|
397
419
|
letterSpacing: 8
|
|
398
420
|
},
|
|
399
|
-
|
|
400
|
-
position: 'absolute',
|
|
401
|
-
top: 0,
|
|
402
|
-
left: 0,
|
|
403
|
-
right: 0,
|
|
404
|
-
height: '100%',
|
|
405
|
-
lineHeight: 56,
|
|
421
|
+
sessionCodeTextInputPlaceholder: {
|
|
406
422
|
fontSize: 16,
|
|
407
|
-
|
|
423
|
+
fontWeight: '400',
|
|
424
|
+
letterSpacing: 0,
|
|
408
425
|
textAlign: 'center',
|
|
409
|
-
|
|
410
|
-
},
|
|
411
|
-
sessionCodeInputPlaceholder: {
|
|
412
|
-
fontSize: 16,
|
|
413
|
-
textAlign: 'center'
|
|
426
|
+
textTransform: 'none'
|
|
414
427
|
},
|
|
415
428
|
otpCodeTextInput: {
|
|
416
|
-
backgroundColor: 'white'
|
|
417
|
-
},
|
|
418
|
-
otpCodeInputWithText: {
|
|
419
429
|
fontWeight: '800',
|
|
420
430
|
fontSize: 28,
|
|
421
431
|
textAlign: 'center',
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { Alert, View, Text, Image, StyleSheet } from 'react-native';
|
|
5
5
|
import NFCManager from 'react-native-nfc-manager';
|
|
6
|
+
import DeviceInfo from 'react-native-device-info';
|
|
6
7
|
import { eidReader } from "../EIDReader/eidReader.js";
|
|
7
8
|
import NativeProgressBar from "./NativeProgressBar.js";
|
|
8
9
|
import { useTranslation } from 'react-i18next';
|
|
@@ -11,6 +12,7 @@ import StyledButton from "./StyledButton.js";
|
|
|
11
12
|
import LottieView from 'lottie-react-native';
|
|
12
13
|
import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
|
|
13
14
|
import { speakWithDebounce } from "../Libs/tts.utils.js";
|
|
15
|
+
import { trackEIDScanStart, trackEIDScanComplete, trackEIDScanFailed } from "../Libs/analytics.utils.js";
|
|
14
16
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
15
17
|
const EIDScanner = ({
|
|
16
18
|
documentNumber,
|
|
@@ -35,33 +37,85 @@ const EIDScanner = ({
|
|
|
35
37
|
} = useTranslation();
|
|
36
38
|
const appContext = React.useContext(AppContext);
|
|
37
39
|
const [voiceGuidanceMessage, setVoiceGuidanceMessage] = useState();
|
|
40
|
+
const [attemptNumber, setAttemptNumber] = useState(0);
|
|
38
41
|
const readNFC = useCallback(async () => {
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
const currentAttempt = attemptNumber + 1;
|
|
44
|
+
setAttemptNumber(currentAttempt);
|
|
39
45
|
setIsScanning(true);
|
|
40
46
|
setIsScanned(false);
|
|
41
47
|
setProgress(0);
|
|
48
|
+
|
|
49
|
+
// Track EID scan start using analytics helper
|
|
50
|
+
const docType = documentType || 'UNKNOWN';
|
|
51
|
+
|
|
52
|
+
// If device doesn't support NFC or NFC is disabled, track and abort early
|
|
53
|
+
if (!hasNfc) {
|
|
54
|
+
await trackEIDScanFailed(docType, 'device_unsupported', 'Device does not support NFC', 0, currentAttempt, DeviceInfo.getModel(), DeviceInfo.getSystemVersion()).catch(() => {});
|
|
55
|
+
|
|
56
|
+
// Stop scanning - user will see the not-supported UI
|
|
57
|
+
setIsScanning(false);
|
|
58
|
+
setProgress(0);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!isEnabled) {
|
|
62
|
+
await trackEIDScanFailed(docType, 'not_enabled', 'NFC is disabled on device', 0, currentAttempt, DeviceInfo.getModel(), DeviceInfo.getSystemVersion()).catch(() => {});
|
|
63
|
+
|
|
64
|
+
// Stop scanning - allow the UI to prompt user to enable NFC
|
|
65
|
+
setIsScanning(false);
|
|
66
|
+
setProgress(0);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await trackEIDScanStart(docType, hasNfc, isEnabled, currentAttempt).catch(() => {});
|
|
42
70
|
try {
|
|
43
71
|
if (documentNumber && dateOfBirth && dateOfExpiry) {
|
|
44
72
|
const passportData = await eidReader(documentNumber, dateOfBirth, dateOfExpiry, progressValue => {
|
|
45
73
|
setProgress(progressValue);
|
|
46
74
|
});
|
|
47
75
|
if (passportData) {
|
|
76
|
+
const scanDuration = Date.now() - startTime;
|
|
48
77
|
setDocumentFaceImage(passportData.faceImage);
|
|
49
78
|
setDocumentMRZInfo(passportData.mrz);
|
|
50
79
|
setDocumentFaceImageMimeType(passportData.mimeType);
|
|
51
80
|
setIsScanned(true);
|
|
81
|
+
|
|
82
|
+
// Track successful EID scan
|
|
83
|
+
await trackEIDScanComplete(docType, scanDuration, currentAttempt).catch(() => {});
|
|
52
84
|
}
|
|
53
85
|
} else {
|
|
86
|
+
const scanDuration = Date.now() - startTime;
|
|
87
|
+
// MRZ input issue — track as NFC scan failure (unknown error type for NFC-specific stream)
|
|
88
|
+
await trackEIDScanFailed(docType, 'unknown', 'Invalid MRZ fields', scanDuration, currentAttempt, DeviceInfo.getModel(), DeviceInfo.getSystemVersion()).catch(() => {});
|
|
54
89
|
Alert.alert(t('general.error'), t('eidScannerScreen.invalidMRZFields'));
|
|
55
90
|
}
|
|
56
91
|
} catch (error) {
|
|
57
|
-
|
|
58
|
-
|
|
92
|
+
const scanDuration = Date.now() - startTime;
|
|
93
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
94
|
+
|
|
95
|
+
// Determine error type
|
|
96
|
+
let errorType = 'unknown';
|
|
97
|
+
if (errorMessage.includes('cancelled') || errorMessage.includes('cancel')) {
|
|
98
|
+
errorType = 'user_cancelled';
|
|
99
|
+
} else if (errorMessage.includes('timeout') || errorMessage.includes('time out')) {
|
|
100
|
+
errorType = 'timeout';
|
|
101
|
+
} else if (errorMessage.includes('not supported')) {
|
|
102
|
+
errorType = 'device_unsupported';
|
|
103
|
+
} else if (errorMessage.includes('not enabled') || errorMessage.includes('disabled')) {
|
|
104
|
+
errorType = 'not_enabled';
|
|
105
|
+
} else if (errorMessage.includes('read') || errorMessage.includes('communication')) {
|
|
106
|
+
errorType = 'reading_error';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Track EID scan failure with detailed metadata
|
|
110
|
+
await trackEIDScanFailed(docType, errorType, errorMessage, scanDuration, currentAttempt, DeviceInfo.getModel(), DeviceInfo.getSystemVersion()).catch(() => {});
|
|
111
|
+
console.debug('NFC scan error:', error);
|
|
112
|
+
// Ignore error - let user retry
|
|
59
113
|
} finally {
|
|
60
114
|
setIsScanning(false);
|
|
61
115
|
setProgress(0);
|
|
62
116
|
setHasGuideShown(false);
|
|
63
117
|
}
|
|
64
|
-
}, [documentNumber, dateOfBirth, dateOfExpiry, t]);
|
|
118
|
+
}, [documentNumber, dateOfBirth, dateOfExpiry, documentType, hasNfc, isEnabled, attemptNumber, t]);
|
|
65
119
|
const getFieldsFromMRZ = useCallback(mrz => {
|
|
66
120
|
return {
|
|
67
121
|
documentCode: mrz.getDocumentCode(),
|
|
@@ -81,8 +135,14 @@ const EIDScanner = ({
|
|
|
81
135
|
const deviceIsSupported = await NFCManager.isSupported();
|
|
82
136
|
setHasNFC(deviceIsSupported);
|
|
83
137
|
isNFCSupported && isNFCSupported(deviceIsSupported);
|
|
138
|
+
|
|
139
|
+
// NFC not supported is a device limitation - not actionable by developers
|
|
140
|
+
|
|
84
141
|
const deviceIsEnabled = await NFCManager.isEnabled();
|
|
85
142
|
setIsEnabled(deviceIsEnabled);
|
|
143
|
+
|
|
144
|
+
// NFC disabled is a user setting - they can enable it in settings
|
|
145
|
+
// Not tracking as it's user-controllable
|
|
86
146
|
};
|
|
87
147
|
checkIsSupported();
|
|
88
148
|
}, [isNFCSupported, isScanning]);
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
import { useIsFocused } from '@react-navigation/native';
|
|
4
4
|
import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
|
|
5
5
|
import React, { useEffect, useState } from 'react';
|
|
6
|
-
import { StyleSheet, Text, View, Platform, Linking, Dimensions } from 'react-native';
|
|
6
|
+
import { StyleSheet, Text, View, Platform, Linking, Dimensions, ActivityIndicator, NativeModules } from 'react-native';
|
|
7
7
|
import { useCameraDevice, useCameraPermission, Camera, useFrameProcessor, useCameraFormat, runAtTargetFps, useMicrophonePermission } from 'react-native-vision-camera';
|
|
8
8
|
import { runAsync } from "../Libs/worklet.utils.js";
|
|
9
9
|
import { useFaceDetector } from "../VisionCameraPlugins/FaceDetector/index.js";
|
|
10
10
|
import { Worklets, useSharedValue } from 'react-native-worklets-core';
|
|
11
11
|
import { crop } from "../VisionCameraPlugins/Cropper/index.js";
|
|
12
|
-
import {
|
|
12
|
+
import { isCircularRegionBright } from "../Libs/camera.utils.js";
|
|
13
13
|
import { useTranslation } from 'react-i18next';
|
|
14
|
-
import { ActivityIndicator } from 'react-native-paper';
|
|
15
14
|
import StyledButton from "./StyledButton.js";
|
|
15
|
+
import { useTheme } from "../Contexts/ThemeContext.js";
|
|
16
16
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
17
17
|
const VIDEO_WIDTH = 1280;
|
|
18
18
|
const VIDEO_HEIGHT = 720;
|
|
@@ -20,9 +20,11 @@ const windowWidth = Dimensions.get('window').width;
|
|
|
20
20
|
const windowHeight = Dimensions.get('window').height;
|
|
21
21
|
const FaceCamera = ({
|
|
22
22
|
onFacesDetected,
|
|
23
|
-
onCameraInitialized
|
|
23
|
+
onCameraInitialized,
|
|
24
|
+
previewRect
|
|
24
25
|
}) => {
|
|
25
26
|
useKeepAwake();
|
|
27
|
+
const theme = useTheme();
|
|
26
28
|
const cameraPermission = useCameraPermission();
|
|
27
29
|
const microphonePermission = useMicrophonePermission();
|
|
28
30
|
const [permissionsRequested, setPermissionsRequested] = useState(false);
|
|
@@ -83,11 +85,72 @@ const FaceCamera = ({
|
|
|
83
85
|
setIsActive(false);
|
|
84
86
|
};
|
|
85
87
|
}, [device, format, isFocused]);
|
|
88
|
+
|
|
89
|
+
// Set screen brightness to maximum when camera is active
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
const {
|
|
92
|
+
DeviceBrightness
|
|
93
|
+
} = NativeModules;
|
|
94
|
+
if (!DeviceBrightness) return;
|
|
95
|
+
let originalBrightness = -1;
|
|
96
|
+
const setBrightness = async () => {
|
|
97
|
+
if (isActive) {
|
|
98
|
+
try {
|
|
99
|
+
originalBrightness = await DeviceBrightness.getBrightness();
|
|
100
|
+
await DeviceBrightness.setBrightness(1.0);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.log('Failed to set brightness:', error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
setBrightness();
|
|
107
|
+
return () => {
|
|
108
|
+
if (originalBrightness >= 0) {
|
|
109
|
+
DeviceBrightness.setBrightness(originalBrightness).catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}, [isActive]);
|
|
86
113
|
const handleWorklet = frame => {
|
|
87
114
|
'worklet';
|
|
88
115
|
|
|
89
116
|
try {
|
|
90
|
-
|
|
117
|
+
// Calculate brightness based on the circular preview area if provided
|
|
118
|
+
// Otherwise fall back to entire frame brightness
|
|
119
|
+
let isBright = false;
|
|
120
|
+
if (previewRect) {
|
|
121
|
+
// Convert preview rect from screen coordinates to frame coordinates
|
|
122
|
+
const scaleX = frame.width / windowWidth;
|
|
123
|
+
const scaleY = frame.height / windowHeight;
|
|
124
|
+
const frameCircleRect = {
|
|
125
|
+
minX: Math.floor(previewRect.minX * scaleX),
|
|
126
|
+
minY: Math.floor(previewRect.minY * scaleY),
|
|
127
|
+
width: Math.floor(previewRect.width * scaleX),
|
|
128
|
+
height: Math.floor(previewRect.height * scaleY)
|
|
129
|
+
};
|
|
130
|
+
isBright = isCircularRegionBright(frame, frameCircleRect, 60);
|
|
131
|
+
} else {
|
|
132
|
+
// Fallback: check entire frame brightness (use legacy method)
|
|
133
|
+
const buffer = frame.toArrayBuffer();
|
|
134
|
+
const data = new Uint8Array(buffer);
|
|
135
|
+
const width = frame.width;
|
|
136
|
+
const height = frame.height;
|
|
137
|
+
let luminanceSum = 0;
|
|
138
|
+
let pixelCount = 0;
|
|
139
|
+
const centerX = Math.floor(width / 2);
|
|
140
|
+
const centerY = Math.floor(height / 2);
|
|
141
|
+
const halfSizeX = Math.floor(width / 2);
|
|
142
|
+
const halfSizeY = Math.floor(height / 2);
|
|
143
|
+
for (let y = centerY - halfSizeY; y < centerY + halfSizeY; y++) {
|
|
144
|
+
for (let x = centerX - halfSizeX; x < centerX + halfSizeX; x++) {
|
|
145
|
+
const index = y * width + x;
|
|
146
|
+
if (data[index] !== undefined) {
|
|
147
|
+
luminanceSum += data[index];
|
|
148
|
+
pixelCount++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
isBright = luminanceSum / pixelCount > 60;
|
|
153
|
+
}
|
|
91
154
|
const image = crop(frame, {
|
|
92
155
|
cropRegion: {
|
|
93
156
|
top: 0,
|
|
@@ -132,11 +195,13 @@ const FaceCamera = ({
|
|
|
132
195
|
return /*#__PURE__*/_jsx(View, {
|
|
133
196
|
style: styles.permissionContainer,
|
|
134
197
|
children: /*#__PURE__*/_jsx(ActivityIndicator, {
|
|
135
|
-
size: "large"
|
|
198
|
+
size: "large",
|
|
199
|
+
color: theme.colors.primary
|
|
136
200
|
})
|
|
137
201
|
});
|
|
138
202
|
}
|
|
139
203
|
if (!cameraPermission.hasPermission) {
|
|
204
|
+
// Camera permission denied by user - their choice, not actionable
|
|
140
205
|
return /*#__PURE__*/_jsxs(View, {
|
|
141
206
|
style: styles.permissionContainer,
|
|
142
207
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
@@ -152,6 +217,7 @@ const FaceCamera = ({
|
|
|
152
217
|
});
|
|
153
218
|
}
|
|
154
219
|
if (!microphonePermission.hasPermission) {
|
|
220
|
+
// Microphone permission denied by user - their choice, not actionable
|
|
155
221
|
return /*#__PURE__*/_jsxs(View, {
|
|
156
222
|
style: styles.permissionContainer,
|
|
157
223
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
@@ -167,6 +233,7 @@ const FaceCamera = ({
|
|
|
167
233
|
});
|
|
168
234
|
}
|
|
169
235
|
if (device == null) {
|
|
236
|
+
// No camera device - device limitation, not actionable
|
|
170
237
|
return /*#__PURE__*/_jsx(View, {
|
|
171
238
|
style: styles.permissionContainer,
|
|
172
239
|
children: /*#__PURE__*/_jsx(Text, {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/* eslint-disable react-native/no-inline-styles */
|
|
4
4
|
import React, { useEffect, useState } from 'react';
|
|
5
|
-
import { View, StyleSheet, Text as TextView, Platform, Vibration, TouchableOpacity, Text, Linking, Image } from 'react-native';
|
|
5
|
+
import { View, StyleSheet, Text as TextView, Platform, Vibration, TouchableOpacity, Text, Linking, Image, ActivityIndicator } from 'react-native';
|
|
6
6
|
import { Camera, runAtTargetFps, useCameraDevice, useCameraFormat, useCameraPermission, useFrameProcessor } from 'react-native-vision-camera';
|
|
7
7
|
import { runAsync } from "../Libs/worklet.utils.js";
|
|
8
8
|
import { useRunOnJS, useSharedValue } from 'react-native-worklets-core';
|
|
@@ -17,11 +17,11 @@ import { AdaptiveThresholdTypes, ColorConversionCodes, DataTypes, ObjectType, Op
|
|
|
17
17
|
import { getAverageBrightness } from "../Libs/camera.utils.js";
|
|
18
18
|
import { useTranslation } from 'react-i18next';
|
|
19
19
|
import LottieView from 'lottie-react-native';
|
|
20
|
-
import { ActivityIndicator } from 'react-native-paper';
|
|
21
20
|
import StyledButton from "./StyledButton.js";
|
|
22
21
|
import { scanCodes } from "../VisionCameraPlugins/BarcodeScanner/index.js";
|
|
23
22
|
import { speakWithDebounce } from "../Libs/tts.utils.js";
|
|
24
23
|
import AppContext from "../Contexts/AppContext.js";
|
|
24
|
+
import { useTheme } from "../Contexts/ThemeContext.js";
|
|
25
25
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
26
26
|
// const windowWidth = Dimensions.get('window').width;
|
|
27
27
|
// const windowHeight = Dimensions.get('window').height;
|
|
@@ -38,6 +38,7 @@ const IdentityDocumentCamera = ({
|
|
|
38
38
|
showDebugImages = false
|
|
39
39
|
}) => {
|
|
40
40
|
useKeepAwake();
|
|
41
|
+
const theme = useTheme();
|
|
41
42
|
const appContext = React.useContext(AppContext);
|
|
42
43
|
const cameraRef = React.useRef(null);
|
|
43
44
|
const cameraPermission = useCameraPermission();
|
|
@@ -298,7 +299,10 @@ const IdentityDocumentCamera = ({
|
|
|
298
299
|
|
|
299
300
|
// Add validation for face bounds to prevent invalid crop operations
|
|
300
301
|
if (face.bounds.x < 0 || face.bounds.y < 0 || face.bounds.width <= 0 || face.bounds.height <= 0 || face.bounds.x >= width || face.bounds.y >= height) {
|
|
301
|
-
console.warn(
|
|
302
|
+
// console.warn(
|
|
303
|
+
// 'Invalid face bounds detected, skipping face:',
|
|
304
|
+
// face.bounds
|
|
305
|
+
// );
|
|
302
306
|
continue;
|
|
303
307
|
}
|
|
304
308
|
|
|
@@ -622,7 +626,8 @@ const IdentityDocumentCamera = ({
|
|
|
622
626
|
return /*#__PURE__*/_jsx(View, {
|
|
623
627
|
style: styles.permissionContainer,
|
|
624
628
|
children: /*#__PURE__*/_jsx(ActivityIndicator, {
|
|
625
|
-
size: "large"
|
|
629
|
+
size: "large",
|
|
630
|
+
color: theme.colors.primary
|
|
626
631
|
})
|
|
627
632
|
});
|
|
628
633
|
}
|
|
@@ -3,26 +3,32 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
5
5
|
import i18n from "../../Translation/index.js";
|
|
6
|
-
import
|
|
6
|
+
import { useTheme } from "../Contexts/ThemeContext.js";
|
|
7
7
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
8
8
|
const LanguageSelector = () => {
|
|
9
|
-
const
|
|
9
|
+
const theme = useTheme();
|
|
10
|
+
const isEnglish = i18n.language.startsWith('en');
|
|
11
|
+
const isTurkish = i18n.language.startsWith('tr');
|
|
10
12
|
return /*#__PURE__*/_jsxs(View, {
|
|
11
13
|
style: styles.container,
|
|
12
14
|
children: [/*#__PURE__*/_jsx(Pressable, {
|
|
13
15
|
onPress: () => i18n.changeLanguage('en'),
|
|
14
16
|
children: /*#__PURE__*/_jsx(Text, {
|
|
15
|
-
style: [styles.buttonText,
|
|
16
|
-
color:
|
|
17
|
+
style: [styles.buttonText, isEnglish && styles.buttonTextSelected, isEnglish && {
|
|
18
|
+
color: theme.colors.primary
|
|
17
19
|
}],
|
|
18
20
|
children: "EN"
|
|
19
21
|
})
|
|
20
22
|
}), /*#__PURE__*/_jsx(View, {
|
|
21
|
-
style: styles.verticalSeperator
|
|
23
|
+
style: [styles.verticalSeperator, {
|
|
24
|
+
borderColor: theme.colors.border
|
|
25
|
+
}]
|
|
22
26
|
}), /*#__PURE__*/_jsx(Pressable, {
|
|
23
27
|
onPress: () => i18n.changeLanguage('tr'),
|
|
24
28
|
children: /*#__PURE__*/_jsx(Text, {
|
|
25
|
-
style: [styles.buttonText,
|
|
29
|
+
style: [styles.buttonText, isTurkish && styles.buttonTextSelected, isTurkish && {
|
|
30
|
+
color: theme.colors.primary
|
|
31
|
+
}],
|
|
26
32
|
children: "TR"
|
|
27
33
|
})
|
|
28
34
|
})]
|
|
@@ -38,16 +44,14 @@ const styles = StyleSheet.create({
|
|
|
38
44
|
},
|
|
39
45
|
buttonText: {
|
|
40
46
|
fontSize: 18,
|
|
41
|
-
color: '
|
|
47
|
+
color: '#999999'
|
|
42
48
|
},
|
|
43
49
|
buttonTextSelected: {
|
|
44
|
-
color: '#000000',
|
|
45
50
|
fontWeight: 'bold'
|
|
46
51
|
},
|
|
47
52
|
verticalSeperator: {
|
|
48
53
|
borderLeftWidth: 1,
|
|
49
|
-
height: 24
|
|
50
|
-
borderColor: 'black'
|
|
54
|
+
height: 24
|
|
51
55
|
}
|
|
52
56
|
});
|
|
53
57
|
export default LanguageSelector;
|