@trustchex/react-native-sdk 1.253.0 → 1.266.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -2
- package/android/src/main/java/com/trustchex/reactnativesdk/DeviceBrightnessModule.kt +66 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
- package/ios/DeviceBrightnessModule.h +4 -0
- package/ios/DeviceBrightnessModule.m +27 -0
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +25 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +19 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +19 -0
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +18 -5
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +10 -2
- package/lib/module/Screens/Static/ResultScreen.js +52 -3
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +41 -2
- package/lib/module/Shared/Components/EIDScanner.js +63 -3
- package/lib/module/Shared/Components/FaceCamera.js +69 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +4 -1
- package/lib/module/Shared/Components/NavigationManager.js +2 -0
- package/lib/module/Shared/Components/QrCodeScannerCamera.js +2 -0
- package/lib/module/Shared/Contexts/AppContext.js +3 -1
- package/lib/module/Shared/Libs/analytics.utils.js +430 -0
- package/lib/module/Shared/Libs/camera.utils.js +58 -2
- package/lib/module/Shared/Libs/deeplink.utils.js +8 -0
- package/lib/module/Shared/Libs/http-client.js +89 -28
- package/lib/module/Shared/Services/AnalyticsService.js +404 -0
- package/lib/module/Shared/Types/analytics.types.js +111 -0
- package/lib/module/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.js +1 -0
- package/lib/module/Translation/index.js +5 -0
- package/lib/module/Trustchex.js +47 -4
- package/lib/module/index.js +3 -0
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts +7 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/QrCodeScannerCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +2 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts +98 -0
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +19 -1
- package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts +86 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts +146 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -0
- package/lib/typescript/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Translation/index.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts +1 -0
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +35 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +30 -0
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +30 -0
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +30 -4
- package/src/Screens/Static/QrCodeScanningScreen.tsx +12 -2
- package/src/Screens/Static/ResultScreen.tsx +79 -4
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +65 -10
- package/src/Shared/Components/EIDScanner.tsx +132 -3
- package/src/Shared/Components/FaceCamera.tsx +77 -2
- package/src/Shared/Components/IdentityDocumentCamera.tsx +4 -4
- package/src/Shared/Components/NavigationManager.tsx +2 -0
- package/src/Shared/Components/QrCodeScannerCamera.tsx +2 -0
- package/src/Shared/Contexts/AppContext.ts +4 -0
- package/src/Shared/Libs/analytics.utils.ts +644 -0
- package/src/Shared/Libs/camera.utils.ts +74 -2
- package/src/Shared/Libs/deeplink.utils.ts +5 -0
- package/src/Shared/Libs/http-client.ts +105 -31
- package/src/Shared/Services/AnalyticsService.ts +470 -0
- package/src/Shared/Types/analytics.types.ts +179 -0
- package/src/Shared/VisionCameraPlugins/BarcodeScanner/hooks/useCameraPermissions.ts +1 -0
- package/src/Translation/Resources/tr.ts +2 -1
- package/src/Translation/index.ts +9 -0
- package/src/Trustchex.tsx +54 -2
- package/src/index.tsx +33 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Service - GDPR/KVKK/PCI-DSS Compliant
|
|
3
|
+
* Sends events immediately when they occur, sanitizes PII
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
7
|
+
import DeviceInfo from 'react-native-device-info';
|
|
8
|
+
import RNFS from 'react-native-fs';
|
|
9
|
+
import 'react-native-get-random-values';
|
|
10
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
+
import packageJson from '../../../package.json';
|
|
12
|
+
import {
|
|
13
|
+
AnalyticsEventCategory,
|
|
14
|
+
AnalyticsEventName,
|
|
15
|
+
} from '../Types/analytics.types';
|
|
16
|
+
import type {
|
|
17
|
+
AnalyticsConfig,
|
|
18
|
+
AnalyticsEvent,
|
|
19
|
+
AnalyticsEventMetadata,
|
|
20
|
+
AnonymizedDeviceInfo,
|
|
21
|
+
IAnalyticsService,
|
|
22
|
+
} from '../Types/analytics.types';
|
|
23
|
+
|
|
24
|
+
class AnalyticsService implements IAnalyticsService {
|
|
25
|
+
private config: AnalyticsConfig | null = null;
|
|
26
|
+
private sessionId: string | null = null;
|
|
27
|
+
private deviceInfo: AnonymizedDeviceInfo | null = null;
|
|
28
|
+
private isDemoSession: boolean = false;
|
|
29
|
+
|
|
30
|
+
// Batching configuration
|
|
31
|
+
private queue: AnalyticsEvent[] = [];
|
|
32
|
+
private readonly BATCH_SIZE = 10;
|
|
33
|
+
private readonly MAX_QUEUE_SIZE = 100; // Prevent memory issues
|
|
34
|
+
private readonly FLUSH_INTERVAL = 5000; // 5 seconds
|
|
35
|
+
private readonly QUEUE_FILE_PATH = `${RNFS.CachesDirectoryPath}/trustchex_analytics_queue.json`;
|
|
36
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
37
|
+
private isFlushing = false;
|
|
38
|
+
private appStateSubscription: { remove: () => void } | null = null;
|
|
39
|
+
|
|
40
|
+
// Critical events that should flush immediately
|
|
41
|
+
private readonly CRITICAL_EVENTS = [
|
|
42
|
+
AnalyticsEventName.SESSION_END,
|
|
43
|
+
AnalyticsEventName.VERIFICATION_SUCCESS,
|
|
44
|
+
AnalyticsEventName.VERIFICATION_FAILED,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set demo session flag to disable analytics for demo sessions
|
|
49
|
+
*/
|
|
50
|
+
setDemoSession(isDemoSession: boolean): void {
|
|
51
|
+
this.isDemoSession = isDemoSession;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize the analytics service
|
|
56
|
+
* @param config Analytics configuration with required verification sessionId
|
|
57
|
+
*/
|
|
58
|
+
async initialize(config: AnalyticsConfig): Promise<void> {
|
|
59
|
+
// Prevent double initialization
|
|
60
|
+
if (this.config) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate required verificationSessionId
|
|
65
|
+
if (!config.verificationSessionId) {
|
|
66
|
+
throw new Error('Analytics initialization failed: verificationSessionId is required (must be verification session ID from backend)');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Merge with defaults
|
|
70
|
+
this.config = {
|
|
71
|
+
enabled: config.enabled ?? true,
|
|
72
|
+
baseUrl: config.baseUrl,
|
|
73
|
+
verificationSessionId: config.verificationSessionId,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
this.sessionId = config.verificationSessionId;
|
|
77
|
+
|
|
78
|
+
// Load persisted queue
|
|
79
|
+
await this.loadPersistedQueue();
|
|
80
|
+
|
|
81
|
+
// Collect anonymized device information
|
|
82
|
+
await this.collectDeviceInfo();
|
|
83
|
+
|
|
84
|
+
// Start flush timer
|
|
85
|
+
this.startFlushTimer();
|
|
86
|
+
|
|
87
|
+
// Listen for app state changes to flush before backgrounding
|
|
88
|
+
this.appStateSubscription = AppState.addEventListener(
|
|
89
|
+
'change',
|
|
90
|
+
this.handleAppStateChange.bind(this)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle app state changes - flush when going to background
|
|
96
|
+
*/
|
|
97
|
+
private handleAppStateChange(nextAppState: AppStateStatus): void {
|
|
98
|
+
if (nextAppState === 'background' || nextAppState === 'inactive') {
|
|
99
|
+
// Persist queue first to ensure data safety, then flush
|
|
100
|
+
// Note: We don't await here as AppState callbacks are sync,
|
|
101
|
+
// but the operations are queued and will complete
|
|
102
|
+
this.persistQueue().then(() => this.flush());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load persisted queue from disk
|
|
108
|
+
*/
|
|
109
|
+
private async loadPersistedQueue(): Promise<void> {
|
|
110
|
+
try {
|
|
111
|
+
if (await RNFS.exists(this.QUEUE_FILE_PATH)) {
|
|
112
|
+
const content = await RNFS.readFile(this.QUEUE_FILE_PATH, 'utf8');
|
|
113
|
+
const persistedQueue = JSON.parse(content);
|
|
114
|
+
if (Array.isArray(persistedQueue) && persistedQueue.length > 0) {
|
|
115
|
+
// Merge with existing queue, prioritizing persisted events
|
|
116
|
+
this.queue = [...persistedQueue, ...this.queue];
|
|
117
|
+
if (__DEV__) console.log(`[Analytics] Loaded ${persistedQueue.length} events from disk`);
|
|
118
|
+
}
|
|
119
|
+
// Clear the file after loading to prevent duplicate loads
|
|
120
|
+
await RNFS.unlink(this.QUEUE_FILE_PATH);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (__DEV__) console.warn('[Analytics] Failed to load persisted queue:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Persist queue to disk
|
|
129
|
+
*/
|
|
130
|
+
private async persistQueue(): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
if (this.queue.length === 0) {
|
|
133
|
+
if (await RNFS.exists(this.QUEUE_FILE_PATH)) {
|
|
134
|
+
await RNFS.unlink(this.QUEUE_FILE_PATH);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await RNFS.writeFile(this.QUEUE_FILE_PATH, JSON.stringify(this.queue), 'utf8');
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (__DEV__) console.warn('[Analytics] Failed to persist queue:', error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Track an analytics event - adds to queue and flushes if full
|
|
146
|
+
*/
|
|
147
|
+
async trackEvent(
|
|
148
|
+
eventName: string,
|
|
149
|
+
category: AnalyticsEventCategory,
|
|
150
|
+
metadata?: AnalyticsEventMetadata
|
|
151
|
+
): Promise<void> {
|
|
152
|
+
// Skip analytics for demo sessions
|
|
153
|
+
if (this.isDemoSession) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip if not initialized or disabled
|
|
158
|
+
if (!this.config?.enabled || !this.sessionId) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Collect device info on first event if not already done
|
|
163
|
+
if (!this.deviceInfo) {
|
|
164
|
+
await this.collectDeviceInfo();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Sanitize metadata to remove any potential PII
|
|
168
|
+
const sanitizedMetadata = this.sanitizeMetadata({
|
|
169
|
+
...metadata,
|
|
170
|
+
deviceModel: DeviceInfo.getModel(),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const event: AnalyticsEvent = {
|
|
174
|
+
eventId: uuidv4(),
|
|
175
|
+
verificationSessionId: this.sessionId,
|
|
176
|
+
eventName,
|
|
177
|
+
category,
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
deviceInfo: this.deviceInfo!,
|
|
180
|
+
metadata: sanitizedMetadata,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Check if this is a critical event that should flush immediately
|
|
184
|
+
const isCriticalEvent = this.CRITICAL_EVENTS.includes(
|
|
185
|
+
eventName as AnalyticsEventName
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Add to queue
|
|
189
|
+
this.queue.push(event);
|
|
190
|
+
|
|
191
|
+
// Drop oldest events if queue exceeds max size (prevent memory issues)
|
|
192
|
+
if (this.queue.length > this.MAX_QUEUE_SIZE) {
|
|
193
|
+
const dropped = this.queue.length - this.MAX_QUEUE_SIZE;
|
|
194
|
+
this.queue = this.queue.slice(dropped);
|
|
195
|
+
if (__DEV__) console.warn(`[Analytics] Queue overflow, dropped ${dropped} oldest events`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Flush immediately for critical events or if batch size reached
|
|
199
|
+
if (isCriticalEvent || this.queue.length >= this.BATCH_SIZE) {
|
|
200
|
+
await this.flush();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Start the periodic flush timer
|
|
206
|
+
*/
|
|
207
|
+
private startFlushTimer(): void {
|
|
208
|
+
if (this.flushTimer) return;
|
|
209
|
+
|
|
210
|
+
this.flushTimer = setInterval(() => {
|
|
211
|
+
this.flush();
|
|
212
|
+
}, this.FLUSH_INTERVAL);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Stop the flush timer
|
|
217
|
+
*/
|
|
218
|
+
private stopFlushTimer(): void {
|
|
219
|
+
if (this.flushTimer) {
|
|
220
|
+
clearInterval(this.flushTimer);
|
|
221
|
+
this.flushTimer = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Flush the event queue
|
|
227
|
+
*/
|
|
228
|
+
async flush(): Promise<void> {
|
|
229
|
+
// Early return checks - must be atomic with setting isFlushing
|
|
230
|
+
if (this.queue.length === 0 || this.isFlushing || !this.config) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Set flag immediately to prevent race conditions
|
|
235
|
+
this.isFlushing = true;
|
|
236
|
+
|
|
237
|
+
// Take a batch, but don't remove from queue yet until success
|
|
238
|
+
const batchSize = Math.min(this.queue.length, this.BATCH_SIZE);
|
|
239
|
+
const batch = this.queue.slice(0, batchSize);
|
|
240
|
+
|
|
241
|
+
// Double-check batch has items (defensive programming)
|
|
242
|
+
if (batch.length === 0) {
|
|
243
|
+
this.isFlushing = false;
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await this.sendBatchWithRetry(batch);
|
|
249
|
+
|
|
250
|
+
// Remove sent events
|
|
251
|
+
this.queue = this.queue.slice(batchSize);
|
|
252
|
+
|
|
253
|
+
// Update persistence
|
|
254
|
+
await this.persistQueue();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// On failure, we keep events in the queue
|
|
257
|
+
if (__DEV__) console.warn('[Analytics] Failed to flush batch, keeping events:', error);
|
|
258
|
+
|
|
259
|
+
// Ensure they are persisted
|
|
260
|
+
await this.persistQueue();
|
|
261
|
+
} finally {
|
|
262
|
+
this.isFlushing = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Send batch with retry logic (exponential backoff: 1s, 2s, 4s)
|
|
268
|
+
*/
|
|
269
|
+
private async sendBatchWithRetry(events: AnalyticsEvent[], maxRetries = 3, attempt = 1): Promise<void> {
|
|
270
|
+
if (!this.config || events.length === 0) return;
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const response = await fetch(
|
|
274
|
+
`${this.config.baseUrl}/api/app/mobile/analytics`,
|
|
275
|
+
{
|
|
276
|
+
method: 'POST',
|
|
277
|
+
headers: {
|
|
278
|
+
'Content-Type': 'application/json',
|
|
279
|
+
},
|
|
280
|
+
body: JSON.stringify(events),
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
if (attempt <= maxRetries && response.status >= 500) {
|
|
286
|
+
// Retry on server errors with exponential backoff
|
|
287
|
+
const delay = Math.pow(2, attempt - 1) * 1000; // 1s, 2s, 4s
|
|
288
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
289
|
+
return this.sendBatchWithRetry(events, maxRetries, attempt + 1);
|
|
290
|
+
}
|
|
291
|
+
// For 429 (Too Many Requests), throw to retry
|
|
292
|
+
if (response.status === 429) {
|
|
293
|
+
throw new Error('Rate limit exceeded');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// For other 4xx errors (Bad Request, Unauthorized, etc.), do NOT retry.
|
|
297
|
+
// We return successfully so the bad events are removed from the queue.
|
|
298
|
+
if (response.status >= 400 && response.status < 500) {
|
|
299
|
+
if (__DEV__) console.warn(`[Analytics] Dropping batch due to ${response.status} error`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (__DEV__) console.warn('[Analytics] Failed to send batch:', response.status);
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (attempt <= maxRetries) {
|
|
307
|
+
// Retry on network errors with exponential backoff
|
|
308
|
+
const delay = Math.pow(2, attempt - 1) * 1000;
|
|
309
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
310
|
+
return this.sendBatchWithRetry(events, maxRetries, attempt + 1);
|
|
311
|
+
}
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Clear analytics session and reset all state
|
|
318
|
+
*/
|
|
319
|
+
async clear(): Promise<void> {
|
|
320
|
+
// Flush any pending events before clearing
|
|
321
|
+
await this.flush();
|
|
322
|
+
|
|
323
|
+
this.stopFlushTimer();
|
|
324
|
+
|
|
325
|
+
// Remove AppState listener
|
|
326
|
+
if (this.appStateSubscription) {
|
|
327
|
+
this.appStateSubscription.remove();
|
|
328
|
+
this.appStateSubscription = null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.queue = [];
|
|
332
|
+
this.sessionId = null;
|
|
333
|
+
this.deviceInfo = null;
|
|
334
|
+
this.config = null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Check if analytics is enabled
|
|
339
|
+
*/
|
|
340
|
+
isEnabled(): boolean {
|
|
341
|
+
return this.config?.enabled === true && this.sessionId !== null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Private: Collect anonymized device information
|
|
346
|
+
*/
|
|
347
|
+
private async collectDeviceInfo(): Promise<void> {
|
|
348
|
+
try {
|
|
349
|
+
const platform = DeviceInfo.getSystemName().toLowerCase();
|
|
350
|
+
|
|
351
|
+
this.deviceInfo = {
|
|
352
|
+
platform: platform as 'ios' | 'android',
|
|
353
|
+
osVersion: DeviceInfo.getSystemVersion(),
|
|
354
|
+
appVersion: DeviceInfo.getVersion(),
|
|
355
|
+
sdkVersion: packageJson.version,
|
|
356
|
+
locale: 'en', // Will be set from app context
|
|
357
|
+
timezone: new Date().toTimeString().split(' ')[1] || 'UTC',
|
|
358
|
+
screenResolution: `${DeviceInfo.getDeviceType()}`,
|
|
359
|
+
};
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (__DEV__) console.warn('[Analytics] Error collecting device info:', error);
|
|
362
|
+
// Fallback device info
|
|
363
|
+
this.deviceInfo = {
|
|
364
|
+
platform: 'android',
|
|
365
|
+
osVersion: 'unknown',
|
|
366
|
+
appVersion: 'unknown',
|
|
367
|
+
sdkVersion: packageJson.version,
|
|
368
|
+
locale: 'en',
|
|
369
|
+
timezone: 'UTC',
|
|
370
|
+
screenResolution: 'unknown',
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Update device locale from app context
|
|
377
|
+
*/
|
|
378
|
+
public setLocale(locale: string): void {
|
|
379
|
+
if (this.deviceInfo) {
|
|
380
|
+
this.deviceInfo.locale = locale;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Private: Sanitize metadata to remove PII
|
|
386
|
+
*/
|
|
387
|
+
private sanitizeMetadata(
|
|
388
|
+
metadata?: AnalyticsEventMetadata
|
|
389
|
+
): AnalyticsEventMetadata | undefined {
|
|
390
|
+
if (!metadata) return undefined;
|
|
391
|
+
|
|
392
|
+
const sanitized: AnalyticsEventMetadata = {};
|
|
393
|
+
const forbiddenKeys = [
|
|
394
|
+
'email',
|
|
395
|
+
'emailaddress',
|
|
396
|
+
'phone',
|
|
397
|
+
'phonenumber',
|
|
398
|
+
'firstname',
|
|
399
|
+
'lastname',
|
|
400
|
+
'fullname',
|
|
401
|
+
'address',
|
|
402
|
+
'streetaddress',
|
|
403
|
+
'ssn',
|
|
404
|
+
'socialsecurity',
|
|
405
|
+
'passport',
|
|
406
|
+
'passportnumber',
|
|
407
|
+
'driverlicense',
|
|
408
|
+
'creditcard',
|
|
409
|
+
'cardnumber',
|
|
410
|
+
'cvv',
|
|
411
|
+
'password',
|
|
412
|
+
'apikey',
|
|
413
|
+
'accesstoken',
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
417
|
+
const lowerKey = key.toLowerCase();
|
|
418
|
+
|
|
419
|
+
// Skip if key is exactly a forbidden key or contains it as a word boundary
|
|
420
|
+
// But allow compound words like "buttonName", "screenName"
|
|
421
|
+
const isForbidden = forbiddenKeys.some((forbidden) => {
|
|
422
|
+
// Check for exact match
|
|
423
|
+
if (lowerKey === forbidden) return true;
|
|
424
|
+
|
|
425
|
+
// Check if forbidden word appears at start followed by non-letter
|
|
426
|
+
if (lowerKey.startsWith(forbidden) && lowerKey.length > forbidden.length) {
|
|
427
|
+
const nextChar = lowerKey[forbidden.length];
|
|
428
|
+
if (!/[a-z]/.test(nextChar)) return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Check if forbidden word appears at end preceded by non-letter
|
|
432
|
+
if (lowerKey.endsWith(forbidden) && lowerKey.length > forbidden.length) {
|
|
433
|
+
const prevChar = lowerKey[lowerKey.length - forbidden.length - 1];
|
|
434
|
+
if (!/[a-z]/.test(prevChar)) return true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return false;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (isForbidden) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Skip if value looks like email
|
|
445
|
+
if (
|
|
446
|
+
typeof value === 'string' &&
|
|
447
|
+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
448
|
+
) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Skip if value looks like phone number
|
|
453
|
+
if (typeof value === 'string' && /^\+?[\d\s\-()]{10,}$/.test(value)) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Skip if value looks like credit card
|
|
458
|
+
if (typeof value === 'string' && /^\d{13,19}$/.test(value)) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
sanitized[key] = value;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return sanitized;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Singleton instance
|
|
470
|
+
export const analyticsService = new AnalyticsService();
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Types for Trustchex React Native SDK
|
|
3
|
+
* GDPR, KVKK, and PCI-DSS Compliant Analytics System
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Categories of analytics events
|
|
8
|
+
*/
|
|
9
|
+
export enum AnalyticsEventCategory {
|
|
10
|
+
SESSION = 'session',
|
|
11
|
+
NAVIGATION = 'navigation',
|
|
12
|
+
USER_ACTION = 'user_action',
|
|
13
|
+
ERROR = 'error',
|
|
14
|
+
PERFORMANCE = 'performance',
|
|
15
|
+
VERIFICATION = 'verification',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Error severity levels for analytics tracking
|
|
20
|
+
*/
|
|
21
|
+
export type ErrorSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Error categories for grouping similar errors
|
|
25
|
+
*/
|
|
26
|
+
export enum ErrorCategory {
|
|
27
|
+
NETWORK = 'network',
|
|
28
|
+
PERMISSION = 'permission',
|
|
29
|
+
CAMERA = 'camera',
|
|
30
|
+
NFC = 'nfc',
|
|
31
|
+
VALIDATION = 'validation',
|
|
32
|
+
API = 'api',
|
|
33
|
+
STORAGE = 'storage',
|
|
34
|
+
DEVICE = 'device',
|
|
35
|
+
USER_INPUT = 'user_input',
|
|
36
|
+
SYSTEM = 'system',
|
|
37
|
+
UNKNOWN = 'unknown',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Context information for error tracking
|
|
42
|
+
*/
|
|
43
|
+
export interface ErrorContext {
|
|
44
|
+
/** The component or module where the error occurred */
|
|
45
|
+
component?: string;
|
|
46
|
+
/** User action that triggered the error */
|
|
47
|
+
userAction?: string;
|
|
48
|
+
/** Whether the error is recoverable */
|
|
49
|
+
recoverable?: boolean;
|
|
50
|
+
/** Number of retry attempts made */
|
|
51
|
+
retryCount?: number;
|
|
52
|
+
/** Sanitized stack trace (no PII) */
|
|
53
|
+
stackTrace?: string;
|
|
54
|
+
/** Additional context data */
|
|
55
|
+
additionalData?: Record<string, string | number | boolean | null>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Event names for different analytics events
|
|
60
|
+
*/
|
|
61
|
+
export enum AnalyticsEventName {
|
|
62
|
+
// Session Events
|
|
63
|
+
SESSION_START = 'session_start',
|
|
64
|
+
SESSION_END = 'session_end',
|
|
65
|
+
|
|
66
|
+
// Navigation Events
|
|
67
|
+
SCREEN_VIEW = 'screen_view',
|
|
68
|
+
SCREEN_EXIT = 'screen_exit',
|
|
69
|
+
|
|
70
|
+
// User Action Events
|
|
71
|
+
BUTTON_CLICK = 'button_click',
|
|
72
|
+
CONSENT_GIVEN = 'consent_given',
|
|
73
|
+
STEP_SKIPPED = 'step_skipped',
|
|
74
|
+
STEP_ABANDONED = 'step_abandoned',
|
|
75
|
+
|
|
76
|
+
// Workflow Step Events (started/completed)
|
|
77
|
+
CONTRACT_ACCEPTANCE_STARTED = 'contract_acceptance_started',
|
|
78
|
+
CONTRACT_ACCEPTANCE_COMPLETED = 'contract_acceptance_completed',
|
|
79
|
+
DOCUMENT_SCAN_STARTED = 'identity_document_scan_started',
|
|
80
|
+
DOCUMENT_SCAN_COMPLETED = 'identity_document_scan_completed',
|
|
81
|
+
IDENTITY_DOCUMENT_EID_SCAN_STARTED = 'identity_document_eid_scan_started',
|
|
82
|
+
IDENTITY_DOCUMENT_EID_SCAN_COMPLETED = 'identity_document_eid_scan_completed',
|
|
83
|
+
LIVENESS_CHECK_STARTED = 'liveness_check_started',
|
|
84
|
+
LIVENESS_CHECK_COMPLETED = 'liveness_check_completed',
|
|
85
|
+
|
|
86
|
+
// NFC Scan Events (used by trackNFCScan* helpers)
|
|
87
|
+
NFC_SCAN_STARTED = 'nfc_scan_started',
|
|
88
|
+
NFC_SCAN_COMPLETED = 'nfc_scan_completed',
|
|
89
|
+
|
|
90
|
+
// NFC Error Events (specific error types for NFC failures)
|
|
91
|
+
NFC_SCAN_FAILED = 'nfc_scan_failed',
|
|
92
|
+
NFC_DEVICE_UNSUPPORTED = 'nfc_device_unsupported',
|
|
93
|
+
NFC_READING_ERROR = 'nfc_reading_error',
|
|
94
|
+
NFC_USER_CANCELLED = 'nfc_user_cancelled',
|
|
95
|
+
|
|
96
|
+
// EID Error Events
|
|
97
|
+
IDENTITY_DOCUMENT_EID_SCAN_FAILED = 'identity_document_eid_scan_failed',
|
|
98
|
+
|
|
99
|
+
// Performance Events
|
|
100
|
+
API_CALL_DURATION = 'api_call_duration',
|
|
101
|
+
|
|
102
|
+
// Verification Outcome Events
|
|
103
|
+
VERIFICATION_SUCCESS = 'verification_success',
|
|
104
|
+
VERIFICATION_FAILED = 'verification_failed',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Device information (anonymized and privacy-compliant)
|
|
109
|
+
*/
|
|
110
|
+
export interface AnonymizedDeviceInfo {
|
|
111
|
+
platform: 'ios' | 'android';
|
|
112
|
+
osVersion: string;
|
|
113
|
+
appVersion: string;
|
|
114
|
+
sdkVersion: string;
|
|
115
|
+
locale: string;
|
|
116
|
+
timezone: string;
|
|
117
|
+
screenResolution: string;
|
|
118
|
+
// No device IDs, MAC addresses, or other PII
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Analytics event metadata
|
|
123
|
+
*/
|
|
124
|
+
export interface AnalyticsEventMetadata {
|
|
125
|
+
[key: string]: string | number | boolean | null | undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Base analytics event structure
|
|
130
|
+
*/
|
|
131
|
+
export interface AnalyticsEvent {
|
|
132
|
+
eventId: string; // UUID v4
|
|
133
|
+
verificationSessionId: string; // Verification session ID from backend
|
|
134
|
+
eventName: string;
|
|
135
|
+
category: AnalyticsEventCategory;
|
|
136
|
+
timestamp: string; // ISO 8601 format
|
|
137
|
+
deviceInfo: AnonymizedDeviceInfo;
|
|
138
|
+
metadata?: AnalyticsEventMetadata;
|
|
139
|
+
// No PII fields allowed
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Analytics configuration
|
|
144
|
+
*/
|
|
145
|
+
export interface AnalyticsConfig {
|
|
146
|
+
enabled: boolean;
|
|
147
|
+
baseUrl: string;
|
|
148
|
+
verificationSessionId: string; // Required: Verification session ID from backend
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Analytics service interface
|
|
153
|
+
*/
|
|
154
|
+
export interface IAnalyticsService {
|
|
155
|
+
initialize(config: AnalyticsConfig): Promise<void>;
|
|
156
|
+
trackEvent(
|
|
157
|
+
eventName: string,
|
|
158
|
+
category: AnalyticsEventCategory,
|
|
159
|
+
metadata?: AnalyticsEventMetadata
|
|
160
|
+
): Promise<void>;
|
|
161
|
+
clear(): Promise<void>;
|
|
162
|
+
isEnabled(): boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Enhanced error event metadata
|
|
167
|
+
*/
|
|
168
|
+
export interface ErrorEventMetadata extends AnalyticsEventMetadata {
|
|
169
|
+
errorCode: string;
|
|
170
|
+
errorMessage: string;
|
|
171
|
+
screenName: string;
|
|
172
|
+
severity: ErrorSeverity;
|
|
173
|
+
category?: ErrorCategory;
|
|
174
|
+
component?: string;
|
|
175
|
+
userAction?: string;
|
|
176
|
+
recoverable?: boolean;
|
|
177
|
+
retryCount?: number;
|
|
178
|
+
stackTrace?: string;
|
|
179
|
+
}
|
|
@@ -23,6 +23,7 @@ export const useCameraPermissions = (): UseCameraPermissionResult => {
|
|
|
23
23
|
const permission = await requestCameraPermission();
|
|
24
24
|
|
|
25
25
|
if (!permission) {
|
|
26
|
+
// Camera permission denied by user - their choice, not actionable
|
|
26
27
|
Alert.alert(
|
|
27
28
|
'Camera Permission Required',
|
|
28
29
|
'This app needs camera access to scan barcodes. Please enable camera permissions in your device settings.',
|
|
@@ -62,7 +62,8 @@ export default {
|
|
|
62
62
|
'livenessDetectionScreen.guideHeader': 'Yüz Taraması İçin Hazırlanın',
|
|
63
63
|
'livenessDetectionScreen.guideText':
|
|
64
64
|
'Başlamadan önce lütfen aşağıdaki hususlara dikkat edin:',
|
|
65
|
-
'livenessDetectionScreen.guidePoint1':
|
|
65
|
+
'livenessDetectionScreen.guidePoint1':
|
|
66
|
+
'Gözlük, şapka veya eşarp takmayın. Lütfen uygun şekilde giyindiğinizden emin olun',
|
|
66
67
|
'livenessDetectionScreen.guidePoint2':
|
|
67
68
|
'Yüzünüzün iyi aydınlatıldığından emin olun',
|
|
68
69
|
'livenessDetectionScreen.guidePoint3': 'Arka plan gürültüsünü minimize edin',
|
package/src/Translation/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import i18n from 'i18next';
|
|
|
2
2
|
import { initReactI18next } from 'react-i18next';
|
|
3
3
|
import * as resources from './Resources';
|
|
4
4
|
|
|
5
|
+
import { trackError } from '../Shared/Libs/analytics.utils';
|
|
6
|
+
|
|
5
7
|
const getCurrentLanguage = () => {
|
|
6
8
|
try {
|
|
7
9
|
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
|
@@ -15,6 +17,13 @@ const getCurrentLanguage = () => {
|
|
|
15
17
|
return 'en';
|
|
16
18
|
} catch (error) {
|
|
17
19
|
console.error('Error detecting language:', error);
|
|
20
|
+
trackError(
|
|
21
|
+
'LANGUAGE_DETECTION_ERROR',
|
|
22
|
+
error instanceof Error ? error.message : 'Unknown error',
|
|
23
|
+
'translation_init',
|
|
24
|
+
'low',
|
|
25
|
+
{ recoverable: true, userAction: 'detect_language' }
|
|
26
|
+
).catch(() => { });
|
|
18
27
|
return 'en';
|
|
19
28
|
}
|
|
20
29
|
};
|