@trustchex/react-native-sdk 1.253.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 +41 -2
- package/lib/module/Shared/Components/EIDScanner.js +63 -3
- package/lib/module/Shared/Components/FaceCamera.js +69 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +4 -1
- package/lib/module/Shared/Components/NavigationManager.js +2 -0
- package/lib/module/Shared/Components/QrCodeScannerCamera.js +2 -0
- package/lib/module/Shared/Contexts/AppContext.js +3 -1
- 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 +5 -0
- package/lib/module/Trustchex.js +47 -4
- 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/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
- 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/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 -2
- 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 +65 -10
- package/src/Shared/Components/EIDScanner.tsx +132 -3
- package/src/Shared/Components/FaceCamera.tsx +77 -2
- package/src/Shared/Components/IdentityDocumentCamera.tsx +4 -4
- package/src/Shared/Components/NavigationManager.tsx +2 -0
- package/src/Shared/Components/QrCodeScannerCamera.tsx +2 -0
- package/src/Shared/Contexts/AppContext.ts +4 -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 +9 -0
- package/src/Trustchex.tsx +54 -2
- package/src/index.tsx +33 -0
|
@@ -10,6 +10,12 @@ import IdentityDocumentCamera, {
|
|
|
10
10
|
type DocumentScannedData,
|
|
11
11
|
} from '../../Shared/Components/IdentityDocumentCamera';
|
|
12
12
|
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import {
|
|
14
|
+
trackFunnelStep,
|
|
15
|
+
useScreenTracking,
|
|
16
|
+
trackVerificationStart,
|
|
17
|
+
trackVerificationComplete,
|
|
18
|
+
} from '../../Shared/Libs/analytics.utils';
|
|
13
19
|
|
|
14
20
|
const IdentityDocumentEIDScanningScreen = () => {
|
|
15
21
|
const appContext = useContext(AppContext);
|
|
@@ -33,6 +39,14 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
33
39
|
null
|
|
34
40
|
);
|
|
35
41
|
|
|
42
|
+
// Track screen view and exit
|
|
43
|
+
useScreenTracking('nfc_scanning');
|
|
44
|
+
|
|
45
|
+
// Track EID scan step started when component mounts
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
trackVerificationStart('IDENTITY_DOCUMENT_EID_SCAN');
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
36
50
|
useEffect(() => {
|
|
37
51
|
if (appContext.identificationInfo.scannedDocument?.mrzFields) {
|
|
38
52
|
const {
|
|
@@ -105,6 +119,8 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
105
119
|
if (allowedCountries && allowedCountries.length > 0) {
|
|
106
120
|
const countryCode = mrzFields.issuingState;
|
|
107
121
|
if (!allowedCountries.includes(countryCode as 'TUR')) {
|
|
122
|
+
// Workflow validation - user's document country not in allowed list
|
|
123
|
+
// Expected behavior, not a bug
|
|
108
124
|
Alert.alert(
|
|
109
125
|
t('general.error'),
|
|
110
126
|
t('general.countryNotAllowed')
|
|
@@ -120,6 +136,8 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
120
136
|
if (
|
|
121
137
|
!allowedDocumentTypes.includes(documentCode as 'I' | 'P')
|
|
122
138
|
) {
|
|
139
|
+
// Workflow validation - user's document type not in allowed list
|
|
140
|
+
// Expected behavior, not a bug
|
|
123
141
|
Alert.alert(
|
|
124
142
|
t('general.error'),
|
|
125
143
|
t('general.documentTypeNotAllowed')
|
|
@@ -130,6 +148,18 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
130
148
|
}
|
|
131
149
|
}
|
|
132
150
|
|
|
151
|
+
// Track successful EID step completion
|
|
152
|
+
trackVerificationComplete('IDENTITY_DOCUMENT_EID_SCAN', true, 1);
|
|
153
|
+
|
|
154
|
+
// Track successful NFC completion as funnel step
|
|
155
|
+
trackFunnelStep(
|
|
156
|
+
'NFC Scan Completed',
|
|
157
|
+
4,
|
|
158
|
+
5,
|
|
159
|
+
'liveness_detection',
|
|
160
|
+
true
|
|
161
|
+
);
|
|
162
|
+
|
|
133
163
|
navigationManagerRef.current?.navigateToNextStep();
|
|
134
164
|
}
|
|
135
165
|
}}
|
|
@@ -10,6 +10,12 @@ import NavigationManager, {
|
|
|
10
10
|
type NavigationManagerRef,
|
|
11
11
|
} from '../../Shared/Components/NavigationManager';
|
|
12
12
|
import { useTranslation } from 'react-i18next';
|
|
13
|
+
import {
|
|
14
|
+
trackFunnelStep,
|
|
15
|
+
useScreenTracking,
|
|
16
|
+
trackVerificationStart,
|
|
17
|
+
trackVerificationComplete,
|
|
18
|
+
} from '../../Shared/Libs/analytics.utils';
|
|
13
19
|
|
|
14
20
|
const IdentityDocumentScanningScreen = () => {
|
|
15
21
|
const [idFrontSideData, setIDFrontSideData] =
|
|
@@ -28,6 +34,14 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
28
34
|
null
|
|
29
35
|
);
|
|
30
36
|
|
|
37
|
+
// Track screen view and exit
|
|
38
|
+
useScreenTracking('document_scanning');
|
|
39
|
+
|
|
40
|
+
// Track document scan started when component mounts
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
trackVerificationStart('IDENTITY_DOCUMENT_SCAN');
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
31
45
|
useEffect(() => {
|
|
32
46
|
if (
|
|
33
47
|
((idFrontSideData && idBackSideData) || passportData) &&
|
|
@@ -71,6 +85,8 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
71
85
|
if (allowedCountries && allowedCountries.length > 0) {
|
|
72
86
|
const countryCode = mrzFields.issuingState;
|
|
73
87
|
if (!allowedCountries.includes(countryCode as 'TUR')) {
|
|
88
|
+
// Workflow validation - user's document country not in allowed list
|
|
89
|
+
// Expected behavior, not a bug
|
|
74
90
|
Alert.alert(t('general.error'), t('general.countryNotAllowed'));
|
|
75
91
|
appContext.onError?.(t('general.countryNotAllowed'));
|
|
76
92
|
navigationManagerRef.current?.reset();
|
|
@@ -81,6 +97,8 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
81
97
|
if (allowedDocumentTypes && allowedDocumentTypes.length > 0) {
|
|
82
98
|
const documentCode = mrzFields.documentCode;
|
|
83
99
|
if (!allowedDocumentTypes.includes(documentCode as 'I' | 'P')) {
|
|
100
|
+
// Workflow validation - user's document type not in allowed list
|
|
101
|
+
// Expected behavior, not a bug
|
|
84
102
|
Alert.alert(t('general.error'), t('general.documentTypeNotAllowed'));
|
|
85
103
|
appContext.onError?.(t('general.documentTypeNotAllowed'));
|
|
86
104
|
navigationManagerRef.current?.reset();
|
|
@@ -88,6 +106,18 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
88
106
|
}
|
|
89
107
|
}
|
|
90
108
|
|
|
109
|
+
// Track successful document scan completion
|
|
110
|
+
trackVerificationComplete('IDENTITY_DOCUMENT_SCAN', true, 1);
|
|
111
|
+
|
|
112
|
+
// Track successful document scan completion as funnel step
|
|
113
|
+
trackFunnelStep(
|
|
114
|
+
'Document Scan Completed',
|
|
115
|
+
2,
|
|
116
|
+
5,
|
|
117
|
+
'contract_acceptance',
|
|
118
|
+
true
|
|
119
|
+
);
|
|
120
|
+
|
|
91
121
|
setTimeout(
|
|
92
122
|
() => navigationManagerRef.current?.navigateToNextStep(),
|
|
93
123
|
1000
|
|
@@ -28,6 +28,12 @@ import { useTranslation } from 'react-i18next';
|
|
|
28
28
|
import StyledButton from '../../Shared/Components/StyledButton';
|
|
29
29
|
import LottieView from 'lottie-react-native';
|
|
30
30
|
import { speakWithDebounce } from '../../Shared/Libs/tts.utils';
|
|
31
|
+
import {
|
|
32
|
+
trackFunnelStep,
|
|
33
|
+
useScreenTracking,
|
|
34
|
+
trackVerificationStart,
|
|
35
|
+
trackVerificationComplete,
|
|
36
|
+
} from '../../Shared/Libs/analytics.utils';
|
|
31
37
|
|
|
32
38
|
const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
|
|
33
39
|
|
|
@@ -80,6 +86,9 @@ const LivenessDetectionScreen = () => {
|
|
|
80
86
|
const navigationManagerRef = React.useRef<NavigationManagerRef>(null);
|
|
81
87
|
const { t } = useTranslation();
|
|
82
88
|
const [isRecording, setIsRecording] = useState(false);
|
|
89
|
+
|
|
90
|
+
// Track screen view and exit
|
|
91
|
+
useScreenTracking('liveness_detection');
|
|
83
92
|
const [initialState, setInitialState] = useState<StateType>({
|
|
84
93
|
brightnessLow: false,
|
|
85
94
|
faceDetected: false,
|
|
@@ -170,20 +179,35 @@ const LivenessDetectionScreen = () => {
|
|
|
170
179
|
try {
|
|
171
180
|
await camera?.cancelRecording();
|
|
172
181
|
setIsRecording(false);
|
|
173
|
-
} catch (
|
|
174
|
-
//
|
|
182
|
+
} catch (error) {
|
|
183
|
+
// User cancelled recording - expected behavior, no need to track
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
setTimeout(() => {
|
|
187
|
+
// Track liveness check started
|
|
188
|
+
trackVerificationStart('LIVENESS_CHECK');
|
|
189
|
+
|
|
178
190
|
camera?.startRecording({
|
|
179
191
|
fileType: 'mp4',
|
|
180
192
|
videoCodec: 'h265',
|
|
181
193
|
onRecordingError() {
|
|
194
|
+
// Recording errors are retried automatically, no need to track them
|
|
182
195
|
setIsRecording(false);
|
|
183
196
|
},
|
|
184
197
|
onRecordingFinished(video) {
|
|
185
198
|
setTimeout(() => {
|
|
186
199
|
dispatch({ type: 'VIDEO_RECORDED', payload: video.path });
|
|
200
|
+
|
|
201
|
+
// Track liveness check completion
|
|
202
|
+
trackVerificationComplete('LIVENESS_CHECK', true, 1);
|
|
203
|
+
|
|
204
|
+
trackFunnelStep(
|
|
205
|
+
'Liveness Check Completed',
|
|
206
|
+
3,
|
|
207
|
+
5,
|
|
208
|
+
'document_scanning',
|
|
209
|
+
true
|
|
210
|
+
);
|
|
187
211
|
setIsRecording(false);
|
|
188
212
|
}, 500);
|
|
189
213
|
},
|
|
@@ -196,8 +220,9 @@ const LivenessDetectionScreen = () => {
|
|
|
196
220
|
try {
|
|
197
221
|
await camera?.stopRecording();
|
|
198
222
|
setIsRecording(false);
|
|
199
|
-
} catch (
|
|
200
|
-
//
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// Stop recording can fail due to race conditions when user navigates away
|
|
225
|
+
// This is expected behavior and not actionable
|
|
201
226
|
}
|
|
202
227
|
}, [camera]);
|
|
203
228
|
|
|
@@ -528,6 +553,7 @@ const LivenessDetectionScreen = () => {
|
|
|
528
553
|
<FaceCamera
|
|
529
554
|
onFacesDetected={onFacesDetected}
|
|
530
555
|
onCameraInitialized={setCamera}
|
|
556
|
+
previewRect={PREVIEW_RECT}
|
|
531
557
|
/>
|
|
532
558
|
<NativeCircularProgress
|
|
533
559
|
style={styles.circularProgress}
|
|
@@ -14,8 +14,18 @@ const QrCodeScanningScreen = () => {
|
|
|
14
14
|
const [bUrl, sId] = handleDeepLink({ url: data });
|
|
15
15
|
|
|
16
16
|
if (bUrl && sId) {
|
|
17
|
-
appContext.
|
|
18
|
-
|
|
17
|
+
if (appContext.setBaseUrl) {
|
|
18
|
+
appContext.setBaseUrl(bUrl);
|
|
19
|
+
} else {
|
|
20
|
+
appContext.baseUrl = bUrl;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (appContext.setSessionId) {
|
|
24
|
+
appContext.setSessionId(sId);
|
|
25
|
+
} else {
|
|
26
|
+
appContext.identificationInfo.sessionId = sId;
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
navigation.navigate('VerificationSessionCheckScreen' as never);
|
|
20
30
|
}
|
|
21
31
|
};
|
|
@@ -36,6 +36,16 @@ import { encryptWithAes, getSessionKey } from '../../Shared/Libs/crypto.utils';
|
|
|
36
36
|
import Video from 'react-native-video';
|
|
37
37
|
import StyledButton from '../../Shared/Components/StyledButton';
|
|
38
38
|
import NativeDeviceInfo from '../../Shared/Libs/native-device-info.utils';
|
|
39
|
+
import {
|
|
40
|
+
trackError,
|
|
41
|
+
trackFunnelStep,
|
|
42
|
+
useScreenTracking,
|
|
43
|
+
} from '../../Shared/Libs/analytics.utils';
|
|
44
|
+
import { analyticsService } from '../../Shared/Services/AnalyticsService';
|
|
45
|
+
import {
|
|
46
|
+
AnalyticsEventName,
|
|
47
|
+
AnalyticsEventCategory,
|
|
48
|
+
} from '../../Shared/Types/analytics.types';
|
|
39
49
|
|
|
40
50
|
const ResultScreen = () => {
|
|
41
51
|
const appContext = useContext(AppContext);
|
|
@@ -46,6 +56,9 @@ const ResultScreen = () => {
|
|
|
46
56
|
const [deviceIdentifier, setDeviceIdentifier] = useState<string>('');
|
|
47
57
|
const { t } = useTranslation();
|
|
48
58
|
|
|
59
|
+
// Track screen view and exit
|
|
60
|
+
useScreenTracking('result_screen');
|
|
61
|
+
|
|
49
62
|
const apiUrl = useMemo(
|
|
50
63
|
() => `${appContext.baseUrl}/api/app/mobile`,
|
|
51
64
|
[appContext.baseUrl]
|
|
@@ -197,7 +210,7 @@ const ResultScreen = () => {
|
|
|
197
210
|
files: [],
|
|
198
211
|
progress: (res) => {
|
|
199
212
|
setProgress(
|
|
200
|
-
|
|
213
|
+
60 + (res.totalBytesSent / res.totalBytesExpectedToSend) * 30
|
|
201
214
|
);
|
|
202
215
|
},
|
|
203
216
|
} satisfies RNFS.UploadFileOptions;
|
|
@@ -335,7 +348,7 @@ const ResultScreen = () => {
|
|
|
335
348
|
|
|
336
349
|
const livenessVideoPath = livenessDetection.videoPath;
|
|
337
350
|
if (livenessVideoPath) {
|
|
338
|
-
let videoFilePath;
|
|
351
|
+
let videoFilePath: string;
|
|
339
352
|
if (Platform.OS === 'ios') {
|
|
340
353
|
await RNFS.mkdir(
|
|
341
354
|
`${RNFS.TemporaryDirectoryPath}/${new Date().getTime()}`
|
|
@@ -345,11 +358,44 @@ const ResultScreen = () => {
|
|
|
345
358
|
videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
|
|
346
359
|
}
|
|
347
360
|
await RNFS.copyFile(livenessVideoPath, videoFilePath);
|
|
348
|
-
|
|
361
|
+
|
|
362
|
+
// Get original file size
|
|
363
|
+
const originalStats = await RNFS.stat(videoFilePath);
|
|
364
|
+
const originalSize = originalStats.size;
|
|
365
|
+
|
|
366
|
+
// Compress video with maximum compression settings
|
|
367
|
+
const compressedVideoPath = await VideoCompressor.compress(
|
|
368
|
+
videoFilePath,
|
|
369
|
+
{
|
|
370
|
+
compressionMethod: 'manual',
|
|
371
|
+
bitrate: 500000, // 500 kbps
|
|
372
|
+
maxSize: 1280, // HD 720p
|
|
373
|
+
minimumFileSizeForCompress: 0, // Always compress
|
|
374
|
+
},
|
|
375
|
+
(compressionProgress) => {
|
|
376
|
+
// Map compression progress (0-1) to main progress (40-60%)
|
|
377
|
+
setProgress(40 + compressionProgress * 20);
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Convert file:// URI to regular path for RNFS upload
|
|
382
|
+
const normalizedPath = compressedVideoPath.replace('file://', '');
|
|
383
|
+
|
|
384
|
+
// Get compressed file size and log difference
|
|
385
|
+
const compressedStats = await RNFS.stat(normalizedPath);
|
|
386
|
+
const compressedSize = compressedStats.size;
|
|
387
|
+
const compressionRatio = (
|
|
388
|
+
((originalSize - compressedSize) / originalSize) *
|
|
389
|
+
100
|
|
390
|
+
).toFixed(2);
|
|
391
|
+
console.log(
|
|
392
|
+
`Video compression: ${(originalSize / 1024 / 1024).toFixed(2)}MB -> ${(compressedSize / 1024 / 1024).toFixed(2)}MB (${compressionRatio}% reduction)`
|
|
393
|
+
);
|
|
394
|
+
|
|
349
395
|
uploadFileOptions.files.push({
|
|
350
396
|
name: 'files',
|
|
351
397
|
filename: 'LIVENESS_VIDEO.mp4',
|
|
352
|
-
filepath:
|
|
398
|
+
filepath: normalizedPath,
|
|
353
399
|
filetype: 'video/mp4',
|
|
354
400
|
});
|
|
355
401
|
}
|
|
@@ -408,15 +454,44 @@ const ResultScreen = () => {
|
|
|
408
454
|
livenessDetection
|
|
409
455
|
)
|
|
410
456
|
);
|
|
457
|
+
setProgress(90);
|
|
411
458
|
|
|
412
459
|
await runWithRetry(() =>
|
|
413
460
|
finishIdentification(identificationId, sessionKey)
|
|
414
461
|
);
|
|
415
462
|
setProgress(100);
|
|
416
463
|
setIsSubmitting(false);
|
|
464
|
+
|
|
465
|
+
// Track successful verification completion as final funnel step
|
|
466
|
+
trackFunnelStep('Verification Completed', 5, 5, 'nfc_scanning', true).catch(() => {});
|
|
467
|
+
analyticsService.trackEvent(
|
|
468
|
+
AnalyticsEventName.VERIFICATION_SUCCESS,
|
|
469
|
+
AnalyticsEventCategory.VERIFICATION,
|
|
470
|
+
{ sessionId: appContext.identificationInfo.sessionId || '' }
|
|
471
|
+
).catch(() => {});
|
|
417
472
|
} catch (error) {
|
|
418
473
|
setIsSubmitting(false);
|
|
419
474
|
setProgress(0);
|
|
475
|
+
|
|
476
|
+
// Track verification failure
|
|
477
|
+
const errorMessage =
|
|
478
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
479
|
+
trackError(
|
|
480
|
+
'VERIFICATION_SUBMISSION_FAILED',
|
|
481
|
+
errorMessage,
|
|
482
|
+
'result_screen',
|
|
483
|
+
'critical',
|
|
484
|
+
{ recoverable: false, userAction: 'submit_verification' }
|
|
485
|
+
).catch(() => {});
|
|
486
|
+
analyticsService.trackEvent(
|
|
487
|
+
AnalyticsEventName.VERIFICATION_FAILED,
|
|
488
|
+
AnalyticsEventCategory.VERIFICATION,
|
|
489
|
+
{
|
|
490
|
+
sessionId: appContext.identificationInfo.sessionId || '',
|
|
491
|
+
errorMessage,
|
|
492
|
+
}
|
|
493
|
+
).catch(() => {});
|
|
494
|
+
|
|
420
495
|
Alert.alert(t('general.error'), t('resultScreen.submissionFailed'));
|
|
421
496
|
appContext.onError?.(t('resultScreen.submissionFailed'));
|
|
422
497
|
navigationManagerRef.current?.reset();
|
|
@@ -20,7 +20,7 @@ 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, {
|
|
@@ -29,6 +29,7 @@ import NavigationManager, {
|
|
|
29
29
|
import type { VerificationSession } from '../../Shared/Types/verificationSession';
|
|
30
30
|
import StyledButton from '../../Shared/Components/StyledButton';
|
|
31
31
|
import StyledTextInput from '../../Shared/Components/StyledTextInput';
|
|
32
|
+
import { analyticsService } from '../../Shared/Services/AnalyticsService';
|
|
32
33
|
import { useTheme } from '../../Shared/Contexts/ThemeContext';
|
|
33
34
|
import LottieView from 'lottie-react-native';
|
|
34
35
|
import {
|
|
@@ -40,6 +41,11 @@ import {
|
|
|
40
41
|
SESSION_CODE_CHARSET_PATTERN,
|
|
41
42
|
SESSION_CODE_VALIDATION_PATTERN,
|
|
42
43
|
} from '../../Shared/Constants/validation.constants';
|
|
44
|
+
import {
|
|
45
|
+
trackError,
|
|
46
|
+
trackFunnelStep,
|
|
47
|
+
useScreenTracking,
|
|
48
|
+
} from '../../Shared/Libs/analytics.utils';
|
|
43
49
|
|
|
44
50
|
const VerificationSessionCheckScreen = () => {
|
|
45
51
|
const [sessionCode, setSessionCode] = useState('');
|
|
@@ -57,6 +63,9 @@ const VerificationSessionCheckScreen = () => {
|
|
|
57
63
|
const theme = useTheme();
|
|
58
64
|
const primaryColor = theme.colors.primary;
|
|
59
65
|
|
|
66
|
+
// Track screen view and exit
|
|
67
|
+
useScreenTracking('verification_session_check');
|
|
68
|
+
|
|
60
69
|
const apiUrl = useMemo(
|
|
61
70
|
() => `${appContext.baseUrl}/api/app/mobile`,
|
|
62
71
|
[appContext.baseUrl]
|
|
@@ -73,17 +82,26 @@ const VerificationSessionCheckScreen = () => {
|
|
|
73
82
|
`${apiUrl}/verification-sessions/${id}`,
|
|
74
83
|
appContext.isDemoSession
|
|
75
84
|
? getSimulatedDemoData<VerificationSession | undefined, void>(
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
'GET_SESSION'
|
|
86
|
+
)
|
|
78
87
|
: undefined
|
|
79
88
|
);
|
|
80
89
|
return response;
|
|
81
90
|
} catch (error) {
|
|
82
91
|
if (error instanceof NotFoundError) {
|
|
92
|
+
// User entered invalid session ID - expected user behavior, not actionable
|
|
83
93
|
Alert.alert(
|
|
84
94
|
t('general.error'),
|
|
85
95
|
t('verificationSessionCheckScreen.noVerificationSessionFound')
|
|
86
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
|
+
);
|
|
87
105
|
}
|
|
88
106
|
}
|
|
89
107
|
},
|
|
@@ -94,12 +112,13 @@ const VerificationSessionCheckScreen = () => {
|
|
|
94
112
|
async (inputCode: string) => {
|
|
95
113
|
try {
|
|
96
114
|
appContext.isDemoSession = isDemoSession(inputCode);
|
|
115
|
+
analyticsService.setDemoSession(appContext.isDemoSession);
|
|
97
116
|
const response = await httpClient.get<{ id: string }>(
|
|
98
117
|
`${apiUrl}/verification-sessions?sessionCode=${inputCode.toUpperCase()}`,
|
|
99
118
|
appContext.isDemoSession
|
|
100
119
|
? getSimulatedDemoData<{ id: string } | undefined, void>(
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
'GET_SESSION_BY_CODE'
|
|
121
|
+
)
|
|
103
122
|
: undefined
|
|
104
123
|
);
|
|
105
124
|
|
|
@@ -107,13 +126,25 @@ const VerificationSessionCheckScreen = () => {
|
|
|
107
126
|
throw new NotFoundError();
|
|
108
127
|
}
|
|
109
128
|
|
|
129
|
+
// Track successful session start as funnel step
|
|
130
|
+
trackFunnelStep('Session Started', 1, 5, undefined, true);
|
|
131
|
+
|
|
110
132
|
return response;
|
|
111
133
|
} catch (error) {
|
|
112
134
|
if (error instanceof NotFoundError) {
|
|
135
|
+
// User entered invalid session code - expected user behavior, not actionable
|
|
113
136
|
Alert.alert(
|
|
114
137
|
t('general.error'),
|
|
115
138
|
t('verificationSessionCheckScreen.noVerificationSessionFound')
|
|
116
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
|
+
);
|
|
117
148
|
}
|
|
118
149
|
}
|
|
119
150
|
},
|
|
@@ -133,11 +164,19 @@ const VerificationSessionCheckScreen = () => {
|
|
|
133
164
|
return true;
|
|
134
165
|
} catch (error) {
|
|
135
166
|
if (error instanceof NotFoundError) {
|
|
167
|
+
// Session expired or invalid - expected behavior, not actionable
|
|
136
168
|
Alert.alert(
|
|
137
169
|
t('general.error'),
|
|
138
170
|
t('verificationSessionCheckScreen.noVerificationSessionFound')
|
|
139
171
|
);
|
|
140
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
|
+
);
|
|
141
180
|
Alert.alert(
|
|
142
181
|
t('general.error'),
|
|
143
182
|
t('verificationSessionCheckScreen.cannotSendVerificationCode')
|
|
@@ -162,19 +201,30 @@ const VerificationSessionCheckScreen = () => {
|
|
|
162
201
|
},
|
|
163
202
|
appContext.isDemoSession
|
|
164
203
|
? getSimulatedDemoData<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
VerificationSession | undefined,
|
|
205
|
+
{ code: string }
|
|
206
|
+
>('GET_VERIFIED_SESSION', { code: verificationCode })
|
|
168
207
|
: undefined
|
|
169
208
|
);
|
|
170
209
|
|
|
171
210
|
return response;
|
|
172
211
|
} catch (error) {
|
|
173
212
|
if (error instanceof NotFoundError) {
|
|
213
|
+
// Session expired or invalid - expected behavior, not actionable
|
|
174
214
|
Alert.alert(
|
|
175
215
|
t('general.error'),
|
|
176
216
|
t('verificationSessionCheckScreen.noVerificationSessionFound')
|
|
177
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
|
+
);
|
|
178
228
|
}
|
|
179
229
|
}
|
|
180
230
|
},
|
|
@@ -307,8 +357,12 @@ const VerificationSessionCheckScreen = () => {
|
|
|
307
357
|
const session =
|
|
308
358
|
await getSessionByCode(alphanumericText);
|
|
309
359
|
if (session?.id) {
|
|
310
|
-
appContext.
|
|
311
|
-
session.id;
|
|
360
|
+
if (appContext.setSessionId) {
|
|
361
|
+
appContext.setSessionId(session.id);
|
|
362
|
+
} else {
|
|
363
|
+
appContext.identificationInfo.sessionId =
|
|
364
|
+
session.id;
|
|
365
|
+
}
|
|
312
366
|
} else {
|
|
313
367
|
setSessionCode('');
|
|
314
368
|
}
|
|
@@ -383,6 +437,7 @@ const VerificationSessionCheckScreen = () => {
|
|
|
383
437
|
setIsCodeSent(false);
|
|
384
438
|
navigationManagerRef.current?.navigateToNextStep();
|
|
385
439
|
} else {
|
|
440
|
+
// User entered wrong OTP code - expected behavior, not actionable
|
|
386
441
|
appContext.onError?.('Invalid OTP code');
|
|
387
442
|
Alert.alert(
|
|
388
443
|
t('general.error'),
|