@trustchex/react-native-sdk 1.253.0 → 1.266.1

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 (88) 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 +41 -2
  13. package/lib/module/Shared/Components/EIDScanner.js +63 -3
  14. package/lib/module/Shared/Components/FaceCamera.js +69 -4
  15. package/lib/module/Shared/Components/IdentityDocumentCamera.js +4 -1
  16. package/lib/module/Shared/Components/NavigationManager.js +2 -0
  17. package/lib/module/Shared/Components/QrCodeScannerCamera.js +2 -0
  18. package/lib/module/Shared/Contexts/AppContext.js +3 -1
  19. package/lib/module/Shared/Libs/analytics.utils.js +430 -0
  20. package/lib/module/Shared/Libs/camera.utils.js +58 -2
  21. package/lib/module/Shared/Libs/deeplink.utils.js +8 -0
  22. package/lib/module/Shared/Libs/http-client.js +89 -28
  23. package/lib/module/Shared/Services/AnalyticsService.js +404 -0
  24. package/lib/module/Shared/Types/analytics.types.js +111 -0
  25. package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +1 -0
  26. package/lib/module/Translation/index.js +5 -0
  27. package/lib/module/Trustchex.js +47 -4
  28. package/lib/module/index.js +3 -0
  29. package/lib/module/version.js +5 -0
  30. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  31. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  32. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  36. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +7 -1
  39. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  41. package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +2 -0
  43. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts +98 -0
  45. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -0
  46. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +19 -1
  47. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +1 -1
  48. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  50. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts +86 -0
  51. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -0
  52. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +146 -0
  53. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -0
  54. package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +1 -1
  55. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  56. package/lib/typescript/src/Translation/index.d.ts.map +1 -1
  57. package/lib/typescript/src/Trustchex.d.ts +1 -0
  58. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  59. package/lib/typescript/src/index.d.ts +4 -0
  60. package/lib/typescript/src/index.d.ts.map +1 -1
  61. package/lib/typescript/src/version.d.ts +2 -0
  62. package/lib/typescript/src/version.d.ts.map +1 -0
  63. package/package.json +6 -2
  64. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +35 -1
  65. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +30 -0
  66. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +30 -0
  67. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +30 -4
  68. package/src/Screens/Static/QrCodeScanningScreen.tsx +12 -2
  69. package/src/Screens/Static/ResultScreen.tsx +79 -4
  70. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +65 -10
  71. package/src/Shared/Components/EIDScanner.tsx +132 -3
  72. package/src/Shared/Components/FaceCamera.tsx +77 -2
  73. package/src/Shared/Components/IdentityDocumentCamera.tsx +4 -4
  74. package/src/Shared/Components/NavigationManager.tsx +2 -0
  75. package/src/Shared/Components/QrCodeScannerCamera.tsx +2 -0
  76. package/src/Shared/Contexts/AppContext.ts +4 -0
  77. package/src/Shared/Libs/analytics.utils.ts +644 -0
  78. package/src/Shared/Libs/camera.utils.ts +74 -2
  79. package/src/Shared/Libs/deeplink.utils.ts +5 -0
  80. package/src/Shared/Libs/http-client.ts +105 -31
  81. package/src/Shared/Services/AnalyticsService.ts +470 -0
  82. package/src/Shared/Types/analytics.types.ts +179 -0
  83. package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +1 -0
  84. package/src/Translation/Resources/tr.ts +2 -1
  85. package/src/Translation/index.ts +9 -0
  86. package/src/Trustchex.tsx +54 -2
  87. package/src/index.tsx +33 -0
  88. package/src/version.ts +3 -0
package/README.md CHANGED
@@ -13,6 +13,7 @@ React Native SDK for KYC and identity verification with document scanning, facia
13
13
  - 🌐 Multi-language support (English, Turkish)
14
14
  - 🎨 Customizable branding
15
15
  - 🔗 Deep link support
16
+ - 📊 **Privacy-compliant analytics** (GDPR, KVKK, PCI-DSS)
16
17
 
17
18
  ## Installation
18
19
 
@@ -33,7 +34,6 @@ npm install @react-native-community/image-editor@^4.3.0 \
33
34
  react-native-gesture-handler@^2.27.1 \
34
35
  react-native-get-random-values@^1.11.0 \
35
36
  react-native-nfc-manager@^3.16.2 \
36
- react-native-paper@^5.14.5 \
37
37
  react-native-safe-area-context@^5.5.2 \
38
38
  react-native-screens@^4.13.1 \
39
39
  react-native-svg@^15.12.0 \
@@ -53,7 +53,6 @@ module.exports = {
53
53
  presets: ['module:@react-native/babel-preset'],
54
54
  plugins: [
55
55
  'react-native-worklets-core/plugin',
56
- 'react-native-paper/babel',
57
56
  ],
58
57
  };
59
58
  ```
@@ -182,11 +181,29 @@ import Trustchex from '@trustchex/react-native-sdk';
182
181
  tertiaryColor: '#EF4444'
183
182
  }}
184
183
  locale="en" // 'en' | 'tr'
184
+ enableAnalytics={true}
185
185
  onCompleted={() => console.log('Completed')}
186
186
  onError={(error) => console.error(error)}
187
187
  />
188
188
  ```
189
189
 
190
+ ### With Custom Analytics Tracking
191
+
192
+ ```tsx
193
+ import {
194
+ trackButtonClick,
195
+ trackScreenView,
196
+ analyticsService
197
+ } from '@trustchex/react-native-sdk';
198
+
199
+ // Track custom events
200
+ await trackButtonClick('custom_button', 'custom_screen');
201
+ await trackScreenView('custom_screen');
202
+
203
+ // Access analytics service directly
204
+ await analyticsService.clear(); // Right to be forgotten
205
+ ```
206
+
190
207
  ### Deep Links
191
208
 
192
209
  ```tsx
@@ -205,9 +222,33 @@ const [baseUrl, sessionId] = handleDeepLink({
205
222
  | `sessionId` | `string?` | Verification session ID |
206
223
  | `branding` | `object?` | Theme colors and logo |
207
224
  | `locale` | `'en' \| 'tr'?` | UI language |
225
+ | `enableAnalytics` | `boolean?` | Enable analytics (default: `true`) |
208
226
  | `onCompleted` | `function?` | Success callback |
209
227
  | `onError` | `function?` | Error callback |
210
228
 
229
+ ## Analytics
230
+
231
+ The SDK includes **privacy-compliant analytics** that respects GDPR, KVKK, and PCI-DSS regulations.
232
+
233
+ ### Key Features
234
+
235
+ - ✅ **No PII collected** - No names, emails, addresses, or personal data
236
+ - ✅ **Data anonymization** - Device identifiers excluded
237
+ - ✅ **Right to deletion** - Users can clear their data
238
+
239
+ ### Usage
240
+
241
+ Analytics is enabled by default. To customize:
242
+
243
+ ```tsx
244
+ <Trustchex
245
+ enableAnalytics={true} // Enable/disable analytics
246
+ {...otherProps}
247
+ />
248
+ ```
249
+
250
+ For complete analytics documentation, see [ANALYTICS.md](./ANALYTICS.md).
251
+
211
252
  ## Requirements
212
253
 
213
254
  - React Native ≥ 0.79.5
@@ -0,0 +1,66 @@
1
+ package com.trustchex.reactnativesdk
2
+
3
+ import android.app.Activity
4
+ import android.provider.Settings
5
+ import android.view.WindowManager
6
+ import com.facebook.react.bridge.Promise
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
9
+ import com.facebook.react.bridge.ReactMethod
10
+
11
+ class DeviceBrightnessModule(reactContext: ReactApplicationContext) :
12
+ ReactContextBaseJavaModule(reactContext) {
13
+
14
+ override fun getName(): String {
15
+ return "DeviceBrightness"
16
+ }
17
+
18
+ @ReactMethod
19
+ fun getBrightness(promise: Promise) {
20
+ try {
21
+ val activity: Activity? = currentActivity
22
+ if (activity == null) {
23
+ promise.reject("ERROR", "Activity is null")
24
+ return
25
+ }
26
+
27
+ val brightness = activity.window.attributes.screenBrightness
28
+ if (brightness < 0) {
29
+ // Return system brightness if window brightness is not set
30
+ val systemBrightness = Settings.System.getInt(
31
+ activity.contentResolver,
32
+ Settings.System.SCREEN_BRIGHTNESS
33
+ ) / 255.0f
34
+ promise.resolve(systemBrightness.toDouble())
35
+ } else {
36
+ promise.resolve(brightness.toDouble())
37
+ }
38
+ } catch (e: Exception) {
39
+ promise.reject("ERROR", e.message)
40
+ }
41
+ }
42
+
43
+ @ReactMethod
44
+ fun setBrightness(brightness: Double, promise: Promise) {
45
+ try {
46
+ val activity: Activity? = currentActivity
47
+ if (activity == null) {
48
+ promise.reject("ERROR", "Activity is null")
49
+ return
50
+ }
51
+
52
+ activity.runOnUiThread {
53
+ try {
54
+ val layoutParams = activity.window.attributes
55
+ layoutParams.screenBrightness = brightness.toFloat()
56
+ activity.window.attributes = layoutParams
57
+ promise.resolve(null)
58
+ } catch (e: Exception) {
59
+ promise.reject("ERROR", e.message)
60
+ }
61
+ }
62
+ } catch (e: Exception) {
63
+ promise.reject("ERROR", e.message)
64
+ }
65
+ }
66
+ }
@@ -36,6 +36,7 @@ class TrustchexSDKPackage : BaseReactPackage() {
36
36
  override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
37
37
  return when (name) {
38
38
  TrustchexSDKModule.NAME -> TrustchexSDKModule(reactContext)
39
+ "DeviceBrightness" -> DeviceBrightnessModule(reactContext)
39
40
  else -> null
40
41
  }
41
42
  }
@@ -55,6 +56,17 @@ class TrustchexSDKPackage : BaseReactPackage() {
55
56
  true // isTurboModule
56
57
  )
57
58
 
59
+ // Device brightness module
60
+ moduleInfos["DeviceBrightness"] =
61
+ ReactModuleInfo(
62
+ "DeviceBrightness",
63
+ "DeviceBrightness",
64
+ false, // canOverrideExistingModule
65
+ false, // needsEagerInit
66
+ false, // isCxxModule
67
+ false // isTurboModule
68
+ )
69
+
58
70
  moduleInfos
59
71
  }
60
72
  }
@@ -0,0 +1,4 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface DeviceBrightnessModule : NSObject <RCTBridgeModule>
4
+ @end
@@ -0,0 +1,27 @@
1
+ #import "DeviceBrightnessModule.h"
2
+ #import <UIKit/UIKit.h>
3
+
4
+ @implementation DeviceBrightnessModule
5
+
6
+ RCT_EXPORT_MODULE(DeviceBrightness);
7
+
8
+ RCT_EXPORT_METHOD(getBrightness:(RCTPromiseResolveBlock)resolve
9
+ rejecter:(RCTPromiseRejectBlock)reject)
10
+ {
11
+ dispatch_async(dispatch_get_main_queue(), ^{
12
+ CGFloat brightness = [UIScreen mainScreen].brightness;
13
+ resolve(@(brightness));
14
+ });
15
+ }
16
+
17
+ RCT_EXPORT_METHOD(setBrightness:(double)brightness
18
+ resolver:(RCTPromiseResolveBlock)resolve
19
+ rejecter:(RCTPromiseRejectBlock)reject)
20
+ {
21
+ dispatch_async(dispatch_get_main_queue(), ^{
22
+ [UIScreen mainScreen].brightness = brightness;
23
+ resolve(nil);
24
+ });
25
+ }
26
+
27
+ @end
@@ -9,6 +9,7 @@ import { getI18n, useTranslation } from 'react-i18next';
9
9
  import StyledButton from "../../Shared/Components/StyledButton.js";
10
10
  import NativeDeviceInfo from "../../Shared/Libs/native-device-info.utils.js";
11
11
  import { speakWithDebounce } from "../../Shared/Libs/tts.utils.js";
12
+ import { trackFunnelStep, useScreenTracking, trackConsentGiven, trackVerificationStart, trackVerificationComplete, trackError } from "../../Shared/Libs/analytics.utils.js";
12
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
14
  const ContractAcceptanceScreen = () => {
14
15
  const [isEnabled, setIsEnabled] = useState(false);
@@ -21,6 +22,9 @@ const ContractAcceptanceScreen = () => {
21
22
  const {
22
23
  t
23
24
  } = useTranslation();
25
+
26
+ // Track screen view and exit
27
+ useScreenTracking('contract_acceptance');
24
28
  useEffect(() => {
25
29
  const contracts = appContext.currentWorkflowStep?.data?.contracts;
26
30
  if (!contracts) {
@@ -70,9 +74,21 @@ const ContractAcceptanceScreen = () => {
70
74
  scalesPageToFit: false,
71
75
  scrollEnabled: true,
72
76
  style: styles.webView,
77
+ onError: syntheticEvent => {
78
+ const {
79
+ nativeEvent
80
+ } = syntheticEvent;
81
+ trackError('CONTRACT_WEBVIEW_ERROR', nativeEvent.description || 'Failed to load contract', 'contract_acceptance', 'medium', {
82
+ recoverable: true,
83
+ userAction: 'load_contract'
84
+ });
85
+ },
73
86
  onLoadEnd: event => {
74
87
  if (event.nativeEvent.url === contractUrl) {
75
88
  setIsReady(true);
89
+
90
+ // Track contract acceptance started
91
+ trackVerificationStart('CONTRACT_ACCEPTANCE');
76
92
  if (appContext.currentWorkflowStep?.data?.voiceGuidanceActive) {
77
93
  speakWithDebounce(t('termsOfUseAndDataPrivacyScreen.footerText'));
78
94
  }
@@ -107,6 +123,15 @@ const ContractAcceptanceScreen = () => {
107
123
  // Update the context with the new device info
108
124
  appContext.identificationInfo.consent = consent;
109
125
  }
126
+
127
+ // Track contract acceptance completed
128
+ trackVerificationComplete('CONTRACT_ACCEPTANCE', true, 1);
129
+
130
+ // Track contract acceptance as funnel event
131
+ trackConsentGiven(consent.contractIds, 'contract_acceptance');
132
+
133
+ // Track contract acceptance as funnel step
134
+ trackFunnelStep('Contract Accepted', 1, 5, 'session_start', true);
110
135
  navigationManagerRef.current?.navigateToNextStep();
111
136
  },
112
137
  children: t('termsOfUseAndDataPrivacyScreen.acceptAndContinue')
@@ -7,6 +7,7 @@ import NavigationManager from "../../Shared/Components/NavigationManager.js";
7
7
  import AppContext from "../../Shared/Contexts/AppContext.js";
8
8
  import IdentityDocumentCamera from "../../Shared/Components/IdentityDocumentCamera.js";
9
9
  import { useTranslation } from 'react-i18next';
10
+ import { trackFunnelStep, useScreenTracking, trackVerificationStart, trackVerificationComplete } from "../../Shared/Libs/analytics.utils.js";
10
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
12
  const IdentityDocumentEIDScanningScreen = () => {
12
13
  const appContext = useContext(AppContext);
@@ -24,6 +25,14 @@ const IdentityDocumentEIDScanningScreen = () => {
24
25
  } = useTranslation();
25
26
  const [allowedDocumentTypes, setAllowedDocumentTypes] = useState(null);
26
27
  const [allowedCountries, setAllowedCountries] = useState(null);
28
+
29
+ // Track screen view and exit
30
+ useScreenTracking('nfc_scanning');
31
+
32
+ // Track EID scan step started when component mounts
33
+ useEffect(() => {
34
+ trackVerificationStart('IDENTITY_DOCUMENT_EID_SCAN');
35
+ }, []);
27
36
  useEffect(() => {
28
37
  if (appContext.identificationInfo.scannedDocument?.mrzFields) {
29
38
  const {
@@ -73,6 +82,8 @@ const IdentityDocumentEIDScanningScreen = () => {
73
82
  if (allowedCountries && allowedCountries.length > 0) {
74
83
  const countryCode = mrzFields.issuingState;
75
84
  if (!allowedCountries.includes(countryCode)) {
85
+ // Workflow validation - user's document country not in allowed list
86
+ // Expected behavior, not a bug
76
87
  Alert.alert(t('general.error'), t('general.countryNotAllowed'));
77
88
  appContext.onError?.(t('general.countryNotAllowed'));
78
89
  navigationManagerRef.current?.reset();
@@ -82,12 +93,20 @@ const IdentityDocumentEIDScanningScreen = () => {
82
93
  if (allowedDocumentTypes && allowedDocumentTypes.length > 0) {
83
94
  const documentCode = mrzFields.documentCode;
84
95
  if (!allowedDocumentTypes.includes(documentCode)) {
96
+ // Workflow validation - user's document type not in allowed list
97
+ // Expected behavior, not a bug
85
98
  Alert.alert(t('general.error'), t('general.documentTypeNotAllowed'));
86
99
  appContext.onError?.(t('general.documentTypeNotAllowed'));
87
100
  navigationManagerRef.current?.reset();
88
101
  return;
89
102
  }
90
103
  }
104
+
105
+ // Track successful EID step completion
106
+ trackVerificationComplete('IDENTITY_DOCUMENT_EID_SCAN', true, 1);
107
+
108
+ // Track successful NFC completion as funnel step
109
+ trackFunnelStep('NFC Scan Completed', 4, 5, 'liveness_detection', true);
91
110
  navigationManagerRef.current?.navigateToNextStep();
92
111
  }
93
112
  },
@@ -6,6 +6,7 @@ import IdentityDocumentCamera from "../../Shared/Components/IdentityDocumentCame
6
6
  import AppContext from "../../Shared/Contexts/AppContext.js";
7
7
  import NavigationManager from "../../Shared/Components/NavigationManager.js";
8
8
  import { useTranslation } from 'react-i18next';
9
+ import { trackFunnelStep, useScreenTracking, trackVerificationStart, trackVerificationComplete } from "../../Shared/Libs/analytics.utils.js";
9
10
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
11
  const IdentityDocumentScanningScreen = () => {
11
12
  const [idFrontSideData, setIDFrontSideData] = useState(null);
@@ -18,6 +19,14 @@ const IdentityDocumentScanningScreen = () => {
18
19
  } = useTranslation();
19
20
  const [allowedDocumentTypes, setAllowedDocumentTypes] = useState(null);
20
21
  const [allowedCountries, setAllowedCountries] = useState(null);
22
+
23
+ // Track screen view and exit
24
+ useScreenTracking('document_scanning');
25
+
26
+ // Track document scan started when component mounts
27
+ useEffect(() => {
28
+ trackVerificationStart('IDENTITY_DOCUMENT_SCAN');
29
+ }, []);
21
30
  useEffect(() => {
22
31
  if ((idFrontSideData && idBackSideData || passportData) && appContext.identificationInfo && !appContext.identificationInfo.scannedDocument) {
23
32
  appContext.identificationInfo.scannedDocument = {
@@ -40,6 +49,8 @@ const IdentityDocumentScanningScreen = () => {
40
49
  if (allowedCountries && allowedCountries.length > 0) {
41
50
  const countryCode = mrzFields.issuingState;
42
51
  if (!allowedCountries.includes(countryCode)) {
52
+ // Workflow validation - user's document country not in allowed list
53
+ // Expected behavior, not a bug
43
54
  Alert.alert(t('general.error'), t('general.countryNotAllowed'));
44
55
  appContext.onError?.(t('general.countryNotAllowed'));
45
56
  navigationManagerRef.current?.reset();
@@ -49,12 +60,20 @@ const IdentityDocumentScanningScreen = () => {
49
60
  if (allowedDocumentTypes && allowedDocumentTypes.length > 0) {
50
61
  const documentCode = mrzFields.documentCode;
51
62
  if (!allowedDocumentTypes.includes(documentCode)) {
63
+ // Workflow validation - user's document type not in allowed list
64
+ // Expected behavior, not a bug
52
65
  Alert.alert(t('general.error'), t('general.documentTypeNotAllowed'));
53
66
  appContext.onError?.(t('general.documentTypeNotAllowed'));
54
67
  navigationManagerRef.current?.reset();
55
68
  return;
56
69
  }
57
70
  }
71
+
72
+ // Track successful document scan completion
73
+ trackVerificationComplete('IDENTITY_DOCUMENT_SCAN', true, 1);
74
+
75
+ // Track successful document scan completion as funnel step
76
+ trackFunnelStep('Document Scan Completed', 2, 5, 'contract_acceptance', true);
58
77
  setTimeout(() => navigationManagerRef.current?.navigateToNextStep(), 1000);
59
78
  }
60
79
  }, [idFrontSideData, idBackSideData, passportData, appContext.identificationInfo, allowedCountries, allowedDocumentTypes, t, appContext]);
@@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next';
13
13
  import StyledButton from "../../Shared/Components/StyledButton.js";
14
14
  import LottieView from 'lottie-react-native';
15
15
  import { speakWithDebounce } from "../../Shared/Libs/tts.utils.js";
16
+ import { trackFunnelStep, useScreenTracking, trackVerificationStart, trackVerificationComplete } from "../../Shared/Libs/analytics.utils.js";
16
17
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
17
18
  const {
18
19
  width: windowWidth,
@@ -37,6 +38,9 @@ const LivenessDetectionScreen = () => {
37
38
  t
38
39
  } = useTranslation();
39
40
  const [isRecording, setIsRecording] = useState(false);
41
+
42
+ // Track screen view and exit
43
+ useScreenTracking('liveness_detection');
40
44
  const [initialState, setInitialState] = useState({
41
45
  brightnessLow: false,
42
46
  faceDetected: false,
@@ -109,15 +113,18 @@ const LivenessDetectionScreen = () => {
109
113
  try {
110
114
  await camera?.cancelRecording();
111
115
  setIsRecording(false);
112
- } catch (e) {
113
- // Ignore error
116
+ } catch (error) {
117
+ // User cancelled recording - expected behavior, no need to track
114
118
  }
115
119
  }
116
120
  setTimeout(() => {
121
+ // Track liveness check started
122
+ trackVerificationStart('LIVENESS_CHECK');
117
123
  camera?.startRecording({
118
124
  fileType: 'mp4',
119
125
  videoCodec: 'h265',
120
126
  onRecordingError() {
127
+ // Recording errors are retried automatically, no need to track them
121
128
  setIsRecording(false);
122
129
  },
123
130
  onRecordingFinished(video) {
@@ -126,6 +133,10 @@ const LivenessDetectionScreen = () => {
126
133
  type: 'VIDEO_RECORDED',
127
134
  payload: video.path
128
135
  });
136
+
137
+ // Track liveness check completion
138
+ trackVerificationComplete('LIVENESS_CHECK', true, 1);
139
+ trackFunnelStep('Liveness Check Completed', 3, 5, 'document_scanning', true);
129
140
  setIsRecording(false);
130
141
  }, 500);
131
142
  }
@@ -137,8 +148,9 @@ const LivenessDetectionScreen = () => {
137
148
  try {
138
149
  await camera?.stopRecording();
139
150
  setIsRecording(false);
140
- } catch (e) {
141
- // Ignore error
151
+ } catch (error) {
152
+ // Stop recording can fail due to race conditions when user navigates away
153
+ // This is expected behavior and not actionable
142
154
  }
143
155
  }, [camera]);
144
156
  const areEyesOpen = face => face.leftEyeOpenProbability >= 0.8 && face.rightEyeOpenProbability >= 0.8;
@@ -425,7 +437,8 @@ const LivenessDetectionScreen = () => {
425
437
  }) : /*#__PURE__*/_jsxs(_Fragment, {
426
438
  children: [/*#__PURE__*/_jsx(FaceCamera, {
427
439
  onFacesDetected: onFacesDetected,
428
- onCameraInitialized: setCamera
440
+ onCameraInitialized: setCamera,
441
+ previewRect: PREVIEW_RECT
429
442
  }), /*#__PURE__*/_jsx(NativeCircularProgress, {
430
443
  style: styles.circularProgress,
431
444
  size: PREVIEW_SIZE,
@@ -15,8 +15,16 @@ const QrCodeScanningScreen = () => {
15
15
  url: data
16
16
  });
17
17
  if (bUrl && sId) {
18
- appContext.baseUrl = bUrl;
19
- appContext.identificationInfo.sessionId = sId;
18
+ if (appContext.setBaseUrl) {
19
+ appContext.setBaseUrl(bUrl);
20
+ } else {
21
+ appContext.baseUrl = bUrl;
22
+ }
23
+ if (appContext.setSessionId) {
24
+ appContext.setSessionId(sId);
25
+ } else {
26
+ appContext.identificationInfo.sessionId = sId;
27
+ }
20
28
  navigation.navigate('VerificationSessionCheckScreen');
21
29
  }
22
30
  };
@@ -16,6 +16,9 @@ import { encryptWithAes, getSessionKey } from "../../Shared/Libs/crypto.utils.js
16
16
  import Video from 'react-native-video';
17
17
  import StyledButton from "../../Shared/Components/StyledButton.js";
18
18
  import NativeDeviceInfo from "../../Shared/Libs/native-device-info.utils.js";
19
+ import { trackError, trackFunnelStep, useScreenTracking } from "../../Shared/Libs/analytics.utils.js";
20
+ import { analyticsService } from "../../Shared/Services/AnalyticsService.js";
21
+ import { AnalyticsEventName, AnalyticsEventCategory } from "../../Shared/Types/analytics.types.js";
19
22
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
20
23
  const ResultScreen = () => {
21
24
  const appContext = useContext(AppContext);
@@ -27,6 +30,9 @@ const ResultScreen = () => {
27
30
  const {
28
31
  t
29
32
  } = useTranslation();
33
+
34
+ // Track screen view and exit
35
+ useScreenTracking('result_screen');
30
36
  const apiUrl = useMemo(() => `${appContext.baseUrl}/api/app/mobile`, [appContext.baseUrl]);
31
37
  useEffect(() => {
32
38
  if (appContext.isDemoSession) {
@@ -117,7 +123,7 @@ const ResultScreen = () => {
117
123
  },
118
124
  files: [],
119
125
  progress: res => {
120
- setProgress(40 + res.totalBytesSent / res.totalBytesExpectedToSend * 50);
126
+ setProgress(60 + res.totalBytesSent / res.totalBytesExpectedToSend * 30);
121
127
  }
122
128
  };
123
129
  const frontImage = scannedIdentityDocument?.frontImage;
@@ -202,11 +208,36 @@ const ResultScreen = () => {
202
208
  videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
203
209
  }
204
210
  await RNFS.copyFile(livenessVideoPath, videoFilePath);
205
- await VideoCompressor.compress(videoFilePath);
211
+
212
+ // Get original file size
213
+ const originalStats = await RNFS.stat(videoFilePath);
214
+ const originalSize = originalStats.size;
215
+
216
+ // Compress video with maximum compression settings
217
+ const compressedVideoPath = await VideoCompressor.compress(videoFilePath, {
218
+ compressionMethod: 'manual',
219
+ bitrate: 500000,
220
+ // 500 kbps
221
+ maxSize: 1280,
222
+ // HD 720p
223
+ minimumFileSizeForCompress: 0 // Always compress
224
+ }, compressionProgress => {
225
+ // Map compression progress (0-1) to main progress (40-60%)
226
+ setProgress(40 + compressionProgress * 20);
227
+ });
228
+
229
+ // Convert file:// URI to regular path for RNFS upload
230
+ const normalizedPath = compressedVideoPath.replace('file://', '');
231
+
232
+ // Get compressed file size and log difference
233
+ const compressedStats = await RNFS.stat(normalizedPath);
234
+ const compressedSize = compressedStats.size;
235
+ const compressionRatio = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
236
+ console.log(`Video compression: ${(originalSize / 1024 / 1024).toFixed(2)}MB -> ${(compressedSize / 1024 / 1024).toFixed(2)}MB (${compressionRatio}% reduction)`);
206
237
  uploadFileOptions.files.push({
207
238
  name: 'files',
208
239
  filename: 'LIVENESS_VIDEO.mp4',
209
- filepath: videoFilePath,
240
+ filepath: normalizedPath,
210
241
  filetype: 'video/mp4'
211
242
  });
212
243
  }
@@ -235,12 +266,30 @@ const ResultScreen = () => {
235
266
  setProgress(40);
236
267
  const livenessDetection = identificationInfo.livenessDetection;
237
268
  await runWithRetry(() => uploadIdentificationMedia(identificationId, scannedIdentityDocument, livenessDetection));
269
+ setProgress(90);
238
270
  await runWithRetry(() => finishIdentification(identificationId, sessionKey));
239
271
  setProgress(100);
240
272
  setIsSubmitting(false);
273
+
274
+ // Track successful verification completion as final funnel step
275
+ trackFunnelStep('Verification Completed', 5, 5, 'nfc_scanning', true).catch(() => {});
276
+ analyticsService.trackEvent(AnalyticsEventName.VERIFICATION_SUCCESS, AnalyticsEventCategory.VERIFICATION, {
277
+ sessionId: appContext.identificationInfo.sessionId || ''
278
+ }).catch(() => {});
241
279
  } catch (error) {
242
280
  setIsSubmitting(false);
243
281
  setProgress(0);
282
+
283
+ // Track verification failure
284
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
285
+ trackError('VERIFICATION_SUBMISSION_FAILED', errorMessage, 'result_screen', 'critical', {
286
+ recoverable: false,
287
+ userAction: 'submit_verification'
288
+ }).catch(() => {});
289
+ analyticsService.trackEvent(AnalyticsEventName.VERIFICATION_FAILED, AnalyticsEventCategory.VERIFICATION, {
290
+ sessionId: appContext.identificationInfo.sessionId || '',
291
+ errorMessage
292
+ }).catch(() => {});
244
293
  Alert.alert(t('general.error'), t('resultScreen.submissionFailed'));
245
294
  appContext.onError?.(t('resultScreen.submissionFailed'));
246
295
  navigationManagerRef.current?.reset();