@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
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,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 (
|
|
113
|
-
//
|
|
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 (
|
|
141
|
-
//
|
|
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.
|
|
19
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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();
|