@sanctum-key/react-native-sdk 1.0.57 → 1.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/build/package.json +12 -2
  2. package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
  3. package/build/src/components/EnhancedCameraView.js +5 -1
  4. package/build/src/components/EnhancedCameraView.js.map +1 -1
  5. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  6. package/build/src/components/KYCElements/IDCardCapture.js +34 -13
  7. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  8. package/build/src/components/TemplateKYCExample.d.ts +2 -1
  9. package/build/src/components/TemplateKYCExample.d.ts.map +1 -1
  10. package/build/src/components/TemplateKYCExample.js +2 -2
  11. package/build/src/components/TemplateKYCExample.js.map +1 -1
  12. package/build/src/components/TemplateKYCFlowRefactored.d.ts +1 -0
  13. package/build/src/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
  14. package/build/src/components/TemplateKYCFlowRefactored.js +56 -12
  15. package/build/src/components/TemplateKYCFlowRefactored.js.map +1 -1
  16. package/build/src/config/countriesData.d.ts.map +1 -1
  17. package/build/src/config/countriesData.js +249 -77
  18. package/build/src/config/countriesData.js.map +1 -1
  19. package/build/src/config/region_mapping.d.ts.map +1 -1
  20. package/build/src/config/region_mapping.js +45 -15
  21. package/build/src/config/region_mapping.js.map +1 -1
  22. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  23. package/build/src/modules/api/CardAuthentification.js +10 -2
  24. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  25. package/build/src/utils/cropByObb.d.ts +1 -1
  26. package/build/src/utils/cropByObb.d.ts.map +1 -1
  27. package/build/src/utils/cropByObb.js +112 -5
  28. package/build/src/utils/cropByObb.js.map +1 -1
  29. package/build/src/utils/logger.d.ts +1 -0
  30. package/build/src/utils/logger.d.ts.map +1 -1
  31. package/build/src/utils/logger.js +37 -73
  32. package/build/src/utils/logger.js.map +1 -1
  33. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  34. package/build/src/web/WebKYCEntry.js +0 -2
  35. package/build/src/web/WebKYCEntry.js.map +1 -1
  36. package/package.json +12 -2
  37. package/src/components/EnhancedCameraView.tsx +4 -0
  38. package/src/components/KYCElements/IDCardCapture.tsx +49 -21
  39. package/src/components/TemplateKYCExample.tsx +4 -1
  40. package/src/components/TemplateKYCFlowRefactored.tsx +66 -11
  41. package/src/config/countriesData.ts +250 -78
  42. package/src/config/region_mapping.ts +700 -670
  43. package/src/modules/api/CardAuthentification.ts +11 -3
  44. package/src/utils/cropByObb.ts +160 -6
  45. package/src/utils/logger.ts +42 -93
  46. package/src/web/WebKYCEntry.tsx +0 -2
@@ -284,6 +284,7 @@ export async function backVerification(
284
284
 
285
285
  logger.log("Framing, Country, and Match verified successfully. Proceeding to Data Extraction.");
286
286
  let extractionResult: any = {};
287
+ let mrzCardObb: any;
287
288
 
288
289
  if (result.code === 'CA') {
289
290
  try {
@@ -331,6 +332,9 @@ export async function backVerification(
331
332
  throw new Error(mrz?.parsed_data?.status_message || 'Lecture MRZ échouée');
332
333
  }
333
334
  extractionResult.mrz = mrz;
335
+ if (mrz?.card_obb) {
336
+ mrzCardObb = mrz.card_obb;
337
+ }
334
338
  } catch (mrzError: any) {
335
339
  logger.log(`MRZ échoué: ${mrzError?.message}`);
336
340
  throw new Error(`Lecture MRZ invalide: ${mrzError?.message || 'Veuillez nettoyer l\'objectif et réessayer'}`);
@@ -339,9 +343,13 @@ export async function backVerification(
339
343
 
340
344
  let bbox: IBbox | undefined;
341
345
  let croppedBase64: string | undefined;
346
+ // For MRZ-backed documents, extract_mrz sees the final verified back image
347
+ // and provides the authoritative card bounds. Keep the template OBB as a
348
+ // fallback for barcode-only and non-MRZ document backs.
349
+ const finalCardObb = mrzCardObb || templateResponse.card_obb;
342
350
 
343
351
  try {
344
- const crop = await cropByObb(result?.path || '', templateResponse.card_obb);
352
+ const crop = await cropByObb(result?.path || '', finalCardObb);
345
353
  bbox = crop.bbox;
346
354
  croppedBase64 = crop.base64;
347
355
  } catch {
@@ -352,7 +360,7 @@ export async function backVerification(
352
360
  ...extractionResult,
353
361
  bbox,
354
362
  croppedBase64,
355
- card_obb: templateResponse.card_obb
363
+ card_obb: finalCardObb
356
364
  };
357
365
 
358
366
  } catch (e: any) {
@@ -443,4 +451,4 @@ export async function checkTemplateType(
443
451
 
444
452
  throw new Error(typeof e === 'string' ? e : (e?.message || 'Erreur de vérification du template'));
445
453
  }
446
- }
454
+ }
@@ -4,6 +4,22 @@ import * as ImageManipulator from "expo-image-manipulator";
4
4
 
5
5
  type Point = [number, number];
6
6
 
7
+ type PixelBbox = {
8
+ minX: number;
9
+ minY: number;
10
+ width: number;
11
+ height: number;
12
+ };
13
+
14
+ type BboxOrientation = 'direct' | 'clockwise' | 'counter-clockwise' | 'upside-down';
15
+
16
+ type OrientedBboxCandidate = {
17
+ bbox: PixelBbox;
18
+ orientation: BboxOrientation;
19
+ inFrameRatio: number;
20
+ score: number;
21
+ };
22
+
7
23
  export const OBB_CONFIDENCE_THRESHOLD = 0.85;
8
24
 
9
25
 
@@ -84,6 +100,105 @@ function computeAabb(points: Point[]) {
84
100
  return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
85
101
  }
86
102
 
103
+ function getInFrameRatio(bbox: PixelBbox, imageWidth: number, imageHeight: number): number {
104
+ const left = Math.max(0, bbox.minX);
105
+ const top = Math.max(0, bbox.minY);
106
+ const right = Math.min(imageWidth, bbox.minX + bbox.width);
107
+ const bottom = Math.min(imageHeight, bbox.minY + bbox.height);
108
+ const intersectionArea = Math.max(0, right - left) * Math.max(0, bottom - top);
109
+ const bboxArea = bbox.width * bbox.height;
110
+
111
+ return bboxArea > 0 ? intersectionArea / bboxArea : 0;
112
+ }
113
+
114
+ /**
115
+ * The API can return OBB coordinates in the camera sensor's pixel space while
116
+ * ImageManipulator returns an EXIF-normalized image. On phones held sideways,
117
+ * those spaces are 90 degrees apart. Select the representation which best fits
118
+ * the normalized image instead of applying the server coordinates blindly.
119
+ */
120
+ function resolveBboxOrientation(
121
+ bbox: PixelBbox,
122
+ imageWidth: number,
123
+ imageHeight: number,
124
+ preferredCardOrientation: 'landscape' | 'portrait' | 'either' = 'landscape',
125
+ ): OrientedBboxCandidate {
126
+ const candidates: Array<{ bbox: PixelBbox; orientation: BboxOrientation; priority: number }> = [
127
+ {
128
+ bbox,
129
+ orientation: 'direct',
130
+ priority: 0.75,
131
+ },
132
+ {
133
+ // Source canvas is imageHeight x imageWidth and is rotated clockwise.
134
+ bbox: {
135
+ minX: imageWidth - bbox.minY - bbox.height,
136
+ minY: bbox.minX,
137
+ width: bbox.height,
138
+ height: bbox.width,
139
+ },
140
+ orientation: 'clockwise',
141
+ priority: 0,
142
+ },
143
+ {
144
+ // Source canvas is imageHeight x imageWidth and is rotated counter-clockwise.
145
+ bbox: {
146
+ minX: bbox.minY,
147
+ minY: imageHeight - bbox.minX - bbox.width,
148
+ width: bbox.height,
149
+ height: bbox.width,
150
+ },
151
+ orientation: 'counter-clockwise',
152
+ priority: 0,
153
+ },
154
+ {
155
+ bbox: {
156
+ minX: imageWidth - bbox.minX - bbox.width,
157
+ minY: imageHeight - bbox.minY - bbox.height,
158
+ width: bbox.width,
159
+ height: bbox.height,
160
+ },
161
+ orientation: 'upside-down',
162
+ priority: -0.25,
163
+ },
164
+ ];
165
+
166
+ const scoredCandidates = candidates.map((candidate) => {
167
+ const candidateBbox = candidate.bbox;
168
+ const inFrameRatio = getInFrameRatio(candidateBbox, imageWidth, imageHeight);
169
+ const centerX = candidateBbox.minX + candidateBbox.width / 2;
170
+ const centerY = candidateBbox.minY + candidateBbox.height / 2;
171
+ const normalizedCenterDistance = Math.hypot(
172
+ (centerX - imageWidth / 2) / Math.max(1, imageWidth / 2),
173
+ (centerY - imageHeight / 2) / Math.max(1, imageHeight / 2),
174
+ );
175
+ const centerScore = 1 - Math.min(1, normalizedCenterDistance);
176
+
177
+ const isLandscapeCard = candidateBbox.width >= candidateBbox.height;
178
+ const preferredOrientationScore = preferredCardOrientation === 'either'
179
+ ? 0
180
+ : ((preferredCardOrientation === 'landscape') === isLandscapeCard ? 1 : 0);
181
+ const areaRatio = (candidateBbox.width * candidateBbox.height) / Math.max(1, imageWidth * imageHeight);
182
+ const plausibleAreaScore = Math.min(1, areaRatio / 0.08);
183
+
184
+ return {
185
+ bbox: candidateBbox,
186
+ orientation: candidate.orientation,
187
+ inFrameRatio,
188
+ score:
189
+ inFrameRatio * 100
190
+ + centerScore * 3
191
+ + preferredOrientationScore * 2
192
+ + plausibleAreaScore
193
+ + candidate.priority,
194
+ };
195
+ });
196
+
197
+ return scoredCandidates.reduce((best, candidate) => (
198
+ candidate.score > best.score ? candidate : best
199
+ ));
200
+ }
201
+
87
202
  async function cropWeb(uri: string, points: Point[]): Promise<string> {
88
203
  return new Promise((resolve, reject) => {
89
204
  const img = new Image();
@@ -159,7 +274,12 @@ async function normalizeOrientationAndGetDimensions(
159
274
  }
160
275
 
161
276
 
162
- export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tolerance: number = 0.02): Promise<string> {
277
+ export async function cropImageWithBBoxWithTolerance(
278
+ uri: string,
279
+ bbox: any,
280
+ tolerance: number = 0.02,
281
+ preferredCardOrientation: 'landscape' | 'portrait' | 'either' = 'landscape',
282
+ ): Promise<string> {
163
283
  return new Promise<string>((resolve) => {
164
284
  let isResolved = false;
165
285
 
@@ -195,10 +315,44 @@ export async function cropImageWithBBoxWithTolerance(uri: string, bbox: any, tol
195
315
  pixelWidth = bbox.width * actualWidth;
196
316
  pixelHeight = bbox.height * actualHeight;
197
317
  } else {
198
- pixelMinX = bbox.minX;
199
- pixelMinY = bbox.minY;
200
- pixelWidth = bbox.width;
201
- pixelHeight = bbox.height;
318
+ const resolvedBbox = resolveBboxOrientation(
319
+ {
320
+ minX: bbox.minX,
321
+ minY: bbox.minY,
322
+ width: bbox.width,
323
+ height: bbox.height,
324
+ },
325
+ actualWidth,
326
+ actualHeight,
327
+ preferredCardOrientation,
328
+ );
329
+
330
+ if (resolvedBbox.inFrameRatio < 0.6) {
331
+ if (!isResolved) {
332
+ isResolved = true;
333
+ clearTimeout(timeout);
334
+ console.warn('OBB does not match the normalized image bounds; returning the full image.', {
335
+ bbox,
336
+ imageSize: { width: actualWidth, height: actualHeight },
337
+ bestOrientation: resolvedBbox.orientation,
338
+ inFrameRatio: resolvedBbox.inFrameRatio,
339
+ });
340
+ resolve(normalizedUri);
341
+ }
342
+ return;
343
+ }
344
+
345
+ if (resolvedBbox.orientation !== 'direct') {
346
+ console.info('Adjusted OBB coordinates for the normalized camera orientation.', {
347
+ orientation: resolvedBbox.orientation,
348
+ imageSize: { width: actualWidth, height: actualHeight },
349
+ });
350
+ }
351
+
352
+ pixelMinX = resolvedBbox.bbox.minX;
353
+ pixelMinY = resolvedBbox.bbox.minY;
354
+ pixelWidth = resolvedBbox.bbox.width;
355
+ pixelHeight = resolvedBbox.bbox.height;
202
356
  }
203
357
 
204
358
  // STEP 3: Add tolerance and clamp to image bounds.
@@ -309,4 +463,4 @@ export async function cropToCenterScanArea(
309
463
  }
310
464
  })();
311
465
  });
312
- }
466
+ }
@@ -1,6 +1,13 @@
1
1
  import { Platform, AppState, AppStateStatus } from 'react-native';
2
- import axios from 'axios';
3
- import KYCConfig from '../config/KYCConfig';
2
+ import PostHog from 'posthog-react-native';
3
+
4
+ const posthogClient = new PostHog('phc_qjCHjCgkdVniammXrrfLrmNPoPH5uAmkzoQMhrEhGaYR', {
5
+ host: 'https://eu.i.posthog.com',
6
+ disabled: false,
7
+ captureAppLifecycleEvents: false,
8
+ flushAt: 1,
9
+ flushInterval: 1000, // Backup: flush automatically every 1 second
10
+ });
4
11
 
5
12
  const isProduction = (): boolean => {
6
13
  if (typeof __DEV__ !== 'undefined') {
@@ -11,7 +18,6 @@ const isProduction = (): boolean => {
11
18
  }
12
19
  return false;
13
20
  };
14
-
15
21
  const IS_PROD = isProduction();
16
22
 
17
23
  export const logger = {
@@ -23,32 +29,16 @@ export const logger = {
23
29
  };
24
30
 
25
31
  // ==========================================
26
- // 2. SDK TELEMETRY LOGGER (REMOTE)
32
+ // 2. SDK TELEMETRY LOGGER (POSTHOG ONLY)
27
33
  // ==========================================
28
- // 🚨 FIX 1: Changed 'warn' to 'warning' to match backend enums
29
- type LogLevel = 'info' | 'warning' | 'error' | 'debug';
30
-
31
- interface LogEntry {
32
- level: LogLevel;
33
- message: string;
34
- timestamp: string;
35
- session_id?: string;
36
- metadata?: any;
37
- }
34
+ type LogLevel = 'info' | 'warning' | 'error' | 'debug';
38
35
 
39
36
  class SDKTelemetryLogger {
40
- private queue: LogEntry[] = [];
41
- private isFlushing = false;
42
- private maxQueueSize = 3;
43
- private flushIntervalMs = 3000;
44
37
  private sessionId: string | undefined;
45
- private apiKey: string | undefined;
46
- private timerId?: ReturnType<typeof setInterval>;
47
38
  private appStateSubscription: any;
48
39
 
49
40
  constructor() {
50
- this.timerId = setInterval(() => this.flush(), this.flushIntervalMs);
51
-
41
+ // Flush PostHog when the app goes into the background
52
42
  if (Platform.OS !== 'web') {
53
43
  this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange);
54
44
  }
@@ -62,29 +52,33 @@ class SDKTelemetryLogger {
62
52
 
63
53
  public setSessionId(id: string) {
64
54
  this.sessionId = id;
65
-
66
- this.queue = this.queue.map(log => {
67
- if (!log.session_id) {
68
- return { ...log, session_id: id };
69
- }
70
- return log;
55
+ posthogClient.identify(id, {
56
+ kyc_session_id: id,
71
57
  });
72
-
73
- this.flush();
74
58
  }
75
59
 
76
60
  public clearSessionId() {
77
61
  this.sessionId = undefined;
62
+ posthogClient.reset(); // Clears identity
78
63
  }
79
64
 
80
- public setApiKey(key: string) {
81
- this.apiKey = key;
65
+ // Kept as a no-op just in case your SDK still calls this somewhere
66
+ public setApiKey(_key: string) {
67
+ // No longer needed for PostHog
68
+ }
69
+
70
+ public track(eventName: string, properties?: Record<string, any>) {
71
+ posthogClient.capture(`[SanctumKey] ${eventName}`, {
72
+ session_id: this.sessionId || null,
73
+ os: Platform.OS,
74
+ osVersion: Platform.Version,
75
+ ...properties,
76
+ });
82
77
  }
83
78
 
84
79
  private parseArgs(args: any[]): { message: string, metadata: any } {
85
80
  let message = '';
86
81
  let metadata: any = {};
87
-
88
82
  args.forEach(arg => {
89
83
  if (typeof arg === 'string') {
90
84
  message += (message ? ' ' : '') + arg;
@@ -97,7 +91,6 @@ class SDKTelemetryLogger {
97
91
  message += (message ? ' ' : '') + String(arg);
98
92
  }
99
93
  });
100
-
101
94
  return { message: message || 'No message provided', metadata };
102
95
  }
103
96
 
@@ -107,77 +100,33 @@ class SDKTelemetryLogger {
107
100
  if (!IS_PROD) {
108
101
  const devPrefix = `[SDK_TELEMETRY_${level.toUpperCase()}]`;
109
102
  if (level === 'error') console.error(devPrefix, message, metadata);
110
- else if (level === 'warning') console.warn(devPrefix, message, metadata); // Match updated level
103
+ else if (level === 'warning') console.warn(devPrefix, message, metadata);
111
104
  else console.log(devPrefix, message, metadata);
112
105
  }
113
106
 
114
- const entry: LogEntry = {
115
- level,
116
- message: message.substring(0, 500),
117
- timestamp: new Date().toISOString(),
118
- session_id: this.sessionId,
119
- metadata: {
120
- ...metadata,
121
- os: Platform.OS,
122
- osVersion: Platform.Version,
123
- }
107
+ const payloadMetadata = {
108
+ ...metadata,
109
+ os: Platform.OS,
110
+ osVersion: Platform.Version,
124
111
  };
125
112
 
126
- this.queue.push(entry);
127
-
128
- const isError = level === 'error';
129
- const hitLimit = this.queue.length >= this.maxQueueSize;
113
+ const levelLabel = level.toUpperCase();
114
+ posthogClient.capture(`[SanctumKey] ${message}`, {
115
+ session_id: this.sessionId || null,
116
+ log_level: levelLabel,
117
+ ...payloadMetadata,
118
+ });
130
119
 
131
- if (isError || (this.sessionId && hitLimit)) {
120
+ if (level === 'error') {
132
121
  this.flush();
133
122
  }
134
123
  }
135
124
 
136
125
  public async flush() {
137
- if (this.queue.length === 0 || this.isFlushing) return;
138
-
139
- const batch = this.queue.map(log => ({
140
- ...log,
141
- session_id: log.session_id || 'pre-session-startup'
142
- }));
143
-
144
- this.isFlushing = true;
145
- this.queue = [];
146
-
147
- try {
148
- const url = `${KYCConfig.getBackendUrl()}/core/sdk/logs/`;
149
-
150
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
151
- if (this.apiKey) {
152
- headers['Authorization'] = `ApiKey ${this.apiKey}`;
153
- }
154
-
155
- await axios.post(url, { logs: batch }, {
156
- headers,
157
- timeout: 5000
158
- });
159
- } catch (error: any) {
160
- // 🚨 FIX 2: Stop Infinite Loops
161
- // If the backend actively rejects our payload (400-499 error code), do NOT requeue it.
162
- const isClientError = error.response && error.response.status >= 400 && error.response.status < 500;
163
-
164
- if (!isClientError) {
165
- // Only retry if it's a network failure or a 5xx Server Error
166
- if (this.queue.length < 50) {
167
- this.queue = [...batch, ...this.queue].slice(0, 50);
168
- }
169
- } else {
170
- if (!IS_PROD) {
171
- console.error("[SDK_TELEMETRY] Backend rejected log payload. Dropping batch to prevent infinite loop.", error.response.data);
172
- }
173
- }
174
- } finally {
175
- this.isFlushing = false;
176
- }
126
+ await posthogClient.flush();
177
127
  }
178
128
 
179
129
  public destroy() {
180
- if (this.timerId) clearInterval(this.timerId);
181
130
  if (this.appStateSubscription) this.appStateSubscription.remove();
182
131
  this.flush();
183
132
  }
@@ -192,10 +141,10 @@ export const sdkLogger = {
192
141
  destroy: () => telemetryInstance.destroy(),
193
142
  setApiKey: (key: string) => telemetryInstance.setApiKey(key),
194
143
 
144
+ track: (eventName: string, properties?: Record<string, any>) => telemetryInstance.track(eventName, properties),
195
145
  log: (...args: any[]) => telemetryInstance.processLog('info', ...args),
196
146
  info: (...args: any[]) => telemetryInstance.processLog('info', ...args),
197
- // 🚨 FIX 1 (Continued): Map warn() function to send 'warning' to backend
198
- warn: (...args: any[]) => telemetryInstance.processLog('warning', ...args),
147
+ warn: (...args: any[]) => telemetryInstance.processLog('warning', ...args),
199
148
  error: (...args: any[]) => telemetryInstance.processLog('error', ...args),
200
149
  debug: (...args: any[]) => telemetryInstance.processLog('debug', ...args),
201
150
  };
@@ -42,7 +42,6 @@ interface VerificationSteps {
42
42
  liveness_checked?: boolean;
43
43
  }
44
44
 
45
- // 🚨 MODIFIED: Accepts the webhook secret to generate the signature
46
45
  const postWebhook = async (url: string, payload: unknown, secret?: string, timeoutMs: number = 5000) => {
47
46
  const body = JSON.stringify(payload);
48
47
 
@@ -50,7 +49,6 @@ const postWebhook = async (url: string, payload: unknown, secret?: string, timeo
50
49
  'Content-Type': 'application/json',
51
50
  };
52
51
 
53
- // 🚨 Generate HMAC SHA-256 Signature if secret is provided
54
52
  if (secret) {
55
53
  const signature = CryptoJS.HmacSHA256(body, secret).toString(CryptoJS.enc.Hex);
56
54
  headers['x-sanctumkey-signature'] = signature;