@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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analytics Service - GDPR/KVKK/PCI-DSS Compliant
|
|
5
|
+
* Sends events immediately when they occur, sanitizes PII
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { AppState } from 'react-native';
|
|
9
|
+
import DeviceInfo from 'react-native-device-info';
|
|
10
|
+
import RNFS from 'react-native-fs';
|
|
11
|
+
import 'react-native-get-random-values';
|
|
12
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
13
|
+
import packageJson from '../../../package.json';
|
|
14
|
+
import { AnalyticsEventName } from "../Types/analytics.types.js";
|
|
15
|
+
class AnalyticsService {
|
|
16
|
+
config = null;
|
|
17
|
+
sessionId = null;
|
|
18
|
+
deviceInfo = null;
|
|
19
|
+
isDemoSession = false;
|
|
20
|
+
|
|
21
|
+
// Batching configuration
|
|
22
|
+
queue = [];
|
|
23
|
+
BATCH_SIZE = 10;
|
|
24
|
+
MAX_QUEUE_SIZE = 100; // Prevent memory issues
|
|
25
|
+
FLUSH_INTERVAL = 5000; // 5 seconds
|
|
26
|
+
QUEUE_FILE_PATH = `${RNFS.CachesDirectoryPath}/trustchex_analytics_queue.json`;
|
|
27
|
+
flushTimer = null;
|
|
28
|
+
isFlushing = false;
|
|
29
|
+
appStateSubscription = null;
|
|
30
|
+
|
|
31
|
+
// Critical events that should flush immediately
|
|
32
|
+
CRITICAL_EVENTS = [AnalyticsEventName.SESSION_END, AnalyticsEventName.VERIFICATION_SUCCESS, AnalyticsEventName.VERIFICATION_FAILED];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set demo session flag to disable analytics for demo sessions
|
|
36
|
+
*/
|
|
37
|
+
setDemoSession(isDemoSession) {
|
|
38
|
+
this.isDemoSession = isDemoSession;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize the analytics service
|
|
43
|
+
* @param config Analytics configuration with required verification sessionId
|
|
44
|
+
*/
|
|
45
|
+
async initialize(config) {
|
|
46
|
+
// Prevent double initialization
|
|
47
|
+
if (this.config) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Validate required verificationSessionId
|
|
52
|
+
if (!config.verificationSessionId) {
|
|
53
|
+
throw new Error('Analytics initialization failed: verificationSessionId is required (must be verification session ID from backend)');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Merge with defaults
|
|
57
|
+
this.config = {
|
|
58
|
+
enabled: config.enabled ?? true,
|
|
59
|
+
baseUrl: config.baseUrl,
|
|
60
|
+
verificationSessionId: config.verificationSessionId
|
|
61
|
+
};
|
|
62
|
+
this.sessionId = config.verificationSessionId;
|
|
63
|
+
|
|
64
|
+
// Load persisted queue
|
|
65
|
+
await this.loadPersistedQueue();
|
|
66
|
+
|
|
67
|
+
// Collect anonymized device information
|
|
68
|
+
await this.collectDeviceInfo();
|
|
69
|
+
|
|
70
|
+
// Start flush timer
|
|
71
|
+
this.startFlushTimer();
|
|
72
|
+
|
|
73
|
+
// Listen for app state changes to flush before backgrounding
|
|
74
|
+
this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange.bind(this));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handle app state changes - flush when going to background
|
|
79
|
+
*/
|
|
80
|
+
handleAppStateChange(nextAppState) {
|
|
81
|
+
if (nextAppState === 'background' || nextAppState === 'inactive') {
|
|
82
|
+
// Persist queue first to ensure data safety, then flush
|
|
83
|
+
// Note: We don't await here as AppState callbacks are sync,
|
|
84
|
+
// but the operations are queued and will complete
|
|
85
|
+
this.persistQueue().then(() => this.flush());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load persisted queue from disk
|
|
91
|
+
*/
|
|
92
|
+
async loadPersistedQueue() {
|
|
93
|
+
try {
|
|
94
|
+
if (await RNFS.exists(this.QUEUE_FILE_PATH)) {
|
|
95
|
+
const content = await RNFS.readFile(this.QUEUE_FILE_PATH, 'utf8');
|
|
96
|
+
const persistedQueue = JSON.parse(content);
|
|
97
|
+
if (Array.isArray(persistedQueue) && persistedQueue.length > 0) {
|
|
98
|
+
// Merge with existing queue, prioritizing persisted events
|
|
99
|
+
this.queue = [...persistedQueue, ...this.queue];
|
|
100
|
+
if (__DEV__) console.log(`[Analytics] Loaded ${persistedQueue.length} events from disk`);
|
|
101
|
+
}
|
|
102
|
+
// Clear the file after loading to prevent duplicate loads
|
|
103
|
+
await RNFS.unlink(this.QUEUE_FILE_PATH);
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (__DEV__) console.warn('[Analytics] Failed to load persisted queue:', error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Persist queue to disk
|
|
112
|
+
*/
|
|
113
|
+
async persistQueue() {
|
|
114
|
+
try {
|
|
115
|
+
if (this.queue.length === 0) {
|
|
116
|
+
if (await RNFS.exists(this.QUEUE_FILE_PATH)) {
|
|
117
|
+
await RNFS.unlink(this.QUEUE_FILE_PATH);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await RNFS.writeFile(this.QUEUE_FILE_PATH, JSON.stringify(this.queue), 'utf8');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (__DEV__) console.warn('[Analytics] Failed to persist queue:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Track an analytics event - adds to queue and flushes if full
|
|
129
|
+
*/
|
|
130
|
+
async trackEvent(eventName, category, metadata) {
|
|
131
|
+
// Skip analytics for demo sessions
|
|
132
|
+
if (this.isDemoSession) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Skip if not initialized or disabled
|
|
137
|
+
if (!this.config?.enabled || !this.sessionId) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Collect device info on first event if not already done
|
|
142
|
+
if (!this.deviceInfo) {
|
|
143
|
+
await this.collectDeviceInfo();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Sanitize metadata to remove any potential PII
|
|
147
|
+
const sanitizedMetadata = this.sanitizeMetadata({
|
|
148
|
+
...metadata,
|
|
149
|
+
deviceModel: DeviceInfo.getModel()
|
|
150
|
+
});
|
|
151
|
+
const event = {
|
|
152
|
+
eventId: uuidv4(),
|
|
153
|
+
verificationSessionId: this.sessionId,
|
|
154
|
+
eventName,
|
|
155
|
+
category,
|
|
156
|
+
timestamp: new Date().toISOString(),
|
|
157
|
+
deviceInfo: this.deviceInfo,
|
|
158
|
+
metadata: sanitizedMetadata
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Check if this is a critical event that should flush immediately
|
|
162
|
+
const isCriticalEvent = this.CRITICAL_EVENTS.includes(eventName);
|
|
163
|
+
|
|
164
|
+
// Add to queue
|
|
165
|
+
this.queue.push(event);
|
|
166
|
+
|
|
167
|
+
// Drop oldest events if queue exceeds max size (prevent memory issues)
|
|
168
|
+
if (this.queue.length > this.MAX_QUEUE_SIZE) {
|
|
169
|
+
const dropped = this.queue.length - this.MAX_QUEUE_SIZE;
|
|
170
|
+
this.queue = this.queue.slice(dropped);
|
|
171
|
+
if (__DEV__) console.warn(`[Analytics] Queue overflow, dropped ${dropped} oldest events`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Flush immediately for critical events or if batch size reached
|
|
175
|
+
if (isCriticalEvent || this.queue.length >= this.BATCH_SIZE) {
|
|
176
|
+
await this.flush();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Start the periodic flush timer
|
|
182
|
+
*/
|
|
183
|
+
startFlushTimer() {
|
|
184
|
+
if (this.flushTimer) return;
|
|
185
|
+
this.flushTimer = setInterval(() => {
|
|
186
|
+
this.flush();
|
|
187
|
+
}, this.FLUSH_INTERVAL);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Stop the flush timer
|
|
192
|
+
*/
|
|
193
|
+
stopFlushTimer() {
|
|
194
|
+
if (this.flushTimer) {
|
|
195
|
+
clearInterval(this.flushTimer);
|
|
196
|
+
this.flushTimer = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Flush the event queue
|
|
202
|
+
*/
|
|
203
|
+
async flush() {
|
|
204
|
+
// Early return checks - must be atomic with setting isFlushing
|
|
205
|
+
if (this.queue.length === 0 || this.isFlushing || !this.config) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Set flag immediately to prevent race conditions
|
|
210
|
+
this.isFlushing = true;
|
|
211
|
+
|
|
212
|
+
// Take a batch, but don't remove from queue yet until success
|
|
213
|
+
const batchSize = Math.min(this.queue.length, this.BATCH_SIZE);
|
|
214
|
+
const batch = this.queue.slice(0, batchSize);
|
|
215
|
+
|
|
216
|
+
// Double-check batch has items (defensive programming)
|
|
217
|
+
if (batch.length === 0) {
|
|
218
|
+
this.isFlushing = false;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await this.sendBatchWithRetry(batch);
|
|
223
|
+
|
|
224
|
+
// Remove sent events
|
|
225
|
+
this.queue = this.queue.slice(batchSize);
|
|
226
|
+
|
|
227
|
+
// Update persistence
|
|
228
|
+
await this.persistQueue();
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// On failure, we keep events in the queue
|
|
231
|
+
if (__DEV__) console.warn('[Analytics] Failed to flush batch, keeping events:', error);
|
|
232
|
+
|
|
233
|
+
// Ensure they are persisted
|
|
234
|
+
await this.persistQueue();
|
|
235
|
+
} finally {
|
|
236
|
+
this.isFlushing = false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Send batch with retry logic (exponential backoff: 1s, 2s, 4s)
|
|
242
|
+
*/
|
|
243
|
+
async sendBatchWithRetry(events, maxRetries = 3, attempt = 1) {
|
|
244
|
+
if (!this.config || events.length === 0) return;
|
|
245
|
+
try {
|
|
246
|
+
const response = await fetch(`${this.config.baseUrl}/api/app/mobile/analytics`, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: {
|
|
249
|
+
'Content-Type': 'application/json'
|
|
250
|
+
},
|
|
251
|
+
body: JSON.stringify(events)
|
|
252
|
+
});
|
|
253
|
+
if (!response.ok) {
|
|
254
|
+
if (attempt <= maxRetries && response.status >= 500) {
|
|
255
|
+
// Retry on server errors with exponential backoff
|
|
256
|
+
const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s
|
|
257
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
258
|
+
return this.sendBatchWithRetry(events, maxRetries, attempt + 1);
|
|
259
|
+
}
|
|
260
|
+
// For 429 (Too Many Requests), throw to retry
|
|
261
|
+
if (response.status === 429) {
|
|
262
|
+
throw new Error('Rate limit exceeded');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// For other 4xx errors (Bad Request, Unauthorized, etc.), do NOT retry.
|
|
266
|
+
// We return successfully so the bad events are removed from the queue.
|
|
267
|
+
if (response.status >= 400 && response.status < 500) {
|
|
268
|
+
if (__DEV__) console.warn(`[Analytics] Dropping batch due to ${response.status} error`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (__DEV__) console.warn('[Analytics] Failed to send batch:', response.status);
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (attempt <= maxRetries) {
|
|
275
|
+
// Retry on network errors with exponential backoff
|
|
276
|
+
const delay = Math.pow(2, attempt - 1) * 1000;
|
|
277
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
278
|
+
return this.sendBatchWithRetry(events, maxRetries, attempt + 1);
|
|
279
|
+
}
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Clear analytics session and reset all state
|
|
286
|
+
*/
|
|
287
|
+
async clear() {
|
|
288
|
+
// Flush any pending events before clearing
|
|
289
|
+
await this.flush();
|
|
290
|
+
this.stopFlushTimer();
|
|
291
|
+
|
|
292
|
+
// Remove AppState listener
|
|
293
|
+
if (this.appStateSubscription) {
|
|
294
|
+
this.appStateSubscription.remove();
|
|
295
|
+
this.appStateSubscription = null;
|
|
296
|
+
}
|
|
297
|
+
this.queue = [];
|
|
298
|
+
this.sessionId = null;
|
|
299
|
+
this.deviceInfo = null;
|
|
300
|
+
this.config = null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check if analytics is enabled
|
|
305
|
+
*/
|
|
306
|
+
isEnabled() {
|
|
307
|
+
return this.config?.enabled === true && this.sessionId !== null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Private: Collect anonymized device information
|
|
312
|
+
*/
|
|
313
|
+
async collectDeviceInfo() {
|
|
314
|
+
try {
|
|
315
|
+
const platform = DeviceInfo.getSystemName().toLowerCase();
|
|
316
|
+
this.deviceInfo = {
|
|
317
|
+
platform: platform,
|
|
318
|
+
osVersion: DeviceInfo.getSystemVersion(),
|
|
319
|
+
appVersion: DeviceInfo.getVersion(),
|
|
320
|
+
sdkVersion: packageJson.version,
|
|
321
|
+
locale: 'en',
|
|
322
|
+
// Will be set from app context
|
|
323
|
+
timezone: new Date().toTimeString().split(' ')[1] || 'UTC',
|
|
324
|
+
screenResolution: `${DeviceInfo.getDeviceType()}`
|
|
325
|
+
};
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (__DEV__) console.warn('[Analytics] Error collecting device info:', error);
|
|
328
|
+
// Fallback device info
|
|
329
|
+
this.deviceInfo = {
|
|
330
|
+
platform: 'android',
|
|
331
|
+
osVersion: 'unknown',
|
|
332
|
+
appVersion: 'unknown',
|
|
333
|
+
sdkVersion: packageJson.version,
|
|
334
|
+
locale: 'en',
|
|
335
|
+
timezone: 'UTC',
|
|
336
|
+
screenResolution: 'unknown'
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Update device locale from app context
|
|
343
|
+
*/
|
|
344
|
+
setLocale(locale) {
|
|
345
|
+
if (this.deviceInfo) {
|
|
346
|
+
this.deviceInfo.locale = locale;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Private: Sanitize metadata to remove PII
|
|
352
|
+
*/
|
|
353
|
+
sanitizeMetadata(metadata) {
|
|
354
|
+
if (!metadata) return undefined;
|
|
355
|
+
const sanitized = {};
|
|
356
|
+
const forbiddenKeys = ['email', 'emailaddress', 'phone', 'phonenumber', 'firstname', 'lastname', 'fullname', 'address', 'streetaddress', 'ssn', 'socialsecurity', 'passport', 'passportnumber', 'driverlicense', 'creditcard', 'cardnumber', 'cvv', 'password', 'apikey', 'accesstoken'];
|
|
357
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
358
|
+
const lowerKey = key.toLowerCase();
|
|
359
|
+
|
|
360
|
+
// Skip if key is exactly a forbidden key or contains it as a word boundary
|
|
361
|
+
// But allow compound words like "buttonName", "screenName"
|
|
362
|
+
const isForbidden = forbiddenKeys.some(forbidden => {
|
|
363
|
+
// Check for exact match
|
|
364
|
+
if (lowerKey === forbidden) return true;
|
|
365
|
+
|
|
366
|
+
// Check if forbidden word appears at start followed by non-letter
|
|
367
|
+
if (lowerKey.startsWith(forbidden) && lowerKey.length > forbidden.length) {
|
|
368
|
+
const nextChar = lowerKey[forbidden.length];
|
|
369
|
+
if (!/[a-z]/.test(nextChar)) return true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check if forbidden word appears at end preceded by non-letter
|
|
373
|
+
if (lowerKey.endsWith(forbidden) && lowerKey.length > forbidden.length) {
|
|
374
|
+
const prevChar = lowerKey[lowerKey.length - forbidden.length - 1];
|
|
375
|
+
if (!/[a-z]/.test(prevChar)) return true;
|
|
376
|
+
}
|
|
377
|
+
return false;
|
|
378
|
+
});
|
|
379
|
+
if (isForbidden) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Skip if value looks like email
|
|
384
|
+
if (typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Skip if value looks like phone number
|
|
389
|
+
if (typeof value === 'string' && /^\+?[\d\s\-()]{10,}$/.test(value)) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Skip if value looks like credit card
|
|
394
|
+
if (typeof value === 'string' && /^\d{13,19}$/.test(value)) {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
sanitized[key] = value;
|
|
398
|
+
}
|
|
399
|
+
return sanitized;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Singleton instance
|
|
404
|
+
export const analyticsService = new AnalyticsService();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Analytics Types for Trustchex React Native SDK
|
|
5
|
+
* GDPR, KVKK, and PCI-DSS Compliant Analytics System
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Categories of analytics events
|
|
10
|
+
*/
|
|
11
|
+
export let AnalyticsEventCategory = /*#__PURE__*/function (AnalyticsEventCategory) {
|
|
12
|
+
AnalyticsEventCategory["SESSION"] = "session";
|
|
13
|
+
AnalyticsEventCategory["NAVIGATION"] = "navigation";
|
|
14
|
+
AnalyticsEventCategory["USER_ACTION"] = "user_action";
|
|
15
|
+
AnalyticsEventCategory["ERROR"] = "error";
|
|
16
|
+
AnalyticsEventCategory["PERFORMANCE"] = "performance";
|
|
17
|
+
AnalyticsEventCategory["VERIFICATION"] = "verification";
|
|
18
|
+
return AnalyticsEventCategory;
|
|
19
|
+
}({});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Error severity levels for analytics tracking
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Error categories for grouping similar errors
|
|
27
|
+
*/
|
|
28
|
+
export let ErrorCategory = /*#__PURE__*/function (ErrorCategory) {
|
|
29
|
+
ErrorCategory["NETWORK"] = "network";
|
|
30
|
+
ErrorCategory["PERMISSION"] = "permission";
|
|
31
|
+
ErrorCategory["CAMERA"] = "camera";
|
|
32
|
+
ErrorCategory["NFC"] = "nfc";
|
|
33
|
+
ErrorCategory["VALIDATION"] = "validation";
|
|
34
|
+
ErrorCategory["API"] = "api";
|
|
35
|
+
ErrorCategory["STORAGE"] = "storage";
|
|
36
|
+
ErrorCategory["DEVICE"] = "device";
|
|
37
|
+
ErrorCategory["USER_INPUT"] = "user_input";
|
|
38
|
+
ErrorCategory["SYSTEM"] = "system";
|
|
39
|
+
ErrorCategory["UNKNOWN"] = "unknown";
|
|
40
|
+
return ErrorCategory;
|
|
41
|
+
}({});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Context information for error tracking
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Event names for different analytics events
|
|
49
|
+
*/
|
|
50
|
+
export let AnalyticsEventName = /*#__PURE__*/function (AnalyticsEventName) {
|
|
51
|
+
// Session Events
|
|
52
|
+
AnalyticsEventName["SESSION_START"] = "session_start";
|
|
53
|
+
AnalyticsEventName["SESSION_END"] = "session_end";
|
|
54
|
+
// Navigation Events
|
|
55
|
+
AnalyticsEventName["SCREEN_VIEW"] = "screen_view";
|
|
56
|
+
AnalyticsEventName["SCREEN_EXIT"] = "screen_exit";
|
|
57
|
+
// User Action Events
|
|
58
|
+
AnalyticsEventName["BUTTON_CLICK"] = "button_click";
|
|
59
|
+
AnalyticsEventName["CONSENT_GIVEN"] = "consent_given";
|
|
60
|
+
AnalyticsEventName["STEP_SKIPPED"] = "step_skipped";
|
|
61
|
+
AnalyticsEventName["STEP_ABANDONED"] = "step_abandoned";
|
|
62
|
+
// Workflow Step Events (started/completed)
|
|
63
|
+
AnalyticsEventName["CONTRACT_ACCEPTANCE_STARTED"] = "contract_acceptance_started";
|
|
64
|
+
AnalyticsEventName["CONTRACT_ACCEPTANCE_COMPLETED"] = "contract_acceptance_completed";
|
|
65
|
+
AnalyticsEventName["DOCUMENT_SCAN_STARTED"] = "identity_document_scan_started";
|
|
66
|
+
AnalyticsEventName["DOCUMENT_SCAN_COMPLETED"] = "identity_document_scan_completed";
|
|
67
|
+
AnalyticsEventName["IDENTITY_DOCUMENT_EID_SCAN_STARTED"] = "identity_document_eid_scan_started";
|
|
68
|
+
AnalyticsEventName["IDENTITY_DOCUMENT_EID_SCAN_COMPLETED"] = "identity_document_eid_scan_completed";
|
|
69
|
+
AnalyticsEventName["LIVENESS_CHECK_STARTED"] = "liveness_check_started";
|
|
70
|
+
AnalyticsEventName["LIVENESS_CHECK_COMPLETED"] = "liveness_check_completed";
|
|
71
|
+
// NFC Scan Events (used by trackNFCScan* helpers)
|
|
72
|
+
AnalyticsEventName["NFC_SCAN_STARTED"] = "nfc_scan_started";
|
|
73
|
+
AnalyticsEventName["NFC_SCAN_COMPLETED"] = "nfc_scan_completed";
|
|
74
|
+
// NFC Error Events (specific error types for NFC failures)
|
|
75
|
+
AnalyticsEventName["NFC_SCAN_FAILED"] = "nfc_scan_failed";
|
|
76
|
+
AnalyticsEventName["NFC_DEVICE_UNSUPPORTED"] = "nfc_device_unsupported";
|
|
77
|
+
AnalyticsEventName["NFC_READING_ERROR"] = "nfc_reading_error";
|
|
78
|
+
AnalyticsEventName["NFC_USER_CANCELLED"] = "nfc_user_cancelled";
|
|
79
|
+
// EID Error Events
|
|
80
|
+
AnalyticsEventName["IDENTITY_DOCUMENT_EID_SCAN_FAILED"] = "identity_document_eid_scan_failed";
|
|
81
|
+
// Performance Events
|
|
82
|
+
AnalyticsEventName["API_CALL_DURATION"] = "api_call_duration";
|
|
83
|
+
// Verification Outcome Events
|
|
84
|
+
AnalyticsEventName["VERIFICATION_SUCCESS"] = "verification_success";
|
|
85
|
+
AnalyticsEventName["VERIFICATION_FAILED"] = "verification_failed";
|
|
86
|
+
return AnalyticsEventName;
|
|
87
|
+
}({});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Device information (anonymized and privacy-compliant)
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Analytics event metadata
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Base analytics event structure
|
|
99
|
+
*/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Analytics configuration
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Analytics service interface
|
|
107
|
+
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Enhanced error event metadata
|
|
111
|
+
*/
|
|
@@ -15,6 +15,7 @@ export const useCameraPermissions = () => {
|
|
|
15
15
|
const requestPermission = async () => {
|
|
16
16
|
const permission = await requestCameraPermission();
|
|
17
17
|
if (!permission) {
|
|
18
|
+
// Camera permission denied by user - their choice, not actionable
|
|
18
19
|
Alert.alert('Camera Permission Required', 'This app needs camera access to scan barcodes. Please enable camera permissions in your device settings.', [{
|
|
19
20
|
text: 'Cancel',
|
|
20
21
|
style: 'cancel'
|
|
@@ -3,13 +3,25 @@
|
|
|
3
3
|
import i18n from 'i18next';
|
|
4
4
|
import { initReactI18next } from 'react-i18next';
|
|
5
5
|
import * as resources from "./Resources/index.js";
|
|
6
|
-
import {
|
|
6
|
+
import { trackError } from "../Shared/Libs/analytics.utils.js";
|
|
7
7
|
const getCurrentLanguage = () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
try {
|
|
9
|
+
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
|
10
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
11
|
+
if (locale) {
|
|
12
|
+
// Extract language code from locale (e.g., "en-US" -> "en", "tr-TR" -> "tr")
|
|
13
|
+
return locale.split(/[-_]/)[0].toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return 'en';
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Error detecting language:', error);
|
|
19
|
+
trackError('LANGUAGE_DETECTION_ERROR', error instanceof Error ? error.message : 'Unknown error', 'translation_init', 'low', {
|
|
20
|
+
recoverable: true,
|
|
21
|
+
userAction: 'detect_language'
|
|
22
|
+
}).catch(() => {});
|
|
23
|
+
return 'en';
|
|
11
24
|
}
|
|
12
|
-
return deviceLanguage;
|
|
13
25
|
};
|
|
14
26
|
i18n.use(initReactI18next).init({
|
|
15
27
|
compatibilityJSON: 'v4',
|
package/lib/module/Trustchex.js
CHANGED
|
@@ -4,8 +4,8 @@ import React, { useEffect, useState, useMemo } from 'react';
|
|
|
4
4
|
import { NavigationContainer } from '@react-navigation/native';
|
|
5
5
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
6
6
|
import { View, ActivityIndicator, StyleSheet } from 'react-native';
|
|
7
|
-
import { MD3LightTheme, PaperProvider } from 'react-native-paper';
|
|
8
7
|
import 'react-native-get-random-values';
|
|
8
|
+
import { ThemeProvider } from "./Shared/Contexts/ThemeContext.js";
|
|
9
9
|
import IdentityDocumentEIDScanningScreen from "./Screens/Dynamic/IdentityDocumentEIDScanningScreen.js";
|
|
10
10
|
import IdentityDocumentScanningScreen from "./Screens/Dynamic/IdentityDocumentScanningScreen.js";
|
|
11
11
|
import LivenessDetectionScreen from "./Screens/Dynamic/LivenessDetectionScreen.js";
|
|
@@ -16,6 +16,8 @@ import QrCodeScanningScreen from "./Screens/Static/QrCodeScanningScreen.js";
|
|
|
16
16
|
import AppContext from "./Shared/Contexts/AppContext.js";
|
|
17
17
|
import i18n from "./Translation/index.js";
|
|
18
18
|
import { initializeTTS } from "./Shared/Libs/tts.utils.js";
|
|
19
|
+
import { analyticsService } from "./Shared/Services/AnalyticsService.js";
|
|
20
|
+
import { AnalyticsEventCategory, AnalyticsEventName } from "./Shared/Types/analytics.types.js";
|
|
19
21
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
20
22
|
const Stack = createNativeStackNavigator();
|
|
21
23
|
const DEFAULT_BRANDING = {
|
|
@@ -30,32 +32,32 @@ const Trustchex = ({
|
|
|
30
32
|
branding: propBranding,
|
|
31
33
|
locale: propLocale,
|
|
32
34
|
onCompleted,
|
|
33
|
-
onError
|
|
35
|
+
onError,
|
|
36
|
+
enableAnalytics = true
|
|
34
37
|
}) => {
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
console.log('[Trustchex] Props updated:', {
|
|
40
|
+
baseUrl: propBaseUrl,
|
|
41
|
+
sessionId: propSessionId,
|
|
42
|
+
enableAnalytics
|
|
43
|
+
});
|
|
44
|
+
}, [propBaseUrl, propSessionId, enableAnalytics]);
|
|
35
45
|
const [baseUrl, setBaseUrl] = useState(null);
|
|
36
46
|
const [sessionId, setSessionId] = useState(null);
|
|
37
47
|
const [locale, setLocale] = useState(propLocale || i18n.language);
|
|
38
48
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
49
|
+
const [analyticsInitialized, setAnalyticsInitialized] = useState(false);
|
|
39
50
|
const branding = useMemo(() => ({
|
|
40
51
|
...DEFAULT_BRANDING,
|
|
41
52
|
...propBranding
|
|
42
53
|
}), [propBranding]);
|
|
43
|
-
const theme = useMemo(() => ({
|
|
44
|
-
...MD3LightTheme,
|
|
45
|
-
colors: {
|
|
46
|
-
...MD3LightTheme.colors,
|
|
47
|
-
primary: branding.primaryColor,
|
|
48
|
-
secondary: branding.secondaryColor,
|
|
49
|
-
tertiary: branding.tertiaryColor
|
|
50
|
-
}
|
|
51
|
-
}), [branding]);
|
|
52
54
|
const contextValue = useMemo(() => ({
|
|
53
55
|
isDemoSession: false,
|
|
54
|
-
baseUrl,
|
|
56
|
+
baseUrl: baseUrl || '',
|
|
55
57
|
locale,
|
|
56
58
|
branding,
|
|
57
59
|
identificationInfo: {
|
|
58
|
-
sessionId,
|
|
60
|
+
sessionId: sessionId || '',
|
|
59
61
|
identificationId: '',
|
|
60
62
|
consent: {
|
|
61
63
|
contractIds: [],
|
|
@@ -64,8 +66,39 @@ const Trustchex = ({
|
|
|
64
66
|
locale: propLocale || i18n.language
|
|
65
67
|
},
|
|
66
68
|
onCompleted,
|
|
67
|
-
onError
|
|
69
|
+
onError,
|
|
70
|
+
setSessionId,
|
|
71
|
+
setBaseUrl
|
|
68
72
|
}), [baseUrl, locale, branding, sessionId, propLocale, onCompleted, onError]);
|
|
73
|
+
|
|
74
|
+
// Initialize analytics IMMEDIATELY when SDK mounts or session ID changes
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
console.log('[Trustchex] Analytics check:', {
|
|
77
|
+
enableAnalytics,
|
|
78
|
+
hasBaseUrl: !!baseUrl,
|
|
79
|
+
hasSessionId: !!sessionId,
|
|
80
|
+
analyticsInitialized,
|
|
81
|
+
sessionId: sessionId?.substring(0, 8) + '...'
|
|
82
|
+
});
|
|
83
|
+
if (enableAnalytics && baseUrl && sessionId && !analyticsInitialized) {
|
|
84
|
+
console.log('[Trustchex] Initializing analytics...');
|
|
85
|
+
// Initialize analytics synchronously with the verification sessionId
|
|
86
|
+
analyticsService.initialize({
|
|
87
|
+
enabled: true,
|
|
88
|
+
baseUrl: baseUrl,
|
|
89
|
+
verificationSessionId: sessionId // Pass the verification session ID from backend
|
|
90
|
+
}).then(() => {
|
|
91
|
+
console.log('[Trustchex] Analytics initialized successfully');
|
|
92
|
+
setAnalyticsInitialized(true);
|
|
93
|
+
// Track session start as the very first event
|
|
94
|
+
analyticsService.trackEvent(AnalyticsEventName.SESSION_START, AnalyticsEventCategory.SESSION, {
|
|
95
|
+
source: 'sdk_init'
|
|
96
|
+
});
|
|
97
|
+
}).catch(error => {
|
|
98
|
+
console.warn('[Trustchex] Failed to initialize analytics:', error);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}, [enableAnalytics, baseUrl, sessionId, analyticsInitialized]);
|
|
69
102
|
useEffect(() => {
|
|
70
103
|
initializeTTS();
|
|
71
104
|
}, []);
|
|
@@ -85,6 +118,7 @@ const Trustchex = ({
|
|
|
85
118
|
if (propLocale) {
|
|
86
119
|
setLocale(propLocale);
|
|
87
120
|
i18n.changeLanguage(propLocale);
|
|
121
|
+
analyticsService.setLocale(propLocale);
|
|
88
122
|
}
|
|
89
123
|
}, [propLocale]);
|
|
90
124
|
if (!baseUrl || !isInitialized) {
|
|
@@ -96,8 +130,10 @@ const Trustchex = ({
|
|
|
96
130
|
})
|
|
97
131
|
});
|
|
98
132
|
}
|
|
99
|
-
return /*#__PURE__*/_jsx(
|
|
100
|
-
|
|
133
|
+
return /*#__PURE__*/_jsx(ThemeProvider, {
|
|
134
|
+
primaryColor: branding.primaryColor,
|
|
135
|
+
secondaryColor: branding.secondaryColor,
|
|
136
|
+
tertiaryColor: branding.tertiaryColor,
|
|
101
137
|
children: /*#__PURE__*/_jsx(AppContext.Provider, {
|
|
102
138
|
value: contextValue,
|
|
103
139
|
children: /*#__PURE__*/_jsx(NavigationContainer, {
|
package/lib/module/index.js
CHANGED
|
@@ -2,4 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import Trustchex from "./Trustchex.js";
|
|
4
4
|
export { handleDeepLink } from "./Shared/Libs/deeplink.utils.js";
|
|
5
|
+
export { analyticsService } from "./Shared/Services/AnalyticsService.js";
|
|
6
|
+
export { trackScreenView, trackScreenExit, trackButtonClick, trackError, trackErrorWithDetails, trackApiCall, trackVerificationStart, trackVerificationComplete, trackConsentGiven, trackSessionStart, trackSessionEnd, trackNFCScanStart, trackNFCScanComplete, trackNFCScanFailed, trackFunnelStep, trackStepAbandoned, trackStepSkipped, useScreenTracking } from "./Shared/Libs/analytics.utils.js";
|
|
7
|
+
export { AnalyticsEventCategory, AnalyticsEventName, ErrorCategory } from "./Shared/Types/analytics.types.js";
|
|
5
8
|
export default Trustchex;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContractAcceptanceScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/ContractAcceptanceScreen.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ContractAcceptanceScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/ContractAcceptanceScreen.tsx"],"names":[],"mappings":"AA0BA,QAAA,MAAM,wBAAwB,+CAmJ7B,CAAC;AA4BF,eAAe,wBAAwB,CAAC"}
|