@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
@@ -20,15 +20,17 @@ import {
20
20
  } from 'react-native';
21
21
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
22
22
  import AppContext from '../../Shared/Contexts/AppContext';
23
- import httpClient, { NotFoundError } from '../../Shared/Libs/http-client';
23
+ import httpClient, { BadRequestError, NotFoundError } from '../../Shared/Libs/http-client';
24
24
  import { useTranslation } from 'react-i18next';
25
25
  import LanguageSelector from '../../Shared/Components/LanguageSelector';
26
26
  import NavigationManager, {
27
27
  type NavigationManagerRef,
28
28
  } from '../../Shared/Components/NavigationManager';
29
29
  import type { VerificationSession } from '../../Shared/Types/verificationSession';
30
- import { TextInput, useTheme } from 'react-native-paper';
31
30
  import StyledButton from '../../Shared/Components/StyledButton';
31
+ import StyledTextInput from '../../Shared/Components/StyledTextInput';
32
+ import { analyticsService } from '../../Shared/Services/AnalyticsService';
33
+ import { useTheme } from '../../Shared/Contexts/ThemeContext';
32
34
  import LottieView from 'lottie-react-native';
33
35
  import {
34
36
  getSimulatedDemoData,
@@ -39,6 +41,11 @@ import {
39
41
  SESSION_CODE_CHARSET_PATTERN,
40
42
  SESSION_CODE_VALIDATION_PATTERN,
41
43
  } from '../../Shared/Constants/validation.constants';
44
+ import {
45
+ trackError,
46
+ trackFunnelStep,
47
+ useScreenTracking,
48
+ } from '../../Shared/Libs/analytics.utils';
42
49
 
43
50
  const VerificationSessionCheckScreen = () => {
44
51
  const [sessionCode, setSessionCode] = useState('');
@@ -51,9 +58,13 @@ const VerificationSessionCheckScreen = () => {
51
58
  const { t } = useTranslation();
52
59
  const navigationManagerRef = React.useRef<NavigationManagerRef>(null);
53
60
  const navigation = useNavigation();
54
- const theme = useTheme();
55
61
  const initialized = useRef(false);
56
62
  const insets = useSafeAreaInsets();
63
+ const theme = useTheme();
64
+ const primaryColor = theme.colors.primary;
65
+
66
+ // Track screen view and exit
67
+ useScreenTracking('verification_session_check');
57
68
 
58
69
  const apiUrl = useMemo(
59
70
  () => `${appContext.baseUrl}/api/app/mobile`,
@@ -71,17 +82,26 @@ const VerificationSessionCheckScreen = () => {
71
82
  `${apiUrl}/verification-sessions/${id}`,
72
83
  appContext.isDemoSession
73
84
  ? getSimulatedDemoData<VerificationSession | undefined, void>(
74
- 'GET_SESSION'
75
- )
85
+ 'GET_SESSION'
86
+ )
76
87
  : undefined
77
88
  );
78
89
  return response;
79
90
  } catch (error) {
80
91
  if (error instanceof NotFoundError) {
92
+ // User entered invalid session ID - expected user behavior, not actionable
81
93
  Alert.alert(
82
94
  t('general.error'),
83
95
  t('verificationSessionCheckScreen.noVerificationSessionFound')
84
96
  );
97
+ } else {
98
+ trackError(
99
+ 'SESSION_CHECK_ERROR',
100
+ error instanceof Error ? error.message : 'Unknown error',
101
+ 'verification_session_check',
102
+ 'high',
103
+ { recoverable: false, userAction: 'check_session' }
104
+ );
85
105
  }
86
106
  }
87
107
  },
@@ -92,12 +112,13 @@ const VerificationSessionCheckScreen = () => {
92
112
  async (inputCode: string) => {
93
113
  try {
94
114
  appContext.isDemoSession = isDemoSession(inputCode);
115
+ analyticsService.setDemoSession(appContext.isDemoSession);
95
116
  const response = await httpClient.get<{ id: string }>(
96
117
  `${apiUrl}/verification-sessions?sessionCode=${inputCode.toUpperCase()}`,
97
118
  appContext.isDemoSession
98
119
  ? getSimulatedDemoData<{ id: string } | undefined, void>(
99
- 'GET_SESSION_BY_CODE'
100
- )
120
+ 'GET_SESSION_BY_CODE'
121
+ )
101
122
  : undefined
102
123
  );
103
124
 
@@ -105,13 +126,25 @@ const VerificationSessionCheckScreen = () => {
105
126
  throw new NotFoundError();
106
127
  }
107
128
 
129
+ // Track successful session start as funnel step
130
+ trackFunnelStep('Session Started', 1, 5, undefined, true);
131
+
108
132
  return response;
109
133
  } catch (error) {
110
134
  if (error instanceof NotFoundError) {
135
+ // User entered invalid session code - expected user behavior, not actionable
111
136
  Alert.alert(
112
137
  t('general.error'),
113
138
  t('verificationSessionCheckScreen.noVerificationSessionFound')
114
139
  );
140
+ } else {
141
+ trackError(
142
+ 'SESSION_CODE_CHECK_ERROR',
143
+ error instanceof Error ? error.message : 'Unknown error',
144
+ 'verification_session_check',
145
+ 'high',
146
+ { recoverable: false, userAction: 'check_session_code' }
147
+ );
115
148
  }
116
149
  }
117
150
  },
@@ -131,11 +164,19 @@ const VerificationSessionCheckScreen = () => {
131
164
  return true;
132
165
  } catch (error) {
133
166
  if (error instanceof NotFoundError) {
167
+ // Session expired or invalid - expected behavior, not actionable
134
168
  Alert.alert(
135
169
  t('general.error'),
136
170
  t('verificationSessionCheckScreen.noVerificationSessionFound')
137
171
  );
138
172
  } else {
173
+ trackError(
174
+ 'VERIFICATION_CODE_SEND_FAILED',
175
+ 'Failed to send verification code',
176
+ 'verification_session_check',
177
+ 'high',
178
+ { recoverable: true, userAction: 'send_verification_code' }
179
+ );
139
180
  Alert.alert(
140
181
  t('general.error'),
141
182
  t('verificationSessionCheckScreen.cannotSendVerificationCode')
@@ -160,19 +201,30 @@ const VerificationSessionCheckScreen = () => {
160
201
  },
161
202
  appContext.isDemoSession
162
203
  ? getSimulatedDemoData<
163
- VerificationSession | undefined,
164
- { code: string }
165
- >('GET_VERIFIED_SESSION', { code: verificationCode })
204
+ VerificationSession | undefined,
205
+ { code: string }
206
+ >('GET_VERIFIED_SESSION', { code: verificationCode })
166
207
  : undefined
167
208
  );
168
209
 
169
210
  return response;
170
211
  } catch (error) {
171
212
  if (error instanceof NotFoundError) {
213
+ // Session expired or invalid - expected behavior, not actionable
172
214
  Alert.alert(
173
215
  t('general.error'),
174
216
  t('verificationSessionCheckScreen.noVerificationSessionFound')
175
217
  );
218
+ } else if (error instanceof BadRequestError) {
219
+ // Wrong OTP code - expected user behavior, not actionable
220
+ } else {
221
+ trackError(
222
+ 'VERIFIED_SESSION_CHECK_ERROR',
223
+ error instanceof Error ? error.message : 'Unknown error',
224
+ 'verification_session_check',
225
+ 'high',
226
+ { recoverable: false, userAction: 'check_verified_session' }
227
+ );
176
228
  }
177
229
  }
178
230
  },
@@ -216,9 +268,6 @@ const VerificationSessionCheckScreen = () => {
216
268
  appContext.workflowSteps = session?.workflowSteps;
217
269
  if (session?.branding) {
218
270
  appContext.branding = appContext.branding || session.branding;
219
- theme.colors.primary = appContext.branding.primaryColor;
220
- theme.colors.secondary = appContext.branding.secondaryColor;
221
- theme.colors.tertiary = appContext.branding.tertiaryColor;
222
271
  }
223
272
 
224
273
  if (session?.sendOTP) {
@@ -240,10 +289,6 @@ const VerificationSessionCheckScreen = () => {
240
289
  appContext.identificationInfo.sessionId,
241
290
  getSession,
242
291
  sendOTPCode,
243
- theme.colors,
244
- theme.colors.primary,
245
- theme.colors.secondary,
246
- theme.colors.tertiary,
247
292
  ]);
248
293
 
249
294
  return (
@@ -287,59 +332,57 @@ const VerificationSessionCheckScreen = () => {
287
332
  <Text style={styles.mainText}>
288
333
  {t('verificationSessionCheckScreen.mainText')}
289
334
  </Text>
290
- <View style={styles.inputContainer}>
291
- <TextInput
292
- mode="outlined"
293
- autoCapitalize="characters"
294
- placeholder=""
295
- outlineColor={theme.colors.primary}
296
- activeOutlineColor={theme.colors.primary}
297
- style={styles.sessionCodeTextInput}
298
- contentStyle={styles.sessionCodeInputWithText}
299
- onChangeText={async (text) => {
300
- const alphanumericText = text
301
- .toUpperCase()
302
- .replace(SESSION_CODE_CHARSET_PATTERN, '');
303
- if (alphanumericText.length <= 8) {
304
- setSessionCode(alphanumericText);
305
- if (
306
- validateSessionCode(alphanumericText) &&
307
- alphanumericText.length === 8
308
- ) {
309
- try {
310
- setIsCheckingSession(true);
311
- const session =
312
- await getSessionByCode(alphanumericText);
313
- if (session?.id) {
335
+ <StyledTextInput
336
+ autoCapitalize="characters"
337
+ placeholder={t(
338
+ 'verificationSessionCheckScreen.enterSessionCode'
339
+ )}
340
+ borderColor={primaryColor}
341
+ focusedBorderColor={primaryColor}
342
+ inputStyle={styles.sessionCodeTextInput}
343
+ placeholderStyle={styles.sessionCodeTextInputPlaceholder}
344
+ value={sessionCode.toUpperCase()}
345
+ onChangeText={async (text) => {
346
+ const alphanumericText = text
347
+ .toUpperCase()
348
+ .replace(SESSION_CODE_CHARSET_PATTERN, '');
349
+ if (alphanumericText.length <= 8) {
350
+ setSessionCode(alphanumericText);
351
+ if (
352
+ validateSessionCode(alphanumericText) &&
353
+ alphanumericText.length === 8
354
+ ) {
355
+ try {
356
+ setIsCheckingSession(true);
357
+ const session =
358
+ await getSessionByCode(alphanumericText);
359
+ if (session?.id) {
360
+ if (appContext.setSessionId) {
361
+ appContext.setSessionId(session.id);
362
+ } else {
314
363
  appContext.identificationInfo.sessionId =
315
364
  session.id;
316
- } else {
317
- setSessionCode('');
318
365
  }
319
- } catch {
366
+ } else {
320
367
  setSessionCode('');
321
- } finally {
322
- setTimeout(() => {
323
- setIsCheckingSession(false);
324
- }, 1000);
325
368
  }
369
+ } catch {
370
+ setSessionCode('');
371
+ } finally {
372
+ setTimeout(() => {
373
+ setIsCheckingSession(false);
374
+ }, 1000);
326
375
  }
327
376
  }
328
- }}
329
- value={sessionCode.toUpperCase()}
330
- maxLength={8}
331
- />
332
- {!sessionCode && (
333
- <Text style={styles.placeholderText}>
334
- {t('verificationSessionCheckScreen.enterSessionCode')}
335
- </Text>
336
- )}
337
- </View>
377
+ }
378
+ }}
379
+ maxLength={8}
380
+ />
338
381
  <View style={styles.horizontalLineContainer}>
339
382
  <View
340
383
  style={[
341
384
  styles.horizontalLine,
342
- { backgroundColor: theme.colors.primary },
385
+ { backgroundColor: primaryColor },
343
386
  ]}
344
387
  />
345
388
  <Text style={styles.mainText}>
@@ -348,7 +391,7 @@ const VerificationSessionCheckScreen = () => {
348
391
  <View
349
392
  style={[
350
393
  styles.horizontalLine,
351
- { backgroundColor: theme.colors.primary },
394
+ { backgroundColor: primaryColor },
352
395
  ]}
353
396
  />
354
397
  </View>
@@ -366,14 +409,12 @@ const VerificationSessionCheckScreen = () => {
366
409
  <Text style={styles.mainText}>
367
410
  {t('verificationSessionCheckScreen.codeText')}
368
411
  </Text>
369
- <TextInput
370
- mode="outlined"
412
+ <StyledTextInput
371
413
  autoFocus={true}
372
414
  placeholder=""
373
- outlineColor={theme.colors.primary}
374
- activeOutlineColor={theme.colors.primary}
375
- style={styles.otpCodeTextInput}
376
- contentStyle={styles.otpCodeInputWithText}
415
+ borderColor={primaryColor}
416
+ focusedBorderColor={primaryColor}
417
+ inputStyle={styles.otpCodeTextInput}
377
418
  keyboardType="number-pad"
378
419
  textContentType="oneTimeCode"
379
420
  autoComplete="sms-otp"
@@ -396,6 +437,7 @@ const VerificationSessionCheckScreen = () => {
396
437
  setIsCodeSent(false);
397
438
  navigationManagerRef.current?.navigateToNextStep();
398
439
  } else {
440
+ // User entered wrong OTP code - expected behavior, not actionable
399
441
  appContext.onError?.('Invalid OTP code');
400
442
  Alert.alert(
401
443
  t('general.error'),
@@ -464,7 +506,7 @@ const styles = StyleSheet.create({
464
506
  },
465
507
  horizontalLine: {
466
508
  flex: 1,
467
- height: 2,
509
+ height: 1,
468
510
  },
469
511
  loadingAnimation: {
470
512
  width: '100%',
@@ -533,40 +575,21 @@ const styles = StyleSheet.create({
533
575
  alignItems: 'center',
534
576
  position: 'absolute',
535
577
  },
536
- inputContainer: {
537
- position: 'relative',
538
- width: '100%',
539
- },
540
578
  sessionCodeTextInput: {
541
- backgroundColor: 'white',
542
- },
543
- sessionCodeInputWithText: {
544
579
  textTransform: 'uppercase',
545
580
  fontWeight: '800',
546
581
  fontSize: 28,
547
582
  textAlign: 'center',
548
583
  letterSpacing: 8,
549
584
  },
550
- placeholderText: {
551
- position: 'absolute',
552
- top: 0,
553
- left: 0,
554
- right: 0,
555
- height: '100%',
556
- lineHeight: 56,
557
- fontSize: 16,
558
- color: '#666',
559
- textAlign: 'center',
560
- pointerEvents: 'none',
561
- },
562
- sessionCodeInputPlaceholder: {
585
+ sessionCodeTextInputPlaceholder: {
563
586
  fontSize: 16,
587
+ fontWeight: '400',
588
+ letterSpacing: 0,
564
589
  textAlign: 'center',
590
+ textTransform: 'none',
565
591
  },
566
592
  otpCodeTextInput: {
567
- backgroundColor: 'white',
568
- },
569
- otpCodeInputWithText: {
570
593
  fontWeight: '800',
571
594
  fontSize: 28,
572
595
  textAlign: 'center',
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react';
2
2
  import { Alert, View, Text, Image, StyleSheet } from 'react-native';
3
3
  import NFCManager from 'react-native-nfc-manager';
4
+ import DeviceInfo from 'react-native-device-info';
4
5
  import { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
5
6
  import { eidReader } from '../EIDReader/eidReader';
6
7
  import NativeProgressBar from './NativeProgressBar';
@@ -11,6 +12,11 @@ import StyledButton from './StyledButton';
11
12
  import LottieView from 'lottie-react-native';
12
13
  import { useKeepAwake } from '../Libs/native-keep-awake.utils';
13
14
  import { speakWithDebounce } from '../Libs/tts.utils';
15
+ import {
16
+ trackEIDScanStart,
17
+ trackEIDScanComplete,
18
+ trackEIDScanFailed,
19
+ } from '../Libs/analytics.utils';
14
20
 
15
21
  interface eIDScannerProps {
16
22
  documentNumber: string;
@@ -47,12 +53,58 @@ const EIDScanner = ({
47
53
  const { t } = useTranslation();
48
54
  const appContext = React.useContext(AppContext);
49
55
  const [voiceGuidanceMessage, setVoiceGuidanceMessage] = useState<string>();
56
+ const [attemptNumber, setAttemptNumber] = useState<number>(0);
50
57
 
51
58
  const readNFC = useCallback(async () => {
59
+ const startTime = Date.now();
60
+ const currentAttempt = attemptNumber + 1;
61
+ setAttemptNumber(currentAttempt);
62
+
52
63
  setIsScanning(true);
53
64
  setIsScanned(false);
54
65
  setProgress(0);
55
66
 
67
+ // Track EID scan start using analytics helper
68
+ const docType =
69
+ (documentType as 'ID' | 'PASSPORT' | 'UNKNOWN') || 'UNKNOWN';
70
+
71
+ // If device doesn't support NFC or NFC is disabled, track and abort early
72
+ if (!hasNfc) {
73
+ await trackEIDScanFailed(
74
+ docType,
75
+ 'device_unsupported',
76
+ 'Device does not support NFC',
77
+ 0,
78
+ currentAttempt,
79
+ DeviceInfo.getModel(),
80
+ DeviceInfo.getSystemVersion()
81
+ ).catch(() => {});
82
+
83
+ // Stop scanning - user will see the not-supported UI
84
+ setIsScanning(false);
85
+ setProgress(0);
86
+ return;
87
+ }
88
+
89
+ if (!isEnabled) {
90
+ await trackEIDScanFailed(
91
+ docType,
92
+ 'not_enabled',
93
+ 'NFC is disabled on device',
94
+ 0,
95
+ currentAttempt,
96
+ DeviceInfo.getModel(),
97
+ DeviceInfo.getSystemVersion()
98
+ ).catch(() => {});
99
+
100
+ // Stop scanning - allow the UI to prompt user to enable NFC
101
+ setIsScanning(false);
102
+ setProgress(0);
103
+ return;
104
+ }
105
+
106
+ await trackEIDScanStart(docType, hasNfc, isEnabled, currentAttempt).catch(()=>{});
107
+
56
108
  try {
57
109
  if (documentNumber && dateOfBirth && dateOfExpiry) {
58
110
  const passportData = await eidReader(
@@ -64,23 +116,95 @@ const EIDScanner = ({
64
116
  }
65
117
  );
66
118
  if (passportData) {
119
+ const scanDuration = Date.now() - startTime;
67
120
  setDocumentFaceImage(passportData.faceImage);
68
121
  setDocumentMRZInfo(passportData.mrz);
69
122
  setDocumentFaceImageMimeType(passportData.mimeType);
70
123
  setIsScanned(true);
124
+
125
+ // Track successful EID scan
126
+ await trackEIDScanComplete(docType, scanDuration, currentAttempt).catch(()=>{});
71
127
  }
72
128
  } else {
129
+ const scanDuration = Date.now() - startTime;
130
+ // MRZ input issue — track as NFC scan failure (unknown error type for NFC-specific stream)
131
+ await trackEIDScanFailed(
132
+ docType,
133
+ 'unknown',
134
+ 'Invalid MRZ fields',
135
+ scanDuration,
136
+ currentAttempt,
137
+ DeviceInfo.getModel(),
138
+ DeviceInfo.getSystemVersion()
139
+ ).catch(() => {});
73
140
  Alert.alert(t('general.error'), t('eidScannerScreen.invalidMRZFields'));
74
141
  }
75
142
  } catch (error) {
76
- console.debug(error);
77
- // Ignore error
143
+ const scanDuration = Date.now() - startTime;
144
+ const errorMessage =
145
+ error instanceof Error ? error.message : 'Unknown error';
146
+
147
+ // Determine error type
148
+ let errorType:
149
+ | 'device_unsupported'
150
+ | 'not_enabled'
151
+ | 'reading_error'
152
+ | 'user_cancelled'
153
+ | 'timeout'
154
+ | 'unknown' = 'unknown';
155
+
156
+ if (
157
+ errorMessage.includes('cancelled') ||
158
+ errorMessage.includes('cancel')
159
+ ) {
160
+ errorType = 'user_cancelled';
161
+ } else if (
162
+ errorMessage.includes('timeout') ||
163
+ errorMessage.includes('time out')
164
+ ) {
165
+ errorType = 'timeout';
166
+ } else if (errorMessage.includes('not supported')) {
167
+ errorType = 'device_unsupported';
168
+ } else if (
169
+ errorMessage.includes('not enabled') ||
170
+ errorMessage.includes('disabled')
171
+ ) {
172
+ errorType = 'not_enabled';
173
+ } else if (
174
+ errorMessage.includes('read') ||
175
+ errorMessage.includes('communication')
176
+ ) {
177
+ errorType = 'reading_error';
178
+ }
179
+
180
+ // Track EID scan failure with detailed metadata
181
+ await trackEIDScanFailed(
182
+ docType,
183
+ errorType,
184
+ errorMessage,
185
+ scanDuration,
186
+ currentAttempt,
187
+ DeviceInfo.getModel(),
188
+ DeviceInfo.getSystemVersion()
189
+ ).catch(() => {});
190
+
191
+ console.debug('NFC scan error:', error);
192
+ // Ignore error - let user retry
78
193
  } finally {
79
194
  setIsScanning(false);
80
195
  setProgress(0);
81
196
  setHasGuideShown(false);
82
197
  }
83
- }, [documentNumber, dateOfBirth, dateOfExpiry, t]);
198
+ }, [
199
+ documentNumber,
200
+ dateOfBirth,
201
+ dateOfExpiry,
202
+ documentType,
203
+ hasNfc,
204
+ isEnabled,
205
+ attemptNumber,
206
+ t,
207
+ ]);
84
208
 
85
209
  const getFieldsFromMRZ = useCallback((mrz: MRZInfo) => {
86
210
  return {
@@ -103,8 +227,13 @@ const EIDScanner = ({
103
227
  setHasNFC(deviceIsSupported);
104
228
  isNFCSupported && isNFCSupported(deviceIsSupported);
105
229
 
230
+ // NFC not supported is a device limitation - not actionable by developers
231
+
106
232
  const deviceIsEnabled = await NFCManager.isEnabled();
107
233
  setIsEnabled(deviceIsEnabled);
234
+
235
+ // NFC disabled is a user setting - they can enable it in settings
236
+ // Not tracking as it's user-controllable
108
237
  };
109
238
 
110
239
  checkIsSupported();