@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
<
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
330
|
-
|
|
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:
|
|
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:
|
|
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
|
-
<
|
|
370
|
-
mode="outlined"
|
|
412
|
+
<StyledTextInput
|
|
371
413
|
autoFocus={true}
|
|
372
414
|
placeholder=""
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
}, [
|
|
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();
|