smartcomply-web-sdk 1.0.61 → 1.0.63
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/dist/camera/CameraManager.d.ts +8 -0
- package/dist/camera/CameraManager.d.ts.map +1 -1
- package/dist/camera/CameraManager.js +13 -2
- package/dist/camera/CameraManager.js.map +1 -1
- package/dist/camera/Permission.d.ts +11 -0
- package/dist/camera/Permission.d.ts.map +1 -1
- package/dist/camera/Permission.js +23 -0
- package/dist/camera/Permission.js.map +1 -1
- package/dist/esm/camera/CameraManager.d.ts +8 -0
- package/dist/esm/camera/CameraManager.d.ts.map +1 -1
- package/dist/esm/camera/CameraManager.js +11 -1
- package/dist/esm/camera/CameraManager.js.map +1 -1
- package/dist/esm/camera/Permission.d.ts +11 -0
- package/dist/esm/camera/Permission.d.ts.map +1 -1
- package/dist/esm/camera/Permission.js +22 -0
- package/dist/esm/camera/Permission.js.map +1 -1
- package/dist/esm/flow/DocumentCapture.d.ts +20 -5
- package/dist/esm/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/esm/flow/DocumentCapture.js +191 -207
- package/dist/esm/flow/DocumentCapture.js.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/esm/flow/SmartComplyFlow.js +84 -116
- package/dist/esm/flow/SmartComplyFlow.js.map +1 -1
- package/dist/esm/flow/icons.d.ts +14 -0
- package/dist/esm/flow/icons.d.ts.map +1 -0
- package/dist/esm/flow/icons.js +34 -0
- package/dist/esm/flow/icons.js.map +1 -0
- package/dist/esm/flow/spinner.d.ts +15 -0
- package/dist/esm/flow/spinner.d.ts.map +1 -0
- package/dist/esm/flow/spinner.js +55 -0
- package/dist/esm/flow/spinner.js.map +1 -0
- package/dist/esm/flow/theme.d.ts +19 -0
- package/dist/esm/flow/theme.d.ts.map +1 -1
- package/dist/esm/flow/theme.js +19 -0
- package/dist/esm/flow/theme.js.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.d.ts +10 -0
- package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/esm/modules/liveness/LivenessUI.js +17 -16
- package/dist/esm/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/esm/modules/liveness/liveness.d.ts +2 -0
- package/dist/esm/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/esm/modules/liveness/liveness.js +1 -1
- package/dist/esm/modules/liveness/liveness.js.map +1 -1
- package/dist/esm/utils/constants.d.ts +11 -3
- package/dist/esm/utils/constants.d.ts.map +1 -1
- package/dist/esm/utils/constants.js +11 -3
- package/dist/esm/utils/constants.js.map +1 -1
- package/dist/esm/utils/documentRules.d.ts +15 -0
- package/dist/esm/utils/documentRules.d.ts.map +1 -0
- package/dist/esm/utils/documentRules.js +27 -0
- package/dist/esm/utils/documentRules.js.map +1 -0
- package/dist/flow/DocumentCapture.d.ts +20 -5
- package/dist/flow/DocumentCapture.d.ts.map +1 -1
- package/dist/flow/DocumentCapture.js +191 -207
- package/dist/flow/DocumentCapture.js.map +1 -1
- package/dist/flow/SmartComplyFlow.d.ts.map +1 -1
- package/dist/flow/SmartComplyFlow.js +86 -118
- package/dist/flow/SmartComplyFlow.js.map +1 -1
- package/dist/flow/icons.d.ts +14 -0
- package/dist/flow/icons.d.ts.map +1 -0
- package/dist/flow/icons.js +37 -0
- package/dist/flow/icons.js.map +1 -0
- package/dist/flow/spinner.d.ts +15 -0
- package/dist/flow/spinner.d.ts.map +1 -0
- package/dist/flow/spinner.js +59 -0
- package/dist/flow/spinner.js.map +1 -0
- package/dist/flow/theme.d.ts +19 -0
- package/dist/flow/theme.d.ts.map +1 -1
- package/dist/flow/theme.js +20 -1
- package/dist/flow/theme.js.map +1 -1
- package/dist/modules/liveness/LivenessUI.d.ts +10 -0
- package/dist/modules/liveness/LivenessUI.d.ts.map +1 -1
- package/dist/modules/liveness/LivenessUI.js +17 -16
- package/dist/modules/liveness/LivenessUI.js.map +1 -1
- package/dist/modules/liveness/liveness.d.ts +2 -0
- package/dist/modules/liveness/liveness.d.ts.map +1 -1
- package/dist/modules/liveness/liveness.js +1 -1
- package/dist/modules/liveness/liveness.js.map +1 -1
- package/dist/smartcomply.browser.js +376 -313
- package/dist/smartcomply.browser.js.map +4 -4
- package/dist/utils/constants.d.ts +11 -3
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +11 -3
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/documentRules.d.ts +15 -0
- package/dist/utils/documentRules.d.ts.map +1 -0
- package/dist/utils/documentRules.js +31 -0
- package/dist/utils/documentRules.js.map +1 -0
- package/package.json +6 -3
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DocumentCapture = void 0;
|
|
4
|
+
const theme_1 = require("./theme");
|
|
4
5
|
const ImageQuality_1 = require("../utils/ImageQuality");
|
|
6
|
+
const documentRules_1 = require("../utils/documentRules");
|
|
7
|
+
const CameraManager_1 = require("../camera/CameraManager");
|
|
8
|
+
const Permission_1 = require("../camera/Permission");
|
|
9
|
+
const constants_1 = require("../utils/constants");
|
|
10
|
+
const icons_1 = require("./icons");
|
|
11
|
+
const spinner_1 = require("./spinner");
|
|
5
12
|
class DocumentCapture {
|
|
6
13
|
constructor() {
|
|
7
14
|
this.root = null;
|
|
8
15
|
this.videoEl = null;
|
|
9
|
-
this.
|
|
16
|
+
this.videoWrapEl = null;
|
|
17
|
+
this.cameraManager = new CameraManager_1.CameraManager();
|
|
18
|
+
this.orientationHandler = null;
|
|
10
19
|
this.resolveCapture = null;
|
|
11
20
|
this.rejectCapture = null;
|
|
12
21
|
this.frontBlob = null;
|
|
@@ -66,26 +75,11 @@ class DocumentCapture {
|
|
|
66
75
|
this.backBlob = null;
|
|
67
76
|
this.currentSide = 'front';
|
|
68
77
|
}
|
|
69
|
-
requiresBackSide(documentType) {
|
|
70
|
-
// Only request back-side capture for documents where the backend OCR
|
|
71
|
-
// actually needs the reverse face. NIN cards are single-sided for OCR
|
|
72
|
-
// purposes — do NOT include "nin" here.
|
|
73
|
-
const type = documentType.toLowerCase();
|
|
74
|
-
return type.includes("voter") ||
|
|
75
|
-
type.includes("driver") ||
|
|
76
|
-
type.includes("license") ||
|
|
77
|
-
type.includes("national id") ||
|
|
78
|
-
type.includes("national id card") ||
|
|
79
|
-
type.includes("ghana");
|
|
80
|
-
// Passport: back side is the machine-readable zone but OCR reads from front only.
|
|
81
|
-
// NIN / NIN with face: front only needed.
|
|
82
|
-
// Voter's card / Driver's license / National ID card: back required.
|
|
83
|
-
}
|
|
84
78
|
shouldCaptureBackSide(documentType) {
|
|
85
|
-
// Prefer the backend-provided flag; fall back to
|
|
79
|
+
// Prefer the backend-provided flag; fall back to the shared heuristic only when absent.
|
|
86
80
|
return this.requiresBackOverride !== undefined
|
|
87
81
|
? this.requiresBackOverride
|
|
88
|
-
:
|
|
82
|
+
: (0, documentRules_1.requiresBackSide)(documentType);
|
|
89
83
|
}
|
|
90
84
|
renderChoiceScreen(container, theme, documentType) {
|
|
91
85
|
this.root = document.createElement("div");
|
|
@@ -109,14 +103,14 @@ class DocumentCapture {
|
|
|
109
103
|
iconBox.style.cssText = `
|
|
110
104
|
width:48px;height:48px;border-radius:14px;flex-shrink:0;
|
|
111
105
|
display:flex;align-items:center;justify-content:center;
|
|
112
|
-
|
|
106
|
+
color:${theme.primary};
|
|
113
107
|
background:${theme.primary}15;
|
|
114
108
|
border:1.5px solid ${theme.primary}30;
|
|
115
109
|
`;
|
|
116
|
-
iconBox.
|
|
110
|
+
iconBox.innerHTML = (0, icons_1.icon)(this._docIcon(documentType), 22);
|
|
117
111
|
const titleBlock = document.createElement("div");
|
|
118
112
|
const titleEl = document.createElement("div");
|
|
119
|
-
titleEl.style.cssText = `color:${theme.text};font-size
|
|
113
|
+
titleEl.style.cssText = `color:${theme.text};font-size:${theme_1.TYPE.headingSm}px;font-weight:700;line-height:1.3;`;
|
|
120
114
|
titleEl.textContent = `Capture your ${docLabel}`;
|
|
121
115
|
const subEl = document.createElement("div");
|
|
122
116
|
subEl.style.cssText = `color:${theme.textSecondary};font-size:12px;margin-top:2px;`;
|
|
@@ -145,7 +139,7 @@ class DocumentCapture {
|
|
|
145
139
|
transition:all 0.15s ease;min-height:80px;
|
|
146
140
|
box-shadow:0 4px 14px ${theme.primary}30;
|
|
147
141
|
`;
|
|
148
|
-
cameraBtn.innerHTML = `<span style="
|
|
142
|
+
cameraBtn.innerHTML = `<span style="display:inline-flex;">${(0, icons_1.icon)("camera", 24)}</span><span>Take Photo</span><span style="font-size:10px;opacity:0.8;font-weight:500;">Use camera</span>`;
|
|
149
143
|
cameraBtn.addEventListener("mouseenter", () => { cameraBtn.style.opacity = "0.9"; cameraBtn.style.transform = "translateY(-1px)"; });
|
|
150
144
|
cameraBtn.addEventListener("mouseleave", () => { cameraBtn.style.opacity = "1"; cameraBtn.style.transform = "none"; });
|
|
151
145
|
cameraBtn.addEventListener("click", () => {
|
|
@@ -162,7 +156,7 @@ class DocumentCapture {
|
|
|
162
156
|
font-size:13px;font-weight:600;font-family:inherit;
|
|
163
157
|
transition:all 0.15s ease;min-height:80px;
|
|
164
158
|
`;
|
|
165
|
-
uploadBtn.innerHTML = `<span style="
|
|
159
|
+
uploadBtn.innerHTML = `<span style="display:inline-flex;">${(0, icons_1.icon)("gallery", 24)}</span><span>From Gallery</span><span style="font-size:10px;color:${theme.textMuted || theme.textSecondary};font-weight:500;">Upload file</span>`;
|
|
166
160
|
uploadBtn.addEventListener("mouseenter", () => { uploadBtn.style.borderColor = theme.primary; uploadBtn.style.transform = "translateY(-1px)"; });
|
|
167
161
|
uploadBtn.addEventListener("mouseleave", () => { uploadBtn.style.borderColor = theme.border; uploadBtn.style.transform = "none"; });
|
|
168
162
|
uploadBtn.addEventListener("click", () => { this.handleFileUpload(container, theme); });
|
|
@@ -174,7 +168,7 @@ class DocumentCapture {
|
|
|
174
168
|
tips.style.cssText = `
|
|
175
169
|
margin:0 20px 16px;padding:12px 14px;
|
|
176
170
|
background:${theme.shimmer || theme.cardBg};
|
|
177
|
-
border:1px solid ${theme.border};border-radius
|
|
171
|
+
border:1px solid ${theme.border};border-radius:${theme_1.RADIUS.md}px;
|
|
178
172
|
`;
|
|
179
173
|
const tipsGrid = document.createElement("div");
|
|
180
174
|
tipsGrid.style.cssText = `
|
|
@@ -189,7 +183,7 @@ class DocumentCapture {
|
|
|
189
183
|
tipItems.forEach((t) => {
|
|
190
184
|
const row = document.createElement("div");
|
|
191
185
|
row.style.cssText = `display:flex;align-items:center;gap:5px;font-size:11px;color:${theme.textSecondary};`;
|
|
192
|
-
row.innerHTML = `<span style="color:${theme.primary};
|
|
186
|
+
row.innerHTML = `<span style="color:${theme.primary};flex-shrink:0;display:inline-flex;">${(0, icons_1.icon)("check", 12)}</span><span>${t}</span>`;
|
|
193
187
|
tipsGrid.appendChild(row);
|
|
194
188
|
});
|
|
195
189
|
tips.appendChild(tipsGrid);
|
|
@@ -234,51 +228,99 @@ class DocumentCapture {
|
|
|
234
228
|
this.root.appendChild(fileInput);
|
|
235
229
|
container.appendChild(this.root);
|
|
236
230
|
}
|
|
237
|
-
/** Returns
|
|
231
|
+
/** Returns the icon name matching the document type. */
|
|
238
232
|
_docIcon(documentType) {
|
|
239
233
|
const t = documentType.toLowerCase();
|
|
240
234
|
if (t.includes("passport"))
|
|
241
|
-
return "
|
|
235
|
+
return "passport";
|
|
242
236
|
if (t.includes("driver") || t.includes("licen"))
|
|
243
|
-
return "
|
|
237
|
+
return "car";
|
|
244
238
|
if (t.includes("voter") || t.includes("pvc"))
|
|
245
|
-
return "
|
|
246
|
-
return "
|
|
239
|
+
return "ballot";
|
|
240
|
+
return "id-card";
|
|
247
241
|
}
|
|
248
242
|
renderCameraView(container, theme, documentType) {
|
|
243
|
+
// Dark cinematic chrome + corner-bracket guide — matches the liveness
|
|
244
|
+
// scan screen's visual language (LivenessUI.ts) instead of the plain
|
|
245
|
+
// rectangle outline this used to be, so the two camera experiences in
|
|
246
|
+
// the flow read as one product.
|
|
249
247
|
const wrapper = document.createElement("div");
|
|
250
|
-
wrapper.style.cssText =
|
|
248
|
+
wrapper.style.cssText = `
|
|
249
|
+
position:relative;width:100%;max-width:420px;margin:0 auto;
|
|
250
|
+
background:#0a0a0a;border-radius:${theme_1.RADIUS.lg}px;overflow:hidden;
|
|
251
|
+
box-shadow:0 8px 32px rgba(0,0,0,0.4);
|
|
252
|
+
`;
|
|
253
|
+
this.videoWrapEl = wrapper;
|
|
251
254
|
this.videoEl = document.createElement("video");
|
|
252
255
|
this.videoEl.autoplay = true;
|
|
253
256
|
this.videoEl.playsInline = true;
|
|
254
257
|
this.videoEl.muted = true;
|
|
258
|
+
// Starts at 4:3 (typical document-shaped default); _fitToStream() below
|
|
259
|
+
// corrects this to the stream's real aspect ratio once metadata loads,
|
|
260
|
+
// instead of force-cropping every device's native ratio into one fixed box.
|
|
255
261
|
this.videoEl.style.cssText = "width:100%;display:block;aspect-ratio:4/3;object-fit:cover;";
|
|
256
262
|
wrapper.appendChild(this.videoEl);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
263
|
+
const vignette = document.createElement("div");
|
|
264
|
+
vignette.style.cssText = `
|
|
265
|
+
position:absolute;inset:0;pointer-events:none;
|
|
266
|
+
box-shadow:inset 0 0 0 9999px rgba(0,0,0,0.38);
|
|
267
|
+
-webkit-mask:radial-gradient(ellipse 46% 34% at 50% 50%, transparent 60%, #000 100%);
|
|
268
|
+
mask:radial-gradient(ellipse 46% 34% at 50% 50%, transparent 60%, #000 100%);
|
|
269
|
+
`;
|
|
270
|
+
wrapper.appendChild(vignette);
|
|
271
|
+
// Corner-bracket guide — same bracket-path language as LivenessUI's oval
|
|
272
|
+
// guide, adapted to a rectangle since documents (unlike faces) aren't oval.
|
|
273
|
+
const guideWrap = document.createElement("div");
|
|
274
|
+
guideWrap.style.cssText = `
|
|
275
|
+
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
|
276
|
+
width:84%;aspect-ratio:16/10;pointer-events:none;
|
|
262
277
|
`;
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
278
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
279
|
+
svg.setAttribute("viewBox", "0 0 160 100");
|
|
280
|
+
svg.style.cssText = "width:100%;height:100%;overflow:visible;";
|
|
281
|
+
const bracketGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
282
|
+
bracketGroup.setAttribute("stroke", theme.primary);
|
|
283
|
+
bracketGroup.setAttribute("stroke-width", "3");
|
|
284
|
+
bracketGroup.setAttribute("stroke-linecap", "round");
|
|
285
|
+
bracketGroup.setAttribute("fill", "none");
|
|
286
|
+
const bLen = 16;
|
|
287
|
+
const bx0 = 4, bx1 = 156, by0 = 4, by1 = 96;
|
|
288
|
+
const brackets = [
|
|
289
|
+
`M${bx0},${by0 + bLen} L${bx0},${by0} L${bx0 + bLen},${by0}`,
|
|
290
|
+
`M${bx1 - bLen},${by0} L${bx1},${by0} L${bx1},${by0 + bLen}`,
|
|
291
|
+
`M${bx0},${by1 - bLen} L${bx0},${by1} L${bx0 + bLen},${by1}`,
|
|
292
|
+
`M${bx1 - bLen},${by1} L${bx1},${by1} L${bx1},${by1 - bLen}`,
|
|
293
|
+
];
|
|
294
|
+
for (const d of brackets) {
|
|
295
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
296
|
+
path.setAttribute("d", d);
|
|
297
|
+
bracketGroup.appendChild(path);
|
|
298
|
+
}
|
|
299
|
+
svg.appendChild(bracketGroup);
|
|
300
|
+
guideWrap.appendChild(svg);
|
|
301
|
+
wrapper.appendChild(guideWrap);
|
|
302
|
+
// Header — gradient scrim + document label
|
|
303
|
+
const header = document.createElement("div");
|
|
304
|
+
header.style.cssText = `
|
|
305
|
+
position:absolute;top:0;left:0;right:0;
|
|
306
|
+
background:linear-gradient(180deg,rgba(0,0,0,0.8) 0%,transparent 100%);
|
|
307
|
+
padding:14px 16px 32px;text-align:center;
|
|
267
308
|
`;
|
|
268
|
-
|
|
269
|
-
wrapper.appendChild(
|
|
270
|
-
//
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
position:absolute;bottom:
|
|
274
|
-
|
|
275
|
-
|
|
309
|
+
header.innerHTML = `<div style="color:#fff;font-size:${theme_1.TYPE.sm}px;font-weight:600;">Align your ${this.getDocumentLabel(documentType)} within the frame</div>`;
|
|
310
|
+
wrapper.appendChild(header);
|
|
311
|
+
// Footer — gradient scrim + tips
|
|
312
|
+
const footer = document.createElement("div");
|
|
313
|
+
footer.style.cssText = `
|
|
314
|
+
position:absolute;bottom:0;left:0;right:0;
|
|
315
|
+
background:linear-gradient(0deg,rgba(0,0,0,0.7) 0%,transparent 100%);
|
|
316
|
+
padding:28px 10px 18px;text-align:center;
|
|
276
317
|
`;
|
|
277
|
-
|
|
278
|
-
wrapper.appendChild(
|
|
318
|
+
footer.innerHTML = `<div style="font-size:${theme_1.TYPE.xs}px;color:rgba(255,255,255,0.55);">Good lighting · No glare · All 4 corners visible</div>`;
|
|
319
|
+
wrapper.appendChild(footer);
|
|
279
320
|
container.appendChild(wrapper);
|
|
280
321
|
// Capture button
|
|
281
322
|
const captureBtn = document.createElement("button");
|
|
323
|
+
captureBtn.setAttribute("aria-label", "Capture photo");
|
|
282
324
|
captureBtn.style.cssText = `
|
|
283
325
|
display:block;margin:16px auto 0;width:64px;height:64px;
|
|
284
326
|
border-radius:50%;border:4px solid ${theme.primary};
|
|
@@ -305,6 +347,41 @@ class DocumentCapture {
|
|
|
305
347
|
// Open camera
|
|
306
348
|
this.openCamera();
|
|
307
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Matches the video box's aspect-ratio CSS to the stream's actual
|
|
352
|
+
* negotiated dimensions instead of a single hardcoded ratio — a stream
|
|
353
|
+
* that's natively 16:9 no longer gets force-cropped into a 4:3 box.
|
|
354
|
+
* Clamped to a sane document-shaped range so a degenerate/unusual stream
|
|
355
|
+
* can't produce a sliver-thin or absurdly tall box.
|
|
356
|
+
*/
|
|
357
|
+
_fitToStream() {
|
|
358
|
+
if (!this.videoEl)
|
|
359
|
+
return;
|
|
360
|
+
const { videoWidth: w, videoHeight: h } = this.videoEl;
|
|
361
|
+
if (!w || !h)
|
|
362
|
+
return;
|
|
363
|
+
const ratio = Math.min(Math.max(w / h, 1), 2); // clamp to [1:1, 2:1]
|
|
364
|
+
this.videoEl.style.aspectRatio = `${ratio}`;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Re-fits the video box when the device rotates mid-capture. Does not
|
|
368
|
+
* re-request getUserMedia (disruptive/slow) — the stream's dimensions
|
|
369
|
+
* don't change on rotation, only the viewport does, so re-reading the
|
|
370
|
+
* already-known videoWidth/videoHeight is enough to keep the box sane.
|
|
371
|
+
*/
|
|
372
|
+
_watchOrientation() {
|
|
373
|
+
this._unwatchOrientation();
|
|
374
|
+
this.orientationHandler = () => this._fitToStream();
|
|
375
|
+
window.addEventListener("orientationchange", this.orientationHandler);
|
|
376
|
+
window.addEventListener("resize", this.orientationHandler);
|
|
377
|
+
}
|
|
378
|
+
_unwatchOrientation() {
|
|
379
|
+
if (this.orientationHandler) {
|
|
380
|
+
window.removeEventListener("orientationchange", this.orientationHandler);
|
|
381
|
+
window.removeEventListener("resize", this.orientationHandler);
|
|
382
|
+
this.orientationHandler = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
308
385
|
async openCamera() {
|
|
309
386
|
// Check browser compatibility
|
|
310
387
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
@@ -312,28 +389,32 @@ class DocumentCapture {
|
|
|
312
389
|
return;
|
|
313
390
|
}
|
|
314
391
|
const maxRetries = 3;
|
|
315
|
-
|
|
316
|
-
|
|
392
|
+
// Retries only transient/negotiable failures (busy camera, unsupported
|
|
393
|
+
// constraints, anything unrecognized) — a hard permission denial or "no
|
|
394
|
+
// camera exists" can never be fixed by retrying, so those short-circuit
|
|
395
|
+
// to an immediate error instead of burning ~6s of backoff first.
|
|
396
|
+
const attemptCameraAccess = async (constraints, retryCount = 0) => {
|
|
317
397
|
try {
|
|
318
|
-
return await
|
|
398
|
+
return await this.cameraManager.open(constraints);
|
|
319
399
|
}
|
|
320
400
|
catch (err) {
|
|
321
401
|
console.warn(`Camera access attempt ${retryCount + 1} failed:`, err);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); // Exponential backoff
|
|
325
|
-
return attemptCameraAccess(constraints);
|
|
402
|
+
const status = err instanceof CameraManager_1.CameraAccessError ? err.status : "unknown";
|
|
403
|
+
if ((0, Permission_1.isRetryableCameraError)(status) && retryCount < maxRetries - 1) {
|
|
404
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * (retryCount + 1))); // Exponential backoff
|
|
405
|
+
return attemptCameraAccess(constraints, retryCount + 1);
|
|
326
406
|
}
|
|
327
407
|
throw err;
|
|
328
408
|
}
|
|
329
409
|
};
|
|
410
|
+
let stream;
|
|
330
411
|
try {
|
|
331
412
|
// Try rear camera first
|
|
332
|
-
|
|
413
|
+
stream = await attemptCameraAccess({
|
|
333
414
|
video: {
|
|
334
415
|
facingMode: { ideal: "environment" },
|
|
335
|
-
width: { ideal:
|
|
336
|
-
height: { ideal:
|
|
416
|
+
width: { ideal: constants_1.DOC_CAMERA_IDEAL_WIDTH },
|
|
417
|
+
height: { ideal: constants_1.DOC_CAMERA_IDEAL_HEIGHT },
|
|
337
418
|
},
|
|
338
419
|
});
|
|
339
420
|
}
|
|
@@ -341,48 +422,39 @@ class DocumentCapture {
|
|
|
341
422
|
console.warn("Rear camera failed, trying any camera:", err);
|
|
342
423
|
// Fallback to any camera
|
|
343
424
|
try {
|
|
344
|
-
|
|
345
|
-
video: { width: { ideal:
|
|
425
|
+
stream = await attemptCameraAccess({
|
|
426
|
+
video: { width: { ideal: constants_1.DOC_CAMERA_IDEAL_WIDTH } },
|
|
346
427
|
});
|
|
347
428
|
}
|
|
348
429
|
catch (fallbackErr) {
|
|
349
|
-
|
|
350
|
-
this.rejectCapture?.(new Error(errorMessage));
|
|
430
|
+
this.rejectCapture?.(new Error(fallbackErr?.message || "Camera access failed. Please check your camera permissions and try again."));
|
|
351
431
|
return;
|
|
352
432
|
}
|
|
353
433
|
}
|
|
434
|
+
// A capture that's already resolved (photo taken, camera intentionally
|
|
435
|
+
// stopped) must not surface a stale interruption from the old stream.
|
|
436
|
+
this.cameraManager.onStreamInterrupted(() => {
|
|
437
|
+
if (this.resolveCapture || this.rejectCapture) {
|
|
438
|
+
this.rejectCapture?.(new Error("Camera connection was lost. Please check your camera and try again."));
|
|
439
|
+
}
|
|
440
|
+
});
|
|
354
441
|
if (this.videoEl) {
|
|
355
|
-
this.videoEl.srcObject =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
getCameraErrorMessage(error) {
|
|
359
|
-
if (error.name === 'NotAllowedError') {
|
|
360
|
-
return "Camera access denied. Please allow camera permissions and try again.";
|
|
361
|
-
}
|
|
362
|
-
else if (error.name === 'NotFoundError') {
|
|
363
|
-
return "No camera found. Please ensure your device has a working camera.";
|
|
364
|
-
}
|
|
365
|
-
else if (error.name === 'NotReadableError') {
|
|
366
|
-
return "Camera is already in use by another application. Please close other apps using the camera and try again.";
|
|
367
|
-
}
|
|
368
|
-
else if (error.name === 'OverconstrainedError') {
|
|
369
|
-
return "Camera constraints not supported. Trying with different settings...";
|
|
370
|
-
}
|
|
371
|
-
else {
|
|
372
|
-
return "Camera access failed. Please check your camera permissions and try again.";
|
|
442
|
+
this.videoEl.srcObject = stream;
|
|
443
|
+
this.videoEl.addEventListener("loadedmetadata", () => this._fitToStream(), { once: true });
|
|
444
|
+
this._watchOrientation();
|
|
373
445
|
}
|
|
374
446
|
}
|
|
375
447
|
captureFrame(container, theme, documentType) {
|
|
376
448
|
if (!this.videoEl)
|
|
377
449
|
return;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
450
|
+
// Draw at the video's native resolution — the downstream compressImage()
|
|
451
|
+
// call (see proceedWithPhoto) is the single place resolution gets capped
|
|
452
|
+
// (via computeScaleRatio, which never upscales). Resizing here too used
|
|
453
|
+
// to cap captures at 1280x960 before compressImage ever ran, silently
|
|
454
|
+
// undermining its higher resolution/quality target.
|
|
383
455
|
const canvas = document.createElement("canvas");
|
|
384
|
-
canvas.width =
|
|
385
|
-
canvas.height =
|
|
456
|
+
canvas.width = this.videoEl.videoWidth;
|
|
457
|
+
canvas.height = this.videoEl.videoHeight;
|
|
386
458
|
const ctx = canvas.getContext("2d");
|
|
387
459
|
if (!ctx)
|
|
388
460
|
return;
|
|
@@ -390,14 +462,15 @@ class DocumentCapture {
|
|
|
390
462
|
canvas.toBlob(async (blob) => {
|
|
391
463
|
if (blob) {
|
|
392
464
|
this.stopCamera();
|
|
393
|
-
// Quality analysis runs synchronously over
|
|
394
|
-
// show a loading state so the capture->preview
|
|
395
|
-
// looks like a frozen/unresponsive tap on
|
|
465
|
+
// Quality analysis runs synchronously over the captured frame's
|
|
466
|
+
// pixel data — show a loading state so the capture->preview
|
|
467
|
+
// transition never looks like a frozen/unresponsive tap on
|
|
468
|
+
// slower devices.
|
|
396
469
|
this.showLoadingState("Analyzing photo...", theme);
|
|
397
470
|
const quality = await this._assessImageQuality(canvas, ctx);
|
|
398
471
|
this.showPreview(container, theme, blob, documentType, quality);
|
|
399
472
|
}
|
|
400
|
-
}, "image/jpeg", 0.
|
|
473
|
+
}, "image/jpeg", 0.92);
|
|
401
474
|
}
|
|
402
475
|
/**
|
|
403
476
|
* Runs the local sharpness/glare/contrast/brightness check against a
|
|
@@ -445,23 +518,36 @@ class DocumentCapture {
|
|
|
445
518
|
container.appendChild(this.root);
|
|
446
519
|
}
|
|
447
520
|
this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;";
|
|
521
|
+
// Step badge — same pill treatment as SmartComplyFlow's document-confirm
|
|
522
|
+
// screen (Step 2: check what we read), so this doesn't read as
|
|
523
|
+
// unexplained repetition of that later screen — they check different
|
|
524
|
+
// things (photo clarity here, OCR-extracted data there).
|
|
525
|
+
const stepBadge = document.createElement("div");
|
|
526
|
+
stepBadge.style.cssText = `
|
|
527
|
+
display:inline-flex;align-items:center;gap:5px;
|
|
528
|
+
padding:3px 11px;border-radius:${theme_1.RADIUS.pill}px;
|
|
529
|
+
font-size:10.5px;font-weight:600;letter-spacing:0.2px;
|
|
530
|
+
color:${theme.primary};background:${theme.primary}15;border:1px solid ${theme.primary}30;
|
|
531
|
+
`;
|
|
532
|
+
stepBadge.textContent = "STEP 1 OF 2 · CHECK THE PHOTO";
|
|
533
|
+
this.root.appendChild(stepBadge);
|
|
448
534
|
// Preview image
|
|
449
535
|
const img = document.createElement("img");
|
|
450
536
|
const imageUrl = URL.createObjectURL(imageData);
|
|
451
537
|
img.src = imageUrl;
|
|
452
538
|
img.style.cssText = `
|
|
453
|
-
width:100%;max-height:300px;object-fit:contain;border-radius
|
|
539
|
+
width:100%;max-height:300px;object-fit:contain;border-radius:${theme_1.RADIUS.md}px;
|
|
454
540
|
border:2px solid ${theme.border};
|
|
455
541
|
`;
|
|
456
542
|
this.root.appendChild(img);
|
|
457
543
|
// Store URL for cleanup
|
|
458
544
|
img._blobUrl = imageUrl;
|
|
459
545
|
const label = document.createElement("div");
|
|
460
|
-
label.style.cssText = `color:${theme.
|
|
546
|
+
label.style.cssText = `color:${theme.text};font-size:${theme_1.TYPE.base}px;font-weight:700;text-align:center;`;
|
|
461
547
|
label.textContent = "Does the photo look clear?";
|
|
462
548
|
this.root.appendChild(label);
|
|
463
549
|
const sublabel = document.createElement("div");
|
|
464
|
-
sublabel.style.cssText = `color:${theme.textMuted};font-size
|
|
550
|
+
sublabel.style.cssText = `color:${theme.textMuted};font-size:${theme_1.TYPE.xs}px;text-align:center;line-height:1.6;`;
|
|
465
551
|
sublabel.textContent = "All text must be readable and all four corners must be visible.";
|
|
466
552
|
this.root.appendChild(sublabel);
|
|
467
553
|
// Quality warning banner — only shown when the local check flags an issue.
|
|
@@ -590,126 +676,24 @@ class DocumentCapture {
|
|
|
590
676
|
input.click();
|
|
591
677
|
}
|
|
592
678
|
stopCamera() {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
this.stream = null;
|
|
596
|
-
}
|
|
679
|
+
this._unwatchOrientation();
|
|
680
|
+
this.cameraManager.stop();
|
|
597
681
|
if (this.videoEl) {
|
|
598
682
|
this.videoEl.srcObject = null;
|
|
599
683
|
this.videoEl = null;
|
|
600
684
|
}
|
|
601
|
-
|
|
602
|
-
createEnhancedButton(label, theme, isPrimary, ariaLabel) {
|
|
603
|
-
const btn = document.createElement("button");
|
|
604
|
-
btn.textContent = label;
|
|
605
|
-
btn.setAttribute("aria-label", ariaLabel);
|
|
606
|
-
btn.style.cssText = `
|
|
607
|
-
padding:18px 24px;border-radius:16px;font-size:16px;font-weight:600;
|
|
608
|
-
cursor:pointer;transition:all 0.3s ease;border:none;width:100%;
|
|
609
|
-
min-height:56px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
|
|
610
|
-
position:relative;overflow:hidden;
|
|
611
|
-
${isPrimary
|
|
612
|
-
? `background:linear-gradient(135deg, ${theme.primary}, ${theme.primary}DD);color:#fff;box-shadow:0 8px 24px ${theme.primary}25, 0 4px 12px ${theme.primary}15;`
|
|
613
|
-
: `background:${theme.inputBg};color:${theme.text};border:2px solid ${theme.border};box-shadow:0 2px 8px ${theme.border}20;`}
|
|
614
|
-
`;
|
|
615
|
-
// Add ripple effect
|
|
616
|
-
btn.addEventListener("click", (e) => {
|
|
617
|
-
const ripple = document.createElement("span");
|
|
618
|
-
const rect = btn.getBoundingClientRect();
|
|
619
|
-
const size = Math.max(rect.width, rect.height);
|
|
620
|
-
const x = e.clientX - rect.left - size / 2;
|
|
621
|
-
const y = e.clientY - rect.top - size / 2;
|
|
622
|
-
ripple.style.cssText = `
|
|
623
|
-
position:absolute;width:${size}px;height:${size}px;
|
|
624
|
-
left:${x}px;top:${y}px;border-radius:50%;
|
|
625
|
-
background:${isPrimary ? 'rgba(255,255,255,0.3)' : theme.primary + '20'};
|
|
626
|
-
transform:scale(0);animation:ripple 0.6s ease-out;
|
|
627
|
-
pointer-events:none;
|
|
628
|
-
`;
|
|
629
|
-
btn.appendChild(ripple);
|
|
630
|
-
setTimeout(() => ripple.remove(), 600);
|
|
631
|
-
});
|
|
632
|
-
btn.addEventListener("mouseenter", () => {
|
|
633
|
-
btn.style.transform = "translateY(-2px)";
|
|
634
|
-
if (isPrimary) {
|
|
635
|
-
btn.style.boxShadow = `0 12px 32px ${theme.primary}30, 0 6px 16px ${theme.primary}20`;
|
|
636
|
-
}
|
|
637
|
-
else {
|
|
638
|
-
btn.style.boxShadow = `0 4px 16px ${theme.border}30, 0 2px 8px ${theme.border}20`;
|
|
639
|
-
btn.style.borderColor = theme.primary;
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
btn.addEventListener("mouseleave", () => {
|
|
643
|
-
btn.style.transform = "translateY(0)";
|
|
644
|
-
if (isPrimary) {
|
|
645
|
-
btn.style.boxShadow = `0 8px 24px ${theme.primary}25, 0 4px 12px ${theme.primary}15`;
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
btn.style.boxShadow = `0 2px 8px ${theme.border}20`;
|
|
649
|
-
btn.style.borderColor = theme.border;
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
// Add ripple animation
|
|
653
|
-
if (!document.head.querySelector('style[data-ripple-animation]')) {
|
|
654
|
-
const style = document.createElement('style');
|
|
655
|
-
style.setAttribute('data-ripple-animation', 'true');
|
|
656
|
-
style.textContent = `
|
|
657
|
-
@keyframes ripple {
|
|
658
|
-
to { transform: scale(4); opacity: 0; }
|
|
659
|
-
}
|
|
660
|
-
`;
|
|
661
|
-
document.head.appendChild(style);
|
|
662
|
-
}
|
|
663
|
-
return btn;
|
|
685
|
+
this.videoWrapEl = null;
|
|
664
686
|
}
|
|
665
687
|
showLoadingState(message, theme) {
|
|
666
688
|
if (!this.root)
|
|
667
689
|
return;
|
|
668
690
|
this.root.innerHTML = "";
|
|
669
691
|
this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:40px 20px;min-height:300px;";
|
|
670
|
-
|
|
671
|
-
const spinnerContainer = document.createElement("div");
|
|
672
|
-
spinnerContainer.style.cssText = "position:relative;width:80px;height:80px;";
|
|
673
|
-
const spinner = document.createElement("div");
|
|
674
|
-
spinner.style.cssText = `
|
|
675
|
-
position:absolute;inset:0;border-radius:50%;
|
|
676
|
-
border:4px solid ${theme.border};
|
|
677
|
-
border-top-color:${theme.primary};
|
|
678
|
-
animation:sc-spin 1s linear infinite;
|
|
679
|
-
`;
|
|
680
|
-
spinnerContainer.appendChild(spinner);
|
|
681
|
-
const centerIcon = document.createElement("div");
|
|
682
|
-
centerIcon.style.cssText = `
|
|
683
|
-
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
|
684
|
-
font-size:28px;animation:pulse 2s ease-in-out infinite;
|
|
685
|
-
`;
|
|
686
|
-
centerIcon.textContent = "📄";
|
|
687
|
-
spinnerContainer.appendChild(centerIcon);
|
|
688
|
-
this.root.appendChild(spinnerContainer);
|
|
689
|
-
// Loading message
|
|
692
|
+
this.root.appendChild((0, spinner_1.renderSpinner)(theme, { size: 80, icon: (0, icons_1.icon)("document", 28) }));
|
|
690
693
|
const loadingText = document.createElement("div");
|
|
691
|
-
loadingText.style.cssText = `
|
|
692
|
-
color:${theme.text};font-size:16px;font-weight:600;text-align:center;
|
|
693
|
-
animation:fadeIn 0.5s ease-out;
|
|
694
|
-
`;
|
|
694
|
+
loadingText.style.cssText = `color:${theme.text};font-size:${theme_1.TYPE.heading}px;font-weight:600;text-align:center;`;
|
|
695
695
|
loadingText.textContent = message;
|
|
696
696
|
this.root.appendChild(loadingText);
|
|
697
|
-
// Add pulse animation
|
|
698
|
-
if (!document.head.querySelector('style[data-pulse-animation]')) {
|
|
699
|
-
const style = document.createElement('style');
|
|
700
|
-
style.setAttribute('data-pulse-animation', 'true');
|
|
701
|
-
style.textContent = `
|
|
702
|
-
@keyframes pulse {
|
|
703
|
-
0%, 100% { transform: scale(1); opacity: 1; }
|
|
704
|
-
50% { transform: scale(1.1); opacity: 0.8; }
|
|
705
|
-
}
|
|
706
|
-
@keyframes fadeIn {
|
|
707
|
-
from { opacity: 0; transform: translateY(10px); }
|
|
708
|
-
to { opacity: 1; transform: translateY(0); }
|
|
709
|
-
}
|
|
710
|
-
`;
|
|
711
|
-
document.head.appendChild(style);
|
|
712
|
-
}
|
|
713
697
|
}
|
|
714
698
|
showError(message) {
|
|
715
699
|
if (!this.root)
|
|
@@ -717,7 +701,7 @@ class DocumentCapture {
|
|
|
717
701
|
const errorContainer = document.createElement("div");
|
|
718
702
|
errorContainer.style.cssText = `
|
|
719
703
|
display:flex;align-items:center;gap:12px;padding:16px;
|
|
720
|
-
background:#ff444415;border:1px solid #ff444430;border-radius
|
|
704
|
+
background:#ff444415;border:1px solid #ff444430;border-radius:${theme_1.RADIUS.md}px;
|
|
721
705
|
color:#ff4444;font-size:14px;margin-top:16px;animation:shake 0.5s ease-in-out;
|
|
722
706
|
`;
|
|
723
707
|
errorContainer.innerHTML = `
|
|
@@ -744,7 +728,7 @@ class DocumentCapture {
|
|
|
744
728
|
const btn = document.createElement("button");
|
|
745
729
|
btn.textContent = label;
|
|
746
730
|
btn.style.cssText = `
|
|
747
|
-
padding:16px 24px;border-radius
|
|
731
|
+
padding:16px 24px;border-radius:${theme_1.RADIUS.md}px;font-size:16px;font-weight:600;
|
|
748
732
|
cursor:pointer;transition:all 0.2s ease;border:none;width:100%;
|
|
749
733
|
min-height:52px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
|
|
750
734
|
${isPrimary
|
|
@@ -777,7 +761,7 @@ class DocumentCapture {
|
|
|
777
761
|
const swapped = orientation >= 5 && orientation <= 8;
|
|
778
762
|
const srcW = swapped ? h : w;
|
|
779
763
|
const srcH = swapped ? w : h;
|
|
780
|
-
const ratio =
|
|
764
|
+
const ratio = (0, documentRules_1.computeScaleRatio)(srcW, srcH, maxWidth, maxHeight);
|
|
781
765
|
const outW = Math.round(srcW * ratio);
|
|
782
766
|
const outH = Math.round(srcH * ratio);
|
|
783
767
|
const canvas = document.createElement("canvas");
|
|
@@ -824,7 +808,7 @@ class DocumentCapture {
|
|
|
824
808
|
imgEl.onload = () => {
|
|
825
809
|
URL.revokeObjectURL(url);
|
|
826
810
|
let { naturalWidth: w, naturalHeight: h } = imgEl;
|
|
827
|
-
const ratio =
|
|
811
|
+
const ratio = (0, documentRules_1.computeScaleRatio)(w, h, maxWidth, maxHeight);
|
|
828
812
|
const canvas = document.createElement("canvas");
|
|
829
813
|
canvas.width = Math.round(w * ratio);
|
|
830
814
|
canvas.height = Math.round(h * ratio);
|