datakeen-session-react 1.1.199-rc.4 → 1.1.200-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var tslib_es6 = require('../node_modules/tslib/tslib.es6.js');
4
+ var cameraResolution = require('../utils/cameraResolution.js');
4
5
  var professionalImageQuality = require('./professionalImageQuality.js');
5
6
 
6
7
  // Permit to enable development mode to test without a camera
@@ -123,18 +124,18 @@ var CameraService = /** @class */ (function () {
123
124
  };
124
125
  CameraService.prototype._doStartCamera = function (videoElement, options) {
125
126
  return tslib_es6.__awaiter(this, void 0, void 0, function () {
126
- var facingMode, preferImageCapture, desiredWidth, desiredHeight, videoConstraints, stream, err_2, errorMessage;
127
- var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
128
- return tslib_es6.__generator(this, function (_o) {
129
- switch (_o.label) {
127
+ var facingMode, preferImageCapture, desiredWidth, desiredHeight, videoConstraints, stream, retriedWithHd, err_2, errorMessage;
128
+ var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
129
+ return tslib_es6.__generator(this, function (_t) {
130
+ switch (_t.label) {
130
131
  case 0:
131
132
  facingMode = (_b = options.facingMode) !== null && _b !== void 0 ? _b : "environment";
132
133
  preferImageCapture = (_c = options.preferImageCapture) !== null && _c !== void 0 ? _c : true;
133
134
  desiredWidth = (_e = (_d = options.idealResolution) === null || _d === void 0 ? void 0 : _d.width) !== null && _e !== void 0 ? _e : DEFAULT_RESOLUTION.width;
134
135
  desiredHeight = (_g = (_f = options.idealResolution) === null || _f === void 0 ? void 0 : _f.height) !== null && _g !== void 0 ? _g : DEFAULT_RESOLUTION.height;
135
- _o.label = 1;
136
+ _t.label = 1;
136
137
  case 1:
137
- _o.trys.push([1, 8, , 9]);
138
+ _t.trys.push([1, 10, , 11]);
138
139
  // Close any existing stream before opening a new one
139
140
  if (this.stream) {
140
141
  this.stream.getTracks().forEach(function (t) { return t.stop(); });
@@ -157,28 +158,45 @@ var CameraService = /** @class */ (function () {
157
158
  video: videoConstraints,
158
159
  })];
159
160
  case 2:
160
- stream = _o.sent();
161
+ stream = _t.sent();
161
162
  this.stream = stream;
162
163
  this.videoTrack = (_j = stream.getVideoTracks()[0]) !== null && _j !== void 0 ? _j : null;
163
164
  this.lastTrackSettings = (_l = (_k = this.videoTrack) === null || _k === void 0 ? void 0 : _k.getSettings()) !== null && _l !== void 0 ? _l : null;
165
+ retriedWithHd = false;
166
+ if (!(cameraResolution.isBelowHd((_m = this.lastTrackSettings) !== null && _m !== void 0 ? _m : undefined) && desiredWidth / desiredHeight !== 16 / 9)) return [3 /*break*/, 4];
167
+ retriedWithHd = true;
168
+ stream.getTracks().forEach(function (track) { return track.stop(); });
169
+ this.stream = null;
170
+ this.videoTrack = null;
171
+ this.lastTrackSettings = null;
172
+ return [4 /*yield*/, navigator.mediaDevices.getUserMedia({
173
+ video: tslib_es6.__assign(tslib_es6.__assign({}, videoConstraints), { width: { ideal: 1920 }, height: { ideal: 1080 } }),
174
+ })];
175
+ case 3:
176
+ stream = _t.sent();
177
+ this.stream = stream;
178
+ this.videoTrack = (_o = stream.getVideoTracks()[0]) !== null && _o !== void 0 ? _o : null;
179
+ this.lastTrackSettings = (_q = (_p = this.videoTrack) === null || _p === void 0 ? void 0 : _p.getSettings()) !== null && _q !== void 0 ? _q : null;
180
+ _t.label = 4;
181
+ case 4:
164
182
  console.debug("[CameraService] Camera opened", {
165
183
  desired: { width: desiredWidth, height: desiredHeight },
166
184
  actual: this.lastTrackSettings,
167
185
  });
168
- if (!this.videoTrack) return [3 /*break*/, 6];
169
- _o.label = 3;
170
- case 3:
171
- _o.trys.push([3, 5, , 6]);
186
+ if (!this.videoTrack) return [3 /*break*/, 8];
187
+ _t.label = 5;
188
+ case 5:
189
+ _t.trys.push([5, 7, , 8]);
172
190
  return [4 /*yield*/, this.videoTrack.applyConstraints({
173
191
  advanced: [{ focusMode: "continuous" }],
174
192
  })];
175
- case 4:
176
- _o.sent();
177
- return [3 /*break*/, 6];
178
- case 5:
179
- _o.sent();
180
- return [3 /*break*/, 6];
181
193
  case 6:
194
+ _t.sent();
195
+ return [3 /*break*/, 8];
196
+ case 7:
197
+ _t.sent();
198
+ return [3 /*break*/, 8];
199
+ case 8:
182
200
  if (supportsImageCapture() && this.videoTrack && preferImageCapture) {
183
201
  try {
184
202
  this.imageCapture = new ImageCapture(this.videoTrack);
@@ -196,15 +214,18 @@ var CameraService = /** @class */ (function () {
196
214
  }
197
215
  videoElement.srcObject = stream;
198
216
  return [4 /*yield*/, this.waitForVideoReady(videoElement)];
199
- case 7:
200
- _o.sent();
217
+ case 9:
218
+ _t.sent();
201
219
  return [2 /*return*/, {
202
220
  success: true,
203
- settings: (_m = this.lastTrackSettings) !== null && _m !== void 0 ? _m : undefined,
221
+ settings: (_r = this.lastTrackSettings) !== null && _r !== void 0 ? _r : undefined,
204
222
  captureMethod: this.captureMethod,
223
+ requested: { w: desiredWidth, h: desiredHeight },
224
+ retriedWithHd: retriedWithHd,
225
+ label: (_s = this.videoTrack) === null || _s === void 0 ? void 0 : _s.label,
205
226
  }];
206
- case 8:
207
- err_2 = _o.sent();
227
+ case 10:
228
+ err_2 = _t.sent();
208
229
  console.error("Erreur lors de l'accès à la caméra:", err_2);
209
230
  errorMessage = "Une erreur inconnue s'est produite lors de l'accès à la caméra.";
210
231
  if (err_2 instanceof DOMException) {
@@ -220,7 +241,7 @@ var CameraService = /** @class */ (function () {
220
241
  }
221
242
  }
222
243
  return [2 /*return*/, { success: false, error: errorMessage }];
223
- case 9: return [2 /*return*/];
244
+ case 11: return [2 /*return*/];
224
245
  }
225
246
  });
226
247
  });
@@ -14,7 +14,8 @@ function getSessionCredential(sessionId) {
14
14
  function credentialForRequest(url) {
15
15
  if (/^[a-z]+:/i.test(url) || url.startsWith('//'))
16
16
  return undefined;
17
- var match = url.match(/^\/?session\/sdk\/([^/?]+)/);
17
+ // Routes SDK et biométrie Unissey : toutes adressent une seule session par son id.
18
+ var match = url.match(/^\/?session\/(?:sdk|unissey)\/([^/?]+)/);
18
19
  return match ? credentials.get(decodeURIComponent(match[1])) : undefined;
19
20
  }
20
21
 
@@ -5,12 +5,21 @@ var api = require('./api.js');
5
5
  var auditTrailService = require('./auditTrailService.js');
6
6
  var sessionMemoryStore = require('./sessionMemoryStore.js');
7
7
 
8
- /**
9
- * Fetches session data by ID from the Datakeen backend
10
- *
11
- * @param sessionId - The unique identifier of the session
12
- * @returns The session data
13
- */
8
+ /** Reports camera diagnostics without updating local session state. */
9
+ function reportCaptureDiagnostics(sessionId, nodeId, diag) {
10
+ return tslib_es6.__awaiter(this, void 0, void 0, function () {
11
+ var _a;
12
+ return tslib_es6.__generator(this, function (_b) {
13
+ switch (_b.label) {
14
+ case 0: return [4 /*yield*/, api.apiService.patch("/session/sdk/".concat(sessionId), { captureDiagnostics: (_a = {}, _a[nodeId] = diag, _a) })];
15
+ case 1:
16
+ _b.sent();
17
+ return [2 /*return*/];
18
+ }
19
+ });
20
+ });
21
+ }
22
+ /** Fetches session data by ID from the Datakeen backend. */
14
23
  var fetchSessionById = function (sessionId) { return tslib_es6.__awaiter(void 0, void 0, void 0, function () {
15
24
  var response, error_1;
16
25
  return tslib_es6.__generator(this, function (_a) {
@@ -518,6 +527,7 @@ exports.getRunPrefillData = getRunPrefillData;
518
527
  exports.hasOutgoingEdgeForHandle = hasOutgoingEdgeForHandle;
519
528
  exports.isSessionExpired = isSessionExpired;
520
529
  exports.reconstructHistoryToStep = reconstructHistoryToStep;
530
+ exports.reportCaptureDiagnostics = reportCaptureDiagnostics;
521
531
  exports.reportSelfieFailure = reportSelfieFailure;
522
532
  exports.resetNfcScan = resetNfcScan;
523
533
  exports.sendClientInfo = sendClientInfo;
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ var HD_MIN = { long: 1280, short: 720 };
4
+ function isBelowHd(settings) {
5
+ var _a = settings !== null && settings !== void 0 ? settings : {}, width = _a.width, height = _a.height;
6
+ if (!width || !height)
7
+ return false;
8
+ return Math.max(width, height) < HD_MIN.long || Math.min(width, height) < HD_MIN.short;
9
+ }
10
+
11
+ exports.HD_MIN = HD_MIN;
12
+ exports.isBelowHd = isBelowHd;
@@ -304,7 +304,7 @@ var DocumentCollection = function (_a) {
304
304
  case "before-recto":
305
305
  return jsx(BeforePhoto, { setStep: function () { return setPhotoSubStep("recto"); }, isSingleSided: !requiresTwoSides_1, onBack: handleBack });
306
306
  case "recto":
307
- return jsx(Photo, { onCapture: function (img) { return handlePhotoCapture(img, "recto"); }, automaticCapture: false, maskType: "document" });
307
+ return jsx(Photo, { sessionId: sessionId, nodeId: node.id, onCapture: function (img) { return handlePhotoCapture(img, "recto"); }, automaticCapture: false, maskType: "document" });
308
308
  case "preview-recto":
309
309
  return (jsx(PhotoPreview, { imageUrl: capturedImages.recto, side: "recto", isSingleSided: !requiresTwoSides_1, onValidate: function () { return setPhotoSubStep(requiresTwoSides_1 ? "before-verso" : "confirmation"); }, onRetake: function () {
310
310
  setCapturedImages(function (prev) { var next = __assign({}, prev); delete next.recto; return next; });
@@ -313,7 +313,7 @@ var DocumentCollection = function (_a) {
313
313
  case "before-verso":
314
314
  return jsx(BeforeVersoPhoto, { setStep: function () { return setPhotoSubStep("verso"); } });
315
315
  case "verso":
316
- return jsx(Photo, { onCapture: function (img) { return handlePhotoCapture(img, "verso"); }, automaticCapture: false, maskType: "document" });
316
+ return jsx(Photo, { sessionId: sessionId, nodeId: node.id, onCapture: function (img) { return handlePhotoCapture(img, "verso"); }, automaticCapture: false, maskType: "document" });
317
317
  case "preview-verso":
318
318
  return (jsx(PhotoPreview, { imageUrl: capturedImages.verso, side: "verso", onValidate: function () { return setPhotoSubStep("confirmation"); }, onRetake: function () {
319
319
  setCapturedImages(function (prev) { var next = __assign({}, prev); delete next.verso; return next; });
@@ -10,6 +10,9 @@ import { isServiceErrorCode } from '../../utils/analysisErrors.js';
10
10
  var DocumentError = function (_a) {
11
11
  var documentType = _a.documentType, documentTypeLabel = _a.documentTypeLabel, onRetry = _a.onRetry, onContinueAnyway = _a.onContinueAnyway, onBackToUserInput = _a.onBackToUserInput, _b = _a.retryCount, retryCount = _b === void 0 ? 0 : _b, _c = _a.predictions, predictions = _c === void 0 ? [] : _c; _a.errorDetails; _a.allowResubmission; var maxResubmissionAttempts = _a.maxResubmissionAttempts, _d = _a.isRetryAllowed, isRetryAllowed = _d === void 0 ? true : _d, errorCode = _a.errorCode, _e = _a.blockContinue, blockContinue = _e === void 0 ? false : _e;
12
12
  var t = useI18n().t;
13
+ // APP3-1955: a service refusal (no credits / quota) blocks the journey,
14
+ // whatever node.allowContinueOnFailure says.
15
+ var continueAnyway = isServiceErrorCode(errorCode) ? undefined : onContinueAnyway;
13
16
  var _f = useState([]), validationPoints = _f[0], setValidationPoints = _f[1];
14
17
  useEffect(function () {
15
18
  // Use shared utility to get validation points
@@ -58,7 +61,7 @@ var DocumentError = function (_a) {
58
61
  : t("errors.resubmission.no_resubmission_allowed") })] })), !blockContinue && getUserGuidance() && (jsxs("div", { className: "bg-yellow-50 border border-yellow-200 rounded-lg p-4", children: [jsx("h3", { className: "font-medium text-yellow-900 mb-2", children: isServiceErrorCode(errorCode)
59
62
  ? // Quota / out of credits: nothing for the end user to improve.
60
63
  t("errors.service_refusal_title")
61
- : "\uD83D\uDCA1 ".concat(t("errors.improvement_tips", "Conseils d'amélioration")) }), jsx("p", { className: "text-sm text-yellow-800", children: getUserGuidance() })] })), !blockContinue && isRetryAllowed && (jsxs("div", { className: "bg-blue-50 border border-blue-200 rounded-lg p-4", children: [jsx("h3", { className: "font-medium text-blue-900 mb-2", children: t("errors.next_steps", "Prochaines étapes") }), jsx("p", { className: "text-sm text-blue-800", children: t("errors.next_steps_description", "Veuillez reprendre les photos de votre document en suivant les conseils d'amélioration pour une meilleure qualité.") })] }))] }) }), jsx("div", { className: "sticky bottom-0 md:static bg-white border-t md:border-t-0 p-4 md:p-0 md:pb-8", children: jsxs("div", { className: "w-full max-w-md mx-auto", children: [jsx("div", { className: "flex flex-col space-y-3 md:hidden", children: blockContinue ? (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", children: t("errors.back_to_user_input", "Retour aux informations d'identité") })), retryCount >= 1 && isRetryAllowed && onContinueAnyway && (jsx(ButtonDesktop, { onClick: onContinueAnyway, type: "back", children: t("errors.continue_anyway", "Poursuivre tout de même") })), isRetryAllowed && (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })), !isRetryAllowed && onContinueAnyway && (jsx(ButtonDesktop, { onClick: onContinueAnyway, type: "back", children: t("errors.continue_anyway", "Poursuivre tout de même") }))] })) }), jsx("div", { className: "hidden md:flex flex-col space-y-3", children: jsx("div", { className: "flex justify-center space-x-3", children: blockContinue ? (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", children: t("errors.back_to_user_input", "Retour aux informations d'identité") })), retryCount >= 1 && isRetryAllowed && onContinueAnyway && (jsx(ButtonDesktop, { onClick: onContinueAnyway, type: "back", children: t("errors.continue_anyway", "Poursuivre tout de même") })), isRetryAllowed && (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })), !isRetryAllowed && onContinueAnyway && (jsx(ButtonDesktop, { onClick: onContinueAnyway, type: "continue", children: t("errors.continue_anyway", "Poursuivre tout de même") }))] })) }) })] }) })] }));
64
+ : "\uD83D\uDCA1 ".concat(t("errors.improvement_tips", "Conseils d'amélioration")) }), jsx("p", { className: "text-sm text-yellow-800", children: getUserGuidance() })] })), !blockContinue && isRetryAllowed && (jsxs("div", { className: "bg-blue-50 border border-blue-200 rounded-lg p-4", children: [jsx("h3", { className: "font-medium text-blue-900 mb-2", children: t("errors.next_steps", "Prochaines étapes") }), jsx("p", { className: "text-sm text-blue-800", children: t("errors.next_steps_description", "Veuillez reprendre les photos de votre document en suivant les conseils d'amélioration pour une meilleure qualité.") })] }))] }) }), jsx("div", { className: "sticky bottom-0 md:static bg-white border-t md:border-t-0 p-4 md:p-0 md:pb-8", children: jsxs("div", { className: "w-full max-w-md mx-auto", children: [jsx("div", { className: "flex flex-col space-y-3 md:hidden", children: blockContinue ? (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", children: t("errors.back_to_user_input", "Retour aux informations d'identité") })), retryCount >= 1 && isRetryAllowed && continueAnyway && (jsx(ButtonDesktop, { onClick: continueAnyway, type: "back", children: t("errors.continue_anyway", "Poursuivre tout de même") })), isRetryAllowed && (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })), !isRetryAllowed && continueAnyway && (jsx(ButtonDesktop, { onClick: continueAnyway, type: "back", children: t("errors.continue_anyway", "Poursuivre tout de même") }))] })) }), jsx("div", { className: "hidden md:flex flex-col space-y-3", children: jsx("div", { className: "flex justify-center space-x-3", children: blockContinue ? (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", children: t("errors.back_to_user_input", "Retour aux informations d'identité") })), retryCount >= 1 && isRetryAllowed && continueAnyway && (jsx(ButtonDesktop, { onClick: continueAnyway, type: "back", children: t("errors.continue_anyway", "Poursuivre tout de même") })), isRetryAllowed && (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification", "Recommencer la vérification") })), !isRetryAllowed && continueAnyway && (jsx(ButtonDesktop, { onClick: continueAnyway, type: "continue", children: t("errors.continue_anyway", "Poursuivre tout de même") }))] })) }) })] }) })] }));
62
65
  };
63
66
 
64
67
  export { DocumentError as default };
@@ -7,27 +7,31 @@ import CameraError from './CameraError.js';
7
7
  import CameraMask from './CameraMask.js';
8
8
  import OrientationToggle from './OrientationToggle.js';
9
9
  import { useI18n } from '../../hooks/useI18n.js';
10
+ import { isBelowHd } from '../../utils/cameraResolution.js';
11
+ import { reportCaptureDiagnostics } from '../../services/sessionService.js';
10
12
  import useIsMobile from '../../hooks/useIsMobile.js';
11
13
 
12
14
  var Photo = function (_a) {
13
- var onCapture = _a.onCapture, _b = _a.automaticCapture, automaticCapture = _b === void 0 ? false : _b, _c = _a.maskType, maskType = _c === void 0 ? "id" : _c;
15
+ var onCapture = _a.onCapture, sessionId = _a.sessionId, nodeId = _a.nodeId, _b = _a.automaticCapture, automaticCapture = _b === void 0 ? false : _b, _c = _a.maskType, maskType = _c === void 0 ? "id" : _c;
14
16
  var isMobile = useIsMobile(768);
15
17
  var videoRef = useRef(null);
16
- var _d = useState(false), isDetecting = _d[0], setIsDetecting = _d[1];
17
- var _e = useState("environment"), facingMode = _e[0], setFacingMode = _e[1];
18
- var _f = useState(null), cameraError = _f[0], setCameraError = _f[1];
19
- var _g = useState(window.matchMedia("(orientation: portrait)").matches), isPortrait = _g[0], setIsPortrait = _g[1];
20
- var _h = useState(false), isCountingDown = _h[0], setIsCountingDown = _h[1];
21
- var _j = useState(0), countdown = _j[0], setCountdown = _j[1];
22
- var _k = useState(false), documentDetected = _k[0], setDocumentDetected = _k[1];
23
- var _l = useState(undefined), qualityHint = _l[0], setQualityHint = _l[1];
24
- var _m = useState(true), isInitializing = _m[0], setIsInitializing = _m[1];
25
- var _o = useState(false), isProcessing = _o[0], setIsProcessing = _o[1];
26
- var _p = useState(null), captureError = _p[0], setCaptureError = _p[1];
27
- var _q = useState(false), showCaptureFlash = _q[0], setShowCaptureFlash = _q[1];
18
+ var _d = useState(false), lowResolution = _d[0], setLowResolution = _d[1];
19
+ var _e = useState(0), cameraAttempt = _e[0], setCameraAttempt = _e[1];
20
+ var _f = useState(false), isDetecting = _f[0], setIsDetecting = _f[1];
21
+ var _g = useState("environment"), facingMode = _g[0], setFacingMode = _g[1];
22
+ var _h = useState(null), cameraError = _h[0], setCameraError = _h[1];
23
+ var _j = useState(window.matchMedia("(orientation: portrait)").matches), isPortrait = _j[0], setIsPortrait = _j[1];
24
+ var _k = useState(false), isCountingDown = _k[0], setIsCountingDown = _k[1];
25
+ var _l = useState(0), countdown = _l[0], setCountdown = _l[1];
26
+ var _m = useState(false), documentDetected = _m[0], setDocumentDetected = _m[1];
27
+ var _o = useState(undefined), qualityHint = _o[0], setQualityHint = _o[1];
28
+ var _p = useState(true), isInitializing = _p[0], setIsInitializing = _p[1];
29
+ var _q = useState(false), isProcessing = _q[0], setIsProcessing = _q[1];
30
+ var _r = useState(null), captureError = _r[0], setCaptureError = _r[1];
31
+ var _s = useState(false), showCaptureFlash = _s[0], setShowCaptureFlash = _s[1];
28
32
  var t = useI18n().t;
29
- var _r = useState(null); _r[0]; var setLastCaptureMeta = _r[1];
30
- var _s = useState(null), maskRect = _s[0], setMaskRect = _s[1];
33
+ var _t = useState(null); _t[0]; var setLastCaptureMeta = _t[1];
34
+ var _u = useState(null), maskRect = _u[0], setMaskRect = _u[1];
31
35
  var countdownIntervalRef = useRef(null);
32
36
  var detectionHandlerRef = useRef(null);
33
37
  var flashTimeoutRef = useRef(null);
@@ -216,11 +220,13 @@ var Photo = function (_a) {
216
220
  useEffect(function () {
217
221
  var isCancelled = false;
218
222
  var initCamera = function () { return __awaiter(void 0, void 0, void 0, function () {
219
- var result;
220
- return __generator(this, function (_a) {
221
- switch (_a.label) {
223
+ var result, belowHd;
224
+ var _a, _b, _c, _d;
225
+ return __generator(this, function (_e) {
226
+ switch (_e.label) {
222
227
  case 0:
223
228
  setCameraError(null);
229
+ setLowResolution(false);
224
230
  setIsInitializing(true);
225
231
  return [4 /*yield*/, cameraService.startCamera(videoRef.current, {
226
232
  facingMode: facingMode,
@@ -228,11 +234,25 @@ var Photo = function (_a) {
228
234
  preferImageCapture: true,
229
235
  })];
230
236
  case 1:
231
- result = _a.sent();
237
+ result = _e.sent();
232
238
  if (isCancelled)
233
239
  return [2 /*return*/];
234
240
  setIsInitializing(false);
235
241
  if (result.success) {
242
+ belowHd = isBelowHd(result.settings);
243
+ setLowResolution(belowHd);
244
+ if (sessionId && nodeId) {
245
+ void reportCaptureDiagnostics(sessionId, nodeId, {
246
+ requested: result.requested,
247
+ obtained: { w: (_a = result.settings) === null || _a === void 0 ? void 0 : _a.width, h: (_b = result.settings) === null || _b === void 0 ? void 0 : _b.height, frameRate: (_c = result.settings) === null || _c === void 0 ? void 0 : _c.frameRate },
248
+ retriedWithHd: (_d = result.retriedWithHd) !== null && _d !== void 0 ? _d : false,
249
+ devicePixelRatio: window.devicePixelRatio,
250
+ cameraLabel: result.label,
251
+ lowResolutionWarningShown: belowHd,
252
+ facingMode: facingMode,
253
+ at: new Date().toISOString(),
254
+ }).catch(function () { });
255
+ }
236
256
  setLastCaptureMeta(null);
237
257
  // Detection also runs in manual mode: it only drives the visual
238
258
  // feedback on the mask (documentDetected), it never starts the
@@ -260,7 +280,7 @@ var Photo = function (_a) {
260
280
  flashTimeoutRef.current = null;
261
281
  }
262
282
  };
263
- }, [facingMode, clearCountdown, stopDetection]);
283
+ }, [facingMode, cameraAttempt, sessionId, nodeId, clearCountdown, stopDetection]);
264
284
  useEffect(function () {
265
285
  if (!isDetecting || !videoRef.current) {
266
286
  return;
@@ -303,21 +323,7 @@ var Photo = function (_a) {
303
323
  setIsInitializing(true);
304
324
  stopDetection();
305
325
  cameraService.stopCamera();
306
- cameraService
307
- .startCamera(videoRef.current, {
308
- facingMode: facingMode,
309
- idealResolution: { width: 4096, height: 3072 },
310
- preferImageCapture: true,
311
- })
312
- .then(function (result) {
313
- setIsInitializing(false);
314
- if (result.success) {
315
- setIsDetecting(true);
316
- }
317
- else if (result.error) {
318
- setCameraError(result.error);
319
- }
320
- });
326
+ setCameraAttempt(function (attempt) { return attempt + 1; });
321
327
  };
322
328
  var handleManualCapture = function () {
323
329
  if (isProcessing) {
@@ -341,7 +347,7 @@ var Photo = function (_a) {
341
347
  zIndex: isMobile ? 50 : "auto",
342
348
  height: "100%",
343
349
  width: "100%",
344
- }, children: [jsx("div", { className: "flex-1 relative overflow-hidden bg-black", children: cameraError ? (jsx("div", { className: "flex items-center justify-center h-full px-4 py-6", children: jsx(CameraError, { errorMessage: cameraError, onRetry: handleRetry }) })) : (jsxs(Fragment, { children: [isInitializing && (jsxs("div", { "data-testid": "camera-initializing", className: "absolute inset-0 z-30 flex flex-col items-center justify-center gap-3 bg-black", children: [jsx("div", { className: "w-10 h-10 border-4 border-white border-t-transparent rounded-full animate-spin" }), jsx("span", { className: "text-white text-sm", children: t("id_check.photo.initializing", "Ouverture de la caméra…") })] })), isProcessing && (jsxs("div", { "data-testid": "camera-processing", className: "absolute inset-0 z-30 flex flex-col items-center justify-center gap-3 bg-black bg-opacity-70", children: [jsx("div", { className: "w-10 h-10 border-4 border-white border-t-transparent rounded-full animate-spin" }), jsx("span", { className: "text-white text-sm", children: t("id_check.photo.processing", "Traitement de l'image…") })] })), jsx("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: true, className: "absolute inset-0 w-full h-full object-cover z-0 bg-black ".concat(facingMode === "user" ? "scale-x-[-1]" : "") }), jsx("div", { className: "absolute top-4 left-1/2 transform -translate-x-1/2 z-50", children: jsx("span", { className: "text-white z-50 text-sm font-semibold drop-shadow-lg", children: automaticCapture ? (jsx("span", { className: "animate-pulse", children: t("id_check.photo.mode_autocapture", "Autocapture activée") })) : (jsx("span", { className: "bg-orange-500 px-2 py-1 rounded hidden", children: t("id_check.photo.mode_manual", "Mode manuel") })) }) }), jsx(CameraMask, { isDetecting: isDetecting, isPortrait: isPortrait, facingMode: facingMode, isCountingDown: isCountingDown, countdown: countdown, maskType: maskType, documentDetected: documentDetected, qualityHint: qualityHint, onMaskRectChange: setMaskRect }), jsx("div", { className: "absolute bottom-20 left-0 right-0 z-20 pointer-events-none", children: jsx("div", { className: "text-center px-4", children: jsx("div", { className: "bg-black bg-opacity-60 text-white text-sm px-4 py-2 rounded-lg inline-block", children: t("id_check.photo.position_document", "Posez le document à plat ou tenez-le dans le cadre") }) }) }), jsx(OrientationToggle, { onCameraToggle: toggleCamera }), showCaptureFlash && (jsx("div", { "data-testid": "capture-flash", className: "absolute inset-0 z-40 bg-white pointer-events-none animate-pulse" }))] })) }), jsx("div", { className: "relative z-20 bg-black bg-opacity-80 p-4", children: jsxs("div", { className: "flex flex-col items-center space-y-4", children: [jsx("button", { onClick: handleManualCapture, className: "w-16 h-16 rounded-full border-4 border-white flex items-center justify-center transition-all duration-150 bg-transparent hover:bg-white hover:bg-opacity-20 cursor-pointer ".concat(showCaptureFlash ? "scale-90 ring-4 ring-[#11E5C5]" : ""), children: jsx("div", { className: "w-8 h-8 bg-white rounded-full" }) }), jsxs("div", { className: "text-white text-center text-sm", children: [jsxs("div", { className: "flex items-center justify-center space-x-2", children: [jsx("div", { className: "w-2 h-2 rounded-full ".concat(automaticCapture
350
+ }, children: [jsx("div", { className: "flex-1 relative overflow-hidden bg-black", children: cameraError ? (jsx("div", { className: "flex items-center justify-center h-full px-4 py-6", children: jsx(CameraError, { errorMessage: cameraError, onRetry: handleRetry }) })) : (jsxs(Fragment, { children: [isInitializing && (jsxs("div", { "data-testid": "camera-initializing", className: "absolute inset-0 z-30 flex flex-col items-center justify-center gap-3 bg-black", children: [jsx("div", { className: "w-10 h-10 border-4 border-white border-t-transparent rounded-full animate-spin" }), jsx("span", { className: "text-white text-sm", children: t("id_check.photo.initializing", "Ouverture de la caméra…") })] })), isProcessing && (jsxs("div", { "data-testid": "camera-processing", className: "absolute inset-0 z-30 flex flex-col items-center justify-center gap-3 bg-black bg-opacity-70", children: [jsx("div", { className: "w-10 h-10 border-4 border-white border-t-transparent rounded-full animate-spin" }), jsx("span", { className: "text-white text-sm", children: t("id_check.photo.processing", "Traitement de l'image…") })] })), jsx("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: true, className: "absolute inset-0 w-full h-full object-cover z-0 bg-black ".concat(facingMode === "user" ? "scale-x-[-1]" : "") }), jsx("div", { className: "absolute top-4 left-1/2 transform -translate-x-1/2 z-50", children: jsx("span", { className: "text-white z-50 text-sm font-semibold drop-shadow-lg", children: automaticCapture ? (jsx("span", { className: "animate-pulse", children: t("id_check.photo.mode_autocapture", "Autocapture activée") })) : (jsx("span", { className: "bg-orange-500 px-2 py-1 rounded hidden", children: t("id_check.photo.mode_manual", "Mode manuel") })) }) }), jsx(CameraMask, { isDetecting: isDetecting, isPortrait: isPortrait, facingMode: facingMode, isCountingDown: isCountingDown, countdown: countdown, maskType: maskType, documentDetected: documentDetected, qualityHint: qualityHint, onMaskRectChange: setMaskRect }), jsx("div", { className: "absolute bottom-20 left-0 right-0 z-20 pointer-events-none", children: jsx("div", { className: "text-center px-4", children: jsx("div", { className: "bg-black bg-opacity-60 text-white text-sm px-4 py-2 rounded-lg inline-block", children: t("id_check.photo.position_document", "Posez le document à plat ou tenez-le dans le cadre") }) }) }), jsx(OrientationToggle, { onCameraToggle: toggleCamera }), showCaptureFlash && (jsx("div", { "data-testid": "capture-flash", className: "absolute inset-0 z-40 bg-white pointer-events-none animate-pulse" }))] })) }), lowResolution && !isInitializing && !cameraError && (jsxs("div", { role: "status", className: "relative z-20 shrink-0 border border-orange-200 bg-orange-50 p-4 text-sm text-orange-800", children: [jsx("h3", { className: "mb-2 font-medium text-orange-900", children: t("id_check.camera.low_resolution.title") }), jsx("p", { children: t("id_check.camera.low_resolution.body") }), jsx("p", { className: "mt-2", children: t("id_check.camera.low_resolution.tips") }), jsx("button", { type: "button", onClick: handleRetry, disabled: isProcessing, className: "mt-2 min-h-11 rounded border border-orange-800 px-4 py-2 font-medium hover:bg-orange-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-50", children: t("id_check.camera.low_resolution.retry") })] })), jsx("div", { className: "relative z-20 bg-black bg-opacity-80 p-4", children: jsxs("div", { className: "flex flex-col items-center space-y-4", children: [jsx("button", { onClick: handleManualCapture, className: "w-16 h-16 rounded-full border-4 border-white flex items-center justify-center transition-all duration-150 bg-transparent hover:bg-white hover:bg-opacity-20 cursor-pointer ".concat(showCaptureFlash ? "scale-90 ring-4 ring-[#11E5C5]" : ""), children: jsx("div", { className: "w-8 h-8 bg-white rounded-full" }) }), jsxs("div", { className: "text-white text-center text-sm", children: [jsxs("div", { className: "flex items-center justify-center space-x-2", children: [jsx("div", { className: "w-2 h-2 rounded-full ".concat(automaticCapture
345
351
  ? "bg-[#11E5C5] animate-pulse"
346
352
  : "bg-orange-500") }), jsx("span", { children: statusMessage })] }), captureError && (jsx("p", { className: "text-red-400 text-xs mt-2 text-center", children: captureError }))] })] }) })] }));
347
353
  };
@@ -14,6 +14,9 @@ var JDIError = function (_a) {
14
14
  var _b;
15
15
  _a.documentType; var onRetry = _a.onRetry, onContinueAnyway = _a.onContinueAnyway, onBackToUserInput = _a.onBackToUserInput, _c = _a.retryCount, retryCount = _c === void 0 ? 0 : _c, _d = _a.predictions, predictions = _d === void 0 ? DEFAULT_PREDICTIONS : _d, errorCode = _a.errorCode; _a.allowResubmission; var maxResubmissionAttempts = _a.maxResubmissionAttempts, _e = _a.isRetryAllowed, isRetryAllowed = _e === void 0 ? true : _e, _f = _a.blockContinue, blockContinue = _f === void 0 ? false : _f;
16
16
  var t = useI18n().t;
17
+ // APP3-1955: a service refusal (no credits / quota) blocks the journey,
18
+ // whatever node.allowContinueOnFailure says.
19
+ var continueAnyway = isServiceErrorCode(errorCode) ? undefined : onContinueAnyway;
17
20
  var _g = useState([]), validationPoints = _g[0], setValidationPoints = _g[1];
18
21
  // Détail de mismatch d'identité (code 7.0), calculé côté backend et porté
19
22
  // par la première prédiction. Absent pour tous les autres codes.
@@ -29,7 +32,7 @@ var JDIError = function (_a) {
29
32
  var guidance = t("user_guidance.".concat(errorCode), "");
30
33
  return guidance || null;
31
34
  };
32
- return (jsx(MobilePageLayout, { footer: jsxs("div", { className: "w-full max-w-md mx-auto", children: [jsx("div", { className: "flex flex-col space-y-3 md:hidden", children: blockContinue ? (jsx(Button, { onClick: onRetry, className: "w-full py-3", children: t("errors.retry_verification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", className: "w-full text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.back_to_user_input") })), retryCount >= 1 && isRetryAllowed && onContinueAnyway && (jsx(ButtonDesktop, { onClick: onContinueAnyway, type: "back", className: "w-full text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.continue_anyway") })), isRetryAllowed && (jsx(Button, { onClick: onRetry, className: "w-full py-3", children: t("errors.retry_verification") })), !isRetryAllowed && onContinueAnyway && (jsx(Button, { onClick: onContinueAnyway, className: "w-full py-3", children: t("errors.continue_anyway") }))] })) }), jsx("div", { className: "hidden md:flex flex-col space-y-3", children: jsx("div", { className: "flex justify-center space-x-3", children: blockContinue ? (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", className: "text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.back_to_user_input") })), retryCount >= 1 && isRetryAllowed && onContinueAnyway && (jsx(ButtonDesktop, { onClick: onContinueAnyway, type: "back", className: "text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.continue_anyway") })), isRetryAllowed && (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification") })), !isRetryAllowed && onContinueAnyway && (jsx(ButtonDesktop, { onClick: onContinueAnyway, type: "continue", children: t("errors.continue_anyway") }))] })) }) })] }), children: jsx("div", { className: "px-4 py-6 pt-11 md:px-8 md:py-8", children: jsxs("div", { className: "w-full max-w-md mx-auto space-y-6", children: [jsx("div", { className: "text-center", children: jsx("div", { className: "mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4", children: jsx("div", { className: "w-8 h-8 bg-red-500 rounded-full flex items-center justify-center", children: jsx("span", { className: "text-white text-lg", children: "\u2715" }) }) }) }), jsxs("div", { className: "text-center space-y-4", children: [jsx(Title, { className: "text-xl md:text-2xl lg:text-3xl text-red-600", children: blockContinue
35
+ return (jsx(MobilePageLayout, { footer: jsxs("div", { className: "w-full max-w-md mx-auto", children: [jsx("div", { className: "flex flex-col space-y-3 md:hidden", children: blockContinue ? (jsx(Button, { onClick: onRetry, className: "w-full py-3", children: t("errors.retry_verification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", className: "w-full text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.back_to_user_input") })), retryCount >= 1 && isRetryAllowed && continueAnyway && (jsx(ButtonDesktop, { onClick: continueAnyway, type: "back", className: "w-full text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.continue_anyway") })), isRetryAllowed && (jsx(Button, { onClick: onRetry, className: "w-full py-3", children: t("errors.retry_verification") })), !isRetryAllowed && continueAnyway && (jsx(Button, { onClick: continueAnyway, className: "w-full py-3", children: t("errors.continue_anyway") }))] })) }), jsx("div", { className: "hidden md:flex flex-col space-y-3", children: jsx("div", { className: "flex justify-center space-x-3", children: blockContinue ? (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification") })) : (jsxs(Fragment, { children: [onBackToUserInput && (jsx(ButtonDesktop, { onClick: onBackToUserInput, type: "back", className: "text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.back_to_user_input") })), retryCount >= 1 && isRetryAllowed && continueAnyway && (jsx(ButtonDesktop, { onClick: continueAnyway, type: "back", className: "text-black text-center font-poppins text-sm font-medium py-2", children: t("errors.continue_anyway") })), isRetryAllowed && (jsx(ButtonDesktop, { onClick: onRetry, type: "continue", children: t("errors.retry_verification") })), !isRetryAllowed && continueAnyway && (jsx(ButtonDesktop, { onClick: continueAnyway, type: "continue", children: t("errors.continue_anyway") }))] })) }) })] }), children: jsx("div", { className: "px-4 py-6 pt-11 md:px-8 md:py-8", children: jsxs("div", { className: "w-full max-w-md mx-auto space-y-6", children: [jsx("div", { className: "text-center", children: jsx("div", { className: "mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4", children: jsx("div", { className: "w-8 h-8 bg-red-500 rounded-full flex items-center justify-center", children: jsx("span", { className: "text-white text-lg", children: "\u2715" }) }) }) }), jsxs("div", { className: "text-center space-y-4", children: [jsx(Title, { className: "text-xl md:text-2xl lg:text-3xl text-red-600", children: blockContinue
33
36
  ? t("errors.upload_not_confirmed.title")
34
37
  : t("errors.verification_failed") }), jsx(Subtitle, { className: "text-sm text-gray-600 leading-relaxed", children: blockContinue
35
38
  ? t("errors.upload_not_confirmed.subtitle")
@@ -760,13 +760,13 @@ var DocumentCheck = function (_a) {
760
760
  return (jsx(BeforePhoto, { setStep: function () { return setCurrentPhotoStep("recto"); }, isSingleSided: isSingleSided, onBack: function () { return setDocStep(2); } }));
761
761
  }
762
762
  if (currentPhotoStep === "recto") {
763
- return (jsx(Photo, { onCapture: function (imageData) { return handlePhotoCapture(imageData, "recto"); } }));
763
+ return (jsx(Photo, { sessionId: sessionId, nodeId: node.id, onCapture: function (imageData) { return handlePhotoCapture(imageData, "recto"); } }));
764
764
  }
765
765
  if (currentPhotoStep === "before-verso") {
766
766
  return (jsx(BeforeVersoPhoto, { setStep: function () { return setCurrentPhotoStep("verso"); } }));
767
767
  }
768
768
  if (currentPhotoStep === "verso") {
769
- return (jsx(Photo, { onCapture: function (imageData) { return handlePhotoCapture(imageData, "verso"); } }));
769
+ return (jsx(Photo, { sessionId: sessionId, nodeId: node.id, onCapture: function (imageData) { return handlePhotoCapture(imageData, "verso"); } }));
770
770
  }
771
771
  if (currentPhotoStep === "confirmation") {
772
772
  return (jsx(PhotoConfirmation, { imageUrl: capturedImages.recto || "", versoImageUrl: capturedImages.verso, requiresTwoSides: !!capturedImages.verso, onConfirm: onConfirm, onRetry: function () { return setCurrentPhotoStep("before-recto"); }, onRetryAfterProcessing: function () { return setCurrentPhotoStep("before-recto"); }, fileUploaded: fileUploaded, isSingleSided: isSingleSided }));
@@ -780,11 +780,21 @@ var id_check = {
780
780
  alt_verso: "Before taking the verso photo"
781
781
  },
782
782
  camera: {
783
+ low_resolution: {
784
+ title: "Limited camera resolution",
785
+ body: "Your camera resolution is below HD. The photo may be blurry, but you can continue.",
786
+ tips: "Close other apps using the camera, choose another camera or switch to mobile, and improve the lighting.",
787
+ retry: "Try again"
788
+ },
783
789
  error_title: "Camera error",
784
790
  retry: "Retry",
785
791
  document_detected: "Document detected"
786
792
  },
787
793
  photo: {
794
+ hint: {
795
+ too_dark: "Too dark, find more light",
796
+ too_bright: "Too much glare, avoid direct light"
797
+ },
788
798
  capture_failed: "Capture failed. Please try again.",
789
799
  mode_autocapture: "Autocapture enabled",
790
800
  mode_manual: "Manual mode",
@@ -780,11 +780,21 @@ var id_check = {
780
780
  alt_verso: "Antes de tomar la foto del reverso"
781
781
  },
782
782
  camera: {
783
+ low_resolution: {
784
+ title: "Resolución de cámara limitada",
785
+ body: "La resolución de la cámara es inferior a HD. La foto puede salir borrosa, pero puede continuar.",
786
+ tips: "Cierre otras aplicaciones que usen la cámara, elija otra cámara o pase al móvil y mejore la iluminación.",
787
+ retry: "Reintentar"
788
+ },
783
789
  error_title: "Error de cámara",
784
790
  retry: "Reintentar",
785
791
  document_detected: "Documento detectado"
786
792
  },
787
793
  photo: {
794
+ hint: {
795
+ too_dark: "Demasiado oscuro, busque más luz",
796
+ too_bright: "Demasiados reflejos, evite la luz directa"
797
+ },
788
798
  capture_failed: "La captura ha fallado. Inténtalo de nuevo.",
789
799
  mode_autocapture: "Captura automática activada",
790
800
  mode_manual: "Modo manual",
@@ -780,11 +780,21 @@ var id_check = {
780
780
  alt_verso: "Avant de prendre la photo du verso"
781
781
  },
782
782
  camera: {
783
+ low_resolution: {
784
+ title: "Résolution de la caméra limitée",
785
+ body: "La résolution de votre caméra est inférieure à la HD. La photo risque d’être floue, mais vous pouvez continuer.",
786
+ tips: "Fermez les autres applications utilisant la caméra, choisissez une autre caméra ou passez sur mobile, et améliorez l’éclairage.",
787
+ retry: "Réessayer"
788
+ },
783
789
  error_title: "Erreur de caméra",
784
790
  retry: "Réessayer",
785
791
  document_detected: "Document détecté"
786
792
  },
787
793
  photo: {
794
+ hint: {
795
+ too_dark: "Trop sombre, cherchez plus de lumière",
796
+ too_bright: "Trop de reflets, évitez la lumière directe"
797
+ },
788
798
  capture_failed: "La capture a échoué. Veuillez réessayer.",
789
799
  mode_autocapture: "Autocapture activée",
790
800
  mode_manual: "Mode manuel",