@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.
Files changed (100) hide show
  1. package/README.md +43 -2
  2. package/android/src/main/java/com/trustchex/reactnativesdk/DeviceBrightnessModule.kt +66 -0
  3. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
  4. package/ios/DeviceBrightnessModule.h +4 -0
  5. package/ios/DeviceBrightnessModule.m +27 -0
  6. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +25 -0
  7. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +19 -0
  8. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +19 -0
  9. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +18 -5
  10. package/lib/module/Screens/Static/QrCodeScanningScreen.js +10 -2
  11. package/lib/module/Screens/Static/ResultScreen.js +52 -3
  12. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +82 -72
  13. package/lib/module/Shared/Components/EIDScanner.js +63 -3
  14. package/lib/module/Shared/Components/FaceCamera.js +73 -6
  15. package/lib/module/Shared/Components/IdentityDocumentCamera.js +9 -4
  16. package/lib/module/Shared/Components/LanguageSelector.js +14 -10
  17. package/lib/module/Shared/Components/NavigationManager.js +4 -2
  18. package/lib/module/Shared/Components/QrCodeScannerCamera.js +6 -1
  19. package/lib/module/Shared/Components/StyledButton.js +108 -9
  20. package/lib/module/Shared/Components/StyledTextInput.js +87 -0
  21. package/lib/module/Shared/Contexts/AppContext.js +3 -1
  22. package/lib/module/Shared/Contexts/ThemeContext.js +40 -0
  23. package/lib/module/Shared/Libs/analytics.utils.js +430 -0
  24. package/lib/module/Shared/Libs/camera.utils.js +58 -2
  25. package/lib/module/Shared/Libs/deeplink.utils.js +8 -0
  26. package/lib/module/Shared/Libs/http-client.js +89 -28
  27. package/lib/module/Shared/Services/AnalyticsService.js +404 -0
  28. package/lib/module/Shared/Types/analytics.types.js +111 -0
  29. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +1 -0
  30. package/lib/module/Translation/index.js +17 -5
  31. package/lib/module/Trustchex.js +52 -16
  32. package/lib/module/index.js +3 -0
  33. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  36. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  37. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  38. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  39. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  41. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +7 -1
  42. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  43. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/Components/LanguageSelector.d.ts.map +1 -1
  45. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  46. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/Components/StyledButton.d.ts +12 -2
  48. package/lib/typescript/src/Shared/Components/StyledButton.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Components/StyledTextInput.d.ts +15 -0
  50. package/lib/typescript/src/Shared/Components/StyledTextInput.d.ts.map +1 -0
  51. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +2 -0
  52. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  53. package/lib/typescript/src/Shared/Contexts/ThemeContext.d.ts +26 -0
  54. package/lib/typescript/src/Shared/Contexts/ThemeContext.d.ts.map +1 -0
  55. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts +98 -0
  56. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -0
  57. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +19 -1
  58. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  60. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts +86 -0
  62. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -0
  63. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +146 -0
  64. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -0
  65. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +1 -1
  66. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  67. package/lib/typescript/src/Translation/index.d.ts.map +1 -1
  68. package/lib/typescript/src/Trustchex.d.ts +1 -0
  69. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  70. package/lib/typescript/src/index.d.ts +4 -0
  71. package/lib/typescript/src/index.d.ts.map +1 -1
  72. package/package.json +6 -7
  73. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +35 -1
  74. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +30 -0
  75. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +30 -0
  76. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +30 -4
  77. package/src/Screens/Static/QrCodeScanningScreen.tsx +12 -2
  78. package/src/Screens/Static/ResultScreen.tsx +79 -4
  79. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +113 -90
  80. package/src/Shared/Components/EIDScanner.tsx +132 -3
  81. package/src/Shared/Components/FaceCamera.tsx +81 -4
  82. package/src/Shared/Components/IdentityDocumentCamera.tsx +8 -6
  83. package/src/Shared/Components/LanguageSelector.tsx +12 -11
  84. package/src/Shared/Components/NavigationManager.tsx +5 -3
  85. package/src/Shared/Components/QrCodeScannerCamera.tsx +5 -1
  86. package/src/Shared/Components/StyledButton.tsx +141 -10
  87. package/src/Shared/Components/StyledTextInput.tsx +128 -0
  88. package/src/Shared/Contexts/AppContext.ts +4 -0
  89. package/src/Shared/Contexts/ThemeContext.tsx +67 -0
  90. package/src/Shared/Libs/analytics.utils.ts +644 -0
  91. package/src/Shared/Libs/camera.utils.ts +74 -2
  92. package/src/Shared/Libs/deeplink.utils.ts +5 -0
  93. package/src/Shared/Libs/http-client.ts +105 -31
  94. package/src/Shared/Services/AnalyticsService.ts +470 -0
  95. package/src/Shared/Types/analytics.types.ts +179 -0
  96. package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +1 -0
  97. package/src/Translation/Resources/tr.ts +2 -1
  98. package/src/Translation/index.ts +21 -10
  99. package/src/Trustchex.tsx +65 -20
  100. 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, theme.colors, theme.colors.primary, theme.colors.secondary, theme.colors.tertiary]);
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__*/_jsxs(View, {
164
- style: styles.inputContainer,
165
- children: [/*#__PURE__*/_jsx(TextInput, {
166
- mode: "outlined",
167
- autoCapitalize: "characters",
168
- placeholder: "",
169
- outlineColor: theme.colors.primary,
170
- activeOutlineColor: theme.colors.primary,
171
- style: styles.sessionCodeTextInput,
172
- contentStyle: styles.sessionCodeInputWithText,
173
- onChangeText: async text => {
174
- const alphanumericText = text.toUpperCase().replace(SESSION_CODE_CHARSET_PATTERN, '');
175
- if (alphanumericText.length <= 8) {
176
- setSessionCode(alphanumericText);
177
- if (validateSessionCode(alphanumericText) && alphanumericText.length === 8) {
178
- try {
179
- setIsCheckingSession(true);
180
- const session = await getSessionByCode(alphanumericText);
181
- if (session?.id) {
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
- setSessionCode('');
216
+ appContext.identificationInfo.sessionId = session.id;
185
217
  }
186
- } catch {
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
- value: sessionCode.toUpperCase(),
197
- maxLength: 8
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: theme.colors.primary
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: theme.colors.primary
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(TextInput, {
228
- mode: "outlined",
257
+ }), /*#__PURE__*/_jsx(StyledTextInput, {
229
258
  autoFocus: true,
230
259
  placeholder: "",
231
- outlineColor: theme.colors.primary,
232
- activeOutlineColor: theme.colors.primary,
233
- style: styles.otpCodeTextInput,
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: 2
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
- placeholderText: {
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
- color: '#666',
423
+ fontWeight: '400',
424
+ letterSpacing: 0,
408
425
  textAlign: 'center',
409
- pointerEvents: 'none'
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
- console.debug(error);
58
- // Ignore error
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 { isFrameBright } from "../Libs/camera.utils.js";
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
- const isBright = isFrameBright(frame);
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('Invalid face bounds detected, skipping face:', face.bounds);
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 AppContext from "../Contexts/AppContext.js";
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 appContext = React.useContext(AppContext);
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, i18n.language.startsWith('en') ? styles.buttonTextSelected : null, {
16
- color: appContext.branding.primaryColor
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, i18n.language.startsWith('tr') ? styles.buttonTextSelected : null],
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: 'black'
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;