smartcomply-web-sdk 1.0.3 → 1.0.4

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 (130) hide show
  1. package/README.md +43 -0
  2. package/dist/esm/camera/ActionDetector.d.ts +26 -0
  3. package/dist/esm/camera/ActionDetector.d.ts.map +1 -0
  4. package/dist/esm/camera/ActionDetector.js +110 -0
  5. package/dist/esm/camera/ActionDetector.js.map +1 -0
  6. package/dist/esm/camera/CameraManager.d.ts +7 -0
  7. package/dist/esm/camera/CameraManager.d.ts.map +1 -0
  8. package/dist/esm/camera/CameraManager.js +27 -0
  9. package/dist/esm/camera/CameraManager.js.map +1 -0
  10. package/dist/esm/camera/FaceDetector.d.ts +12 -0
  11. package/dist/esm/camera/FaceDetector.d.ts.map +1 -0
  12. package/dist/esm/camera/FaceDetector.js +51 -0
  13. package/dist/esm/camera/FaceDetector.js.map +1 -0
  14. package/dist/esm/camera/Permission.d.ts +42 -0
  15. package/dist/esm/camera/Permission.d.ts.map +1 -0
  16. package/dist/esm/camera/Permission.js +101 -0
  17. package/dist/esm/camera/Permission.js.map +1 -0
  18. package/dist/esm/camera/VideoRecorder.d.ts +12 -0
  19. package/dist/esm/camera/VideoRecorder.d.ts.map +1 -0
  20. package/dist/esm/camera/VideoRecorder.js +47 -0
  21. package/dist/esm/camera/VideoRecorder.js.map +1 -0
  22. package/dist/esm/client/Config.d.ts +9 -0
  23. package/dist/esm/client/Config.d.ts.map +1 -0
  24. package/dist/esm/client/Config.js +5 -0
  25. package/dist/esm/client/Config.js.map +1 -0
  26. package/dist/esm/client/HttpClient.d.ts +44 -0
  27. package/dist/esm/client/HttpClient.d.ts.map +1 -0
  28. package/dist/esm/client/HttpClient.js +145 -0
  29. package/dist/esm/client/HttpClient.js.map +1 -0
  30. package/dist/esm/client/Smartcomply.d.ts +50 -0
  31. package/dist/esm/client/Smartcomply.d.ts.map +1 -0
  32. package/dist/esm/client/Smartcomply.js +73 -0
  33. package/dist/esm/client/Smartcomply.js.map +1 -0
  34. package/dist/esm/components/CameraView.d.ts +59 -0
  35. package/dist/esm/components/CameraView.d.ts.map +1 -0
  36. package/dist/esm/components/CameraView.js +238 -0
  37. package/dist/esm/components/CameraView.js.map +1 -0
  38. package/dist/esm/components/DocumentUpload.d.ts +21 -0
  39. package/dist/esm/components/DocumentUpload.d.ts.map +1 -0
  40. package/dist/esm/components/DocumentUpload.js +216 -0
  41. package/dist/esm/components/DocumentUpload.js.map +1 -0
  42. package/dist/esm/errors/AuthError.d.ts +8 -0
  43. package/dist/esm/errors/AuthError.d.ts.map +1 -0
  44. package/dist/esm/errors/AuthError.js +11 -0
  45. package/dist/esm/errors/AuthError.js.map +1 -0
  46. package/dist/esm/errors/NetworkError.d.ts +8 -0
  47. package/dist/esm/errors/NetworkError.d.ts.map +1 -0
  48. package/dist/esm/errors/NetworkError.js +11 -0
  49. package/dist/esm/errors/NetworkError.js.map +1 -0
  50. package/dist/esm/errors/SDKError.d.ts +7 -0
  51. package/dist/esm/errors/SDKError.d.ts.map +1 -0
  52. package/dist/esm/errors/SDKError.js +10 -0
  53. package/dist/esm/errors/SDKError.js.map +1 -0
  54. package/dist/esm/flow/DocumentCapture.d.ts +54 -0
  55. package/dist/esm/flow/DocumentCapture.d.ts.map +1 -0
  56. package/dist/esm/flow/DocumentCapture.js +797 -0
  57. package/dist/esm/flow/DocumentCapture.js.map +1 -0
  58. package/dist/esm/flow/SmartComplyFlow.d.ts +104 -0
  59. package/dist/esm/flow/SmartComplyFlow.d.ts.map +1 -0
  60. package/dist/esm/flow/SmartComplyFlow.js +1722 -0
  61. package/dist/esm/flow/SmartComplyFlow.js.map +1 -0
  62. package/dist/esm/flow/theme.d.ts +30 -0
  63. package/dist/esm/flow/theme.d.ts.map +1 -0
  64. package/dist/esm/flow/theme.js +99 -0
  65. package/dist/esm/flow/theme.js.map +1 -0
  66. package/dist/esm/index.d.ts +20 -0
  67. package/dist/esm/index.d.ts.map +1 -0
  68. package/dist/esm/index.js +15 -0
  69. package/dist/esm/index.js.map +1 -0
  70. package/dist/esm/modules/liveness/LivenessUI.d.ts +46 -0
  71. package/dist/esm/modules/liveness/LivenessUI.d.ts.map +1 -0
  72. package/dist/esm/modules/liveness/LivenessUI.js +605 -0
  73. package/dist/esm/modules/liveness/LivenessUI.js.map +1 -0
  74. package/dist/esm/modules/liveness/index.d.ts +5 -0
  75. package/dist/esm/modules/liveness/index.d.ts.map +1 -0
  76. package/dist/esm/modules/liveness/index.js +4 -0
  77. package/dist/esm/modules/liveness/index.js.map +1 -0
  78. package/dist/esm/modules/liveness/liveness.d.ts +85 -0
  79. package/dist/esm/modules/liveness/liveness.d.ts.map +1 -0
  80. package/dist/esm/modules/liveness/liveness.js +421 -0
  81. package/dist/esm/modules/liveness/liveness.js.map +1 -0
  82. package/dist/esm/modules/liveness/uploader.d.ts +34 -0
  83. package/dist/esm/modules/liveness/uploader.d.ts.map +1 -0
  84. package/dist/esm/modules/liveness/uploader.js +102 -0
  85. package/dist/esm/modules/liveness/uploader.js.map +1 -0
  86. package/dist/esm/modules/onboarding/index.d.ts +2 -0
  87. package/dist/esm/modules/onboarding/index.d.ts.map +1 -0
  88. package/dist/esm/modules/onboarding/index.js +2 -0
  89. package/dist/esm/modules/onboarding/index.js.map +1 -0
  90. package/dist/esm/modules/onboarding/onboarding.d.ts +25 -0
  91. package/dist/esm/modules/onboarding/onboarding.d.ts.map +1 -0
  92. package/dist/esm/modules/onboarding/onboarding.js +29 -0
  93. package/dist/esm/modules/onboarding/onboarding.js.map +1 -0
  94. package/dist/esm/types/common.d.ts +27 -0
  95. package/dist/esm/types/common.d.ts.map +1 -0
  96. package/dist/esm/types/common.js +2 -0
  97. package/dist/esm/types/common.js.map +1 -0
  98. package/dist/esm/types/liveness.d.ts +142 -0
  99. package/dist/esm/types/liveness.d.ts.map +1 -0
  100. package/dist/esm/types/liveness.js +2 -0
  101. package/dist/esm/types/liveness.js.map +1 -0
  102. package/dist/esm/types/onboarding.d.ts +53 -0
  103. package/dist/esm/types/onboarding.d.ts.map +1 -0
  104. package/dist/esm/types/onboarding.js +2 -0
  105. package/dist/esm/types/onboarding.js.map +1 -0
  106. package/dist/esm/types/quality.d.ts +23 -0
  107. package/dist/esm/types/quality.d.ts.map +1 -0
  108. package/dist/esm/types/quality.js +5 -0
  109. package/dist/esm/types/quality.js.map +1 -0
  110. package/dist/esm/utils/ImageQuality.d.ts +48 -0
  111. package/dist/esm/utils/ImageQuality.d.ts.map +1 -0
  112. package/dist/esm/utils/ImageQuality.js +177 -0
  113. package/dist/esm/utils/ImageQuality.js.map +1 -0
  114. package/dist/esm/utils/constants.d.ts +59 -0
  115. package/dist/esm/utils/constants.d.ts.map +1 -0
  116. package/dist/esm/utils/constants.js +67 -0
  117. package/dist/esm/utils/constants.js.map +1 -0
  118. package/dist/esm/utils/device.d.ts +14 -0
  119. package/dist/esm/utils/device.d.ts.map +1 -0
  120. package/dist/esm/utils/device.js +52 -0
  121. package/dist/esm/utils/device.js.map +1 -0
  122. package/dist/esm/utils/logger.d.ts +28 -0
  123. package/dist/esm/utils/logger.d.ts.map +1 -0
  124. package/dist/esm/utils/logger.js +64 -0
  125. package/dist/esm/utils/logger.js.map +1 -0
  126. package/dist/esm/utils/validator.d.ts +35 -0
  127. package/dist/esm/utils/validator.d.ts.map +1 -0
  128. package/dist/esm/utils/validator.js +102 -0
  129. package/dist/esm/utils/validator.js.map +1 -0
  130. package/package.json +13 -5
@@ -0,0 +1,797 @@
1
+ export class DocumentCapture {
2
+ constructor() {
3
+ this.root = null;
4
+ this.videoEl = null;
5
+ this.stream = null;
6
+ this.resolveCapture = null;
7
+ this.rejectCapture = null;
8
+ this.frontBlob = null;
9
+ this.backBlob = null;
10
+ this.currentSide = 'front';
11
+ }
12
+ /**
13
+ * Mount the document capture UI and return a promise that resolves
14
+ * with the captured document image(s).
15
+ */
16
+ capture(container, theme, documentType) {
17
+ return new Promise((resolve, reject) => {
18
+ // Validate input parameters
19
+ if (!container) {
20
+ reject(new Error("Container element is required"));
21
+ return;
22
+ }
23
+ if (!theme) {
24
+ reject(new Error("Theme configuration is required"));
25
+ return;
26
+ }
27
+ if (!documentType || typeof documentType !== 'string') {
28
+ reject(new Error("Document type is required and must be a string"));
29
+ return;
30
+ }
31
+ // Check if already capturing
32
+ if (this.resolveCapture || this.rejectCapture) {
33
+ reject(new Error("Document capture is already in progress"));
34
+ return;
35
+ }
36
+ this.resolveCapture = resolve;
37
+ this.rejectCapture = reject;
38
+ this.frontBlob = null;
39
+ this.backBlob = null;
40
+ this.currentSide = 'front';
41
+ this.renderChoiceScreen(container, theme, documentType);
42
+ });
43
+ }
44
+ destroy() {
45
+ // Stop any ongoing camera streams
46
+ this.stopCamera();
47
+ // Clean up DOM elements
48
+ if (this.root && this.root.parentElement) {
49
+ this.root.parentElement.removeChild(this.root);
50
+ }
51
+ this.root = null;
52
+ // Clean up any pending promises
53
+ if (this.rejectCapture) {
54
+ this.rejectCapture(new Error("Document capture was cancelled"));
55
+ this.rejectCapture = null;
56
+ }
57
+ this.resolveCapture = null;
58
+ // Clean up blob data
59
+ this.frontBlob = null;
60
+ this.backBlob = null;
61
+ this.currentSide = 'front';
62
+ }
63
+ requiresBackSide(documentType) {
64
+ const type = documentType.toLowerCase();
65
+ return type.includes("voter") ||
66
+ type.includes("nin") ||
67
+ type.includes("driver") ||
68
+ type.includes("license") ||
69
+ type.includes("national id") ||
70
+ type.includes("national id card") ||
71
+ type.includes("ghana");
72
+ }
73
+ renderChoiceScreen(container, theme, documentType) {
74
+ this.root = document.createElement("div");
75
+ this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:24px;padding:24px 20px;max-width:100%;";
76
+ const docLabel = this.getDocumentLabel(documentType);
77
+ const requiresBack = this.requiresBackSide(documentType);
78
+ const isCapturingFront = this.currentSide === 'front';
79
+ // Header with enhanced illustration
80
+ const header = document.createElement("div");
81
+ header.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;text-align:center;";
82
+ // Enhanced illustration with gradient background
83
+ const iconContainer = document.createElement("div");
84
+ iconContainer.style.cssText = `
85
+ width:120px;height:120px;border-radius:24px;
86
+ display:flex;align-items:center;justify-content:center;
87
+ background:linear-gradient(135deg, ${theme.primary}15, ${theme.primary}05);
88
+ border:3px solid ${theme.primary}30;
89
+ position:relative;overflow:hidden;
90
+ box-shadow:0 8px 32px ${theme.primary}20;
91
+ `;
92
+ const icon = document.createElement("div");
93
+ icon.style.cssText = "font-size:48px;animation:float 3s ease-in-out infinite;";
94
+ icon.textContent = "🪪";
95
+ iconContainer.appendChild(icon);
96
+ // Add floating animation
97
+ if (!document.head.querySelector('style[data-float-animation]')) {
98
+ const style = document.createElement('style');
99
+ style.setAttribute('data-float-animation', 'true');
100
+ style.textContent = `
101
+ @keyframes float {
102
+ 0%, 100% { transform: translateY(0px); }
103
+ 50% { transform: translateY(-10px); }
104
+ }
105
+ `;
106
+ document.head.appendChild(style);
107
+ }
108
+ header.appendChild(iconContainer);
109
+ // Enhanced title and subtitle
110
+ const title = document.createElement("div");
111
+ title.style.cssText = `color:${theme.text};font-size:20px;font-weight:700;text-align:center;line-height:1.4;`;
112
+ let labelText = `Capture your ${docLabel}`;
113
+ if (requiresBack) {
114
+ labelText += ` (${isCapturingFront ? 'Front Side' : 'Back Side'})`;
115
+ }
116
+ title.textContent = labelText;
117
+ header.appendChild(title);
118
+ const subtitle = document.createElement("div");
119
+ subtitle.style.cssText = `color:${theme.textSecondary};font-size:14px;text-align:center;line-height:1.5;max-width:280px;`;
120
+ subtitle.textContent = "Choose how you'd like to provide your document";
121
+ header.appendChild(subtitle);
122
+ this.root.appendChild(header);
123
+ // Enhanced progress indicator for multi-sided documents
124
+ if (requiresBack) {
125
+ const progress = document.createElement("div");
126
+ progress.style.cssText = `
127
+ display:flex;flex-direction:column;align-items:center;gap:12px;
128
+ padding:16px 20px;background:${theme.primary}08;border-radius:16px;
129
+ border:1px solid ${theme.primary}20;width:100%;max-width:320px;
130
+ `;
131
+ progress.innerHTML = `
132
+ <div style="display:flex;gap:8px;align-items:center;">
133
+ <div style="width:12px;height:12px;border-radius:50%;background:${isCapturingFront ? theme.primary : theme.border};transition:all 0.3s ease;"></div>
134
+ <div style="width:32px;height:2px;background:${theme.border};border-radius:1px;"></div>
135
+ <div style="width:12px;height:12px;border-radius:50%;background:${!isCapturingFront ? theme.primary : theme.border};transition:all 0.3s ease;"></div>
136
+ </div>
137
+ <span style="color:${theme.text};font-size:13px;font-weight:600;">${isCapturingFront ? 'Step 1 of 2' : 'Step 2 of 2'}</span>
138
+ `;
139
+ this.root.appendChild(progress);
140
+ }
141
+ // Enhanced action buttons container
142
+ const actionsContainer = document.createElement("div");
143
+ actionsContainer.style.cssText = "display:flex;flex-direction:column;gap:16px;width:100%;max-width:320px;";
144
+ // Camera button with enhanced design
145
+ const cameraBtn = this.createEnhancedButton("📷 Take Photo with Camera", theme, true, "Open camera to capture document");
146
+ cameraBtn.addEventListener("click", () => {
147
+ this.showLoadingState("Opening camera...", theme);
148
+ this.root.innerHTML = "";
149
+ setTimeout(() => {
150
+ this.renderCameraView(this.root, theme, documentType);
151
+ }, 500);
152
+ });
153
+ actionsContainer.appendChild(cameraBtn);
154
+ // Upload button with enhanced design
155
+ const uploadBtn = this.createEnhancedButton("📁 Choose from Gallery", theme, false, "Upload document image from device");
156
+ uploadBtn.addEventListener("click", () => {
157
+ this.handleFileUpload(container, theme);
158
+ });
159
+ actionsContainer.appendChild(uploadBtn);
160
+ this.root.appendChild(actionsContainer);
161
+ // Enhanced tips section
162
+ const tips = document.createElement("div");
163
+ tips.style.cssText = `
164
+ width:100%;padding:16px 20px;border-radius:16px;
165
+ background:${theme.shimmer};border:1px solid ${theme.border};
166
+ margin-top:8px;
167
+ `;
168
+ const tipsTitle = document.createElement("div");
169
+ tipsTitle.style.cssText = `font-weight:700;color:${theme.text};margin-bottom:12px;font-size:14px;display:flex;align-items:center;gap:8px;`;
170
+ tipsTitle.innerHTML = `💡 Tips for best results`;
171
+ tips.appendChild(tipsTitle);
172
+ const tipsList = document.createElement("div");
173
+ tipsList.style.cssText = `display:grid;gap:8px;font-size:13px;color:${theme.textSecondary};line-height:1.6;`;
174
+ tipsList.innerHTML = `
175
+ <div style="display:flex;gap:8px;align-items:flex-start;">
176
+ <span style="color:${theme.primary};font-weight:bold;">✓</span>
177
+ <span>Lay document flat on a solid surface</span>
178
+ </div>
179
+ <div style="display:flex;gap:8px;align-items:flex-start;">
180
+ <span style="color:${theme.primary};font-weight:bold;">✓</span>
181
+ <span>Ensure all four corners are visible</span>
182
+ </div>
183
+ <div style="display:flex;gap:8px;align-items:flex-start;">
184
+ <span style="color:${theme.primary};font-weight:bold;">✓</span>
185
+ <span>Use good lighting, avoid glare</span>
186
+ </div>
187
+ <div style="display:flex;gap:8px;align-items:flex-start;">
188
+ <span style="color:${theme.primary};font-weight:bold;">✓</span>
189
+ <span>Make sure all text is clearly readable</span>
190
+ </div>
191
+ `;
192
+ tips.appendChild(tipsList);
193
+ this.root.appendChild(tips);
194
+ // File input (hidden)
195
+ const fileInput = document.createElement("input");
196
+ fileInput.type = "file";
197
+ fileInput.accept = "image/jpeg,image/png";
198
+ fileInput.style.display = "none";
199
+ fileInput.id = "sc-doc-file-input";
200
+ fileInput.addEventListener("change", (e) => {
201
+ const file = e.target.files?.[0];
202
+ if (file) {
203
+ // Validate file
204
+ const validationError = this.validateFile(file);
205
+ if (validationError) {
206
+ this.showError(validationError);
207
+ return;
208
+ }
209
+ this.showLoadingState("Processing image...", theme);
210
+ this.compressImage(file, 1280, 960, 0.85)
211
+ .then((compressed) => this.handleFileUploadComplete(compressed, container, theme, documentType))
212
+ .catch(() => this.handleFileUploadComplete(file, container, theme, documentType)); // fallback to original if compress fails
213
+ }
214
+ });
215
+ this.root.appendChild(fileInput);
216
+ container.appendChild(this.root);
217
+ }
218
+ renderCameraView(container, theme, documentType) {
219
+ const wrapper = document.createElement("div");
220
+ wrapper.style.cssText = "position:relative;width:100%;border-radius:12px;overflow:hidden;background:#000;";
221
+ this.videoEl = document.createElement("video");
222
+ this.videoEl.autoplay = true;
223
+ this.videoEl.playsInline = true;
224
+ this.videoEl.muted = true;
225
+ this.videoEl.style.cssText = "width:100%;display:block;aspect-ratio:4/3;object-fit:cover;";
226
+ wrapper.appendChild(this.videoEl);
227
+ // Guide overlay — rectangular
228
+ const guide = document.createElement("div");
229
+ guide.style.cssText = `
230
+ position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
231
+ pointer-events:none;
232
+ `;
233
+ const rect = document.createElement("div");
234
+ rect.style.cssText = `
235
+ width:85%;height:60%;border:2px solid rgba(255,255,255,0.6);
236
+ border-radius:12px;box-shadow:0 0 0 9999px rgba(0,0,0,0.4);
237
+ `;
238
+ guide.appendChild(rect);
239
+ wrapper.appendChild(guide);
240
+ // Guide label
241
+ const guideLbl = document.createElement("div");
242
+ guideLbl.style.cssText = `
243
+ position:absolute;bottom:60px;left:0;right:0;text-align:center;
244
+ color:#fff;font-size:13px;font-weight:600;
245
+ text-shadow:0 1px 3px rgba(0,0,0,0.5);
246
+ `;
247
+ guideLbl.textContent = `Align your ${this.getDocumentLabel(documentType)} within the frame`;
248
+ wrapper.appendChild(guideLbl);
249
+ container.appendChild(wrapper);
250
+ // Capture button
251
+ const captureBtn = document.createElement("button");
252
+ captureBtn.style.cssText = `
253
+ display:block;margin:16px auto 0;width:64px;height:64px;
254
+ border-radius:50%;border:4px solid ${theme.primary};
255
+ background:transparent;cursor:pointer;position:relative;
256
+ transition:all 0.2s ease;
257
+ `;
258
+ const inner = document.createElement("div");
259
+ inner.style.cssText = `
260
+ width:48px;height:48px;border-radius:50%;background:${theme.primary};
261
+ position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
262
+ transition:all 0.15s ease;
263
+ `;
264
+ captureBtn.appendChild(inner);
265
+ captureBtn.addEventListener("mousedown", () => {
266
+ inner.style.transform = "translate(-50%,-50%) scale(0.9)";
267
+ });
268
+ captureBtn.addEventListener("mouseup", () => {
269
+ inner.style.transform = "translate(-50%,-50%) scale(1)";
270
+ });
271
+ captureBtn.addEventListener("click", () => {
272
+ this.captureFrame(container, theme, documentType);
273
+ });
274
+ container.appendChild(captureBtn);
275
+ // Open camera
276
+ this.openCamera();
277
+ }
278
+ async openCamera() {
279
+ // Check browser compatibility
280
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
281
+ this.rejectCapture?.(new Error("Camera access is not supported in this browser. Please use a modern browser like Chrome, Firefox, or Safari."));
282
+ return;
283
+ }
284
+ const maxRetries = 3;
285
+ let retryCount = 0;
286
+ const attemptCameraAccess = async (constraints) => {
287
+ try {
288
+ return await navigator.mediaDevices.getUserMedia(constraints);
289
+ }
290
+ catch (err) {
291
+ console.warn(`Camera access attempt ${retryCount + 1} failed:`, err);
292
+ if (retryCount < maxRetries - 1) {
293
+ retryCount++;
294
+ await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); // Exponential backoff
295
+ return attemptCameraAccess(constraints);
296
+ }
297
+ throw err;
298
+ }
299
+ };
300
+ try {
301
+ // Try rear camera first
302
+ this.stream = await attemptCameraAccess({
303
+ video: {
304
+ facingMode: { ideal: "environment" },
305
+ width: { ideal: 1280 },
306
+ height: { ideal: 960 },
307
+ },
308
+ });
309
+ }
310
+ catch (err) {
311
+ console.warn("Rear camera failed, trying any camera:", err);
312
+ // Fallback to any camera
313
+ try {
314
+ this.stream = await attemptCameraAccess({
315
+ video: { width: { ideal: 1280 } },
316
+ });
317
+ }
318
+ catch (fallbackErr) {
319
+ const errorMessage = this.getCameraErrorMessage(fallbackErr);
320
+ this.rejectCapture?.(new Error(errorMessage));
321
+ return;
322
+ }
323
+ }
324
+ if (this.videoEl) {
325
+ this.videoEl.srcObject = this.stream;
326
+ }
327
+ }
328
+ getCameraErrorMessage(error) {
329
+ if (error.name === 'NotAllowedError') {
330
+ return "Camera access denied. Please allow camera permissions and try again.";
331
+ }
332
+ else if (error.name === 'NotFoundError') {
333
+ return "No camera found. Please ensure your device has a working camera.";
334
+ }
335
+ else if (error.name === 'NotReadableError') {
336
+ return "Camera is already in use by another application. Please close other apps using the camera and try again.";
337
+ }
338
+ else if (error.name === 'OverconstrainedError') {
339
+ return "Camera constraints not supported. Trying with different settings...";
340
+ }
341
+ else {
342
+ return "Camera access failed. Please check your camera permissions and try again.";
343
+ }
344
+ }
345
+ captureFrame(container, theme, documentType) {
346
+ if (!this.videoEl)
347
+ return;
348
+ const MAX_W = 1280;
349
+ const MAX_H = 960;
350
+ const vw = this.videoEl.videoWidth || MAX_W;
351
+ const vh = this.videoEl.videoHeight || MAX_H;
352
+ const ratio = Math.min(MAX_W / vw, MAX_H / vh, 1);
353
+ const canvas = document.createElement("canvas");
354
+ canvas.width = Math.round(vw * ratio);
355
+ canvas.height = Math.round(vh * ratio);
356
+ const ctx = canvas.getContext("2d");
357
+ if (!ctx)
358
+ return;
359
+ ctx.drawImage(this.videoEl, 0, 0, canvas.width, canvas.height);
360
+ canvas.toBlob((blob) => {
361
+ if (blob) {
362
+ this.stopCamera();
363
+ this.showPreview(container, theme, blob, documentType);
364
+ }
365
+ }, "image/jpeg", 0.85);
366
+ }
367
+ showPreview(container, theme, imageData, documentType) {
368
+ if (this.root)
369
+ this.root.innerHTML = "";
370
+ else {
371
+ this.root = document.createElement("div");
372
+ container.appendChild(this.root);
373
+ }
374
+ this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;gap:16px;";
375
+ // Preview image
376
+ const img = document.createElement("img");
377
+ const imageUrl = URL.createObjectURL(imageData);
378
+ img.src = imageUrl;
379
+ img.style.cssText = `
380
+ width:100%;max-height:300px;object-fit:contain;border-radius:12px;
381
+ border:2px solid ${theme.border};
382
+ `;
383
+ this.root.appendChild(img);
384
+ // Store URL for cleanup
385
+ img._blobUrl = imageUrl;
386
+ const label = document.createElement("div");
387
+ label.style.cssText = `color:${theme.textSecondary};font-size:13px;text-align:center;font-weight:500;`;
388
+ label.textContent = "Does the photo look clear?";
389
+ this.root.appendChild(label);
390
+ const sublabel = document.createElement("div");
391
+ sublabel.style.cssText = `color:${theme.textMuted};font-size:11px;text-align:center;line-height:1.6;`;
392
+ sublabel.textContent = "All text must be readable and all four corners must be visible.";
393
+ this.root.appendChild(sublabel);
394
+ // Buttons
395
+ const btnRow = document.createElement("div");
396
+ btnRow.style.cssText = "display:flex;gap:12px;width:100%;";
397
+ const retakeBtn = this.createButton("Retake", theme, false);
398
+ retakeBtn.style.flex = "1";
399
+ retakeBtn.addEventListener("click", () => {
400
+ if (img._blobUrl) {
401
+ URL.revokeObjectURL(img._blobUrl);
402
+ }
403
+ this.root.innerHTML = "";
404
+ this.renderChoiceScreen(container, theme, documentType);
405
+ });
406
+ const useBtn = this.createButton("Use Photo ✓", theme, true);
407
+ useBtn.style.flex = "1";
408
+ useBtn.addEventListener("click", () => {
409
+ if (img._blobUrl) {
410
+ URL.revokeObjectURL(img._blobUrl);
411
+ }
412
+ useBtn.textContent = "Processing...";
413
+ useBtn.disabled = true;
414
+ this.compressImage(imageData, 1280, 960, 0.85)
415
+ .then((compressed) => this.handleCaptureComplete(compressed, container, theme, documentType))
416
+ .catch(() => this.handleCaptureComplete(imageData, container, theme, documentType)); // fallback to original
417
+ });
418
+ btnRow.appendChild(retakeBtn);
419
+ btnRow.appendChild(useBtn);
420
+ this.root.appendChild(btnRow);
421
+ }
422
+ handleCaptureComplete(imageData, container, theme, documentType) {
423
+ const requiresBack = this.requiresBackSide(documentType);
424
+ const wasCapturingFront = this.currentSide === 'front';
425
+ // Store the captured image
426
+ if (wasCapturingFront) {
427
+ this.frontBlob = imageData;
428
+ }
429
+ else {
430
+ this.backBlob = imageData;
431
+ }
432
+ // If we need back side and haven't captured it yet, move to back side
433
+ if (requiresBack && wasCapturingFront) {
434
+ this.currentSide = 'back';
435
+ this.root.innerHTML = "";
436
+ this.renderChoiceScreen(container, theme, documentType);
437
+ return;
438
+ }
439
+ // All captures complete, resolve with result
440
+ if (!this.frontBlob) {
441
+ this.rejectCapture?.(new Error("No front image captured. Please try again."));
442
+ return;
443
+ }
444
+ const result = {
445
+ front: this.frontBlob,
446
+ back: this.backBlob || undefined
447
+ };
448
+ this.resolveCapture?.(result);
449
+ }
450
+ handleFileUploadComplete(imageData, container, theme, documentType) {
451
+ // Show preview first, then handle completion
452
+ this.showPreview(container, theme, imageData, documentType);
453
+ }
454
+ validateFile(file) {
455
+ // Check file type
456
+ const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png'];
457
+ if (!allowedTypes.includes(file.type)) {
458
+ return "Invalid file type. Please upload a JPEG or PNG image.";
459
+ }
460
+ // Check file size (max 10MB)
461
+ const maxSize = 10 * 1024 * 1024;
462
+ if (file.size > maxSize) {
463
+ return "File is too large. Please upload an image smaller than 10MB.";
464
+ }
465
+ // Check minimum file size (at least 1KB to avoid empty files)
466
+ if (file.size < 1024) {
467
+ return "File is too small. Please upload a valid image file.";
468
+ }
469
+ return null;
470
+ }
471
+ handleFileUpload(container, theme) {
472
+ const input = this.root?.querySelector("#sc-doc-file-input");
473
+ if (input)
474
+ input.click();
475
+ }
476
+ stopCamera() {
477
+ if (this.stream) {
478
+ this.stream.getTracks().forEach((t) => t.stop());
479
+ this.stream = null;
480
+ }
481
+ if (this.videoEl) {
482
+ this.videoEl.srcObject = null;
483
+ this.videoEl = null;
484
+ }
485
+ }
486
+ createEnhancedButton(label, theme, isPrimary, ariaLabel) {
487
+ const btn = document.createElement("button");
488
+ btn.textContent = label;
489
+ btn.setAttribute("aria-label", ariaLabel);
490
+ btn.style.cssText = `
491
+ padding:18px 24px;border-radius:16px;font-size:16px;font-weight:600;
492
+ cursor:pointer;transition:all 0.3s ease;border:none;width:100%;
493
+ min-height:56px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
494
+ position:relative;overflow:hidden;
495
+ ${isPrimary
496
+ ? `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;`
497
+ : `background:${theme.inputBg};color:${theme.text};border:2px solid ${theme.border};box-shadow:0 2px 8px ${theme.border}20;`}
498
+ `;
499
+ // Add ripple effect
500
+ btn.addEventListener("click", (e) => {
501
+ const ripple = document.createElement("span");
502
+ const rect = btn.getBoundingClientRect();
503
+ const size = Math.max(rect.width, rect.height);
504
+ const x = e.clientX - rect.left - size / 2;
505
+ const y = e.clientY - rect.top - size / 2;
506
+ ripple.style.cssText = `
507
+ position:absolute;width:${size}px;height:${size}px;
508
+ left:${x}px;top:${y}px;border-radius:50%;
509
+ background:${isPrimary ? 'rgba(255,255,255,0.3)' : theme.primary + '20'};
510
+ transform:scale(0);animation:ripple 0.6s ease-out;
511
+ pointer-events:none;
512
+ `;
513
+ btn.appendChild(ripple);
514
+ setTimeout(() => ripple.remove(), 600);
515
+ });
516
+ btn.addEventListener("mouseenter", () => {
517
+ btn.style.transform = "translateY(-2px)";
518
+ if (isPrimary) {
519
+ btn.style.boxShadow = `0 12px 32px ${theme.primary}30, 0 6px 16px ${theme.primary}20`;
520
+ }
521
+ else {
522
+ btn.style.boxShadow = `0 4px 16px ${theme.border}30, 0 2px 8px ${theme.border}20`;
523
+ btn.style.borderColor = theme.primary;
524
+ }
525
+ });
526
+ btn.addEventListener("mouseleave", () => {
527
+ btn.style.transform = "translateY(0)";
528
+ if (isPrimary) {
529
+ btn.style.boxShadow = `0 8px 24px ${theme.primary}25, 0 4px 12px ${theme.primary}15`;
530
+ }
531
+ else {
532
+ btn.style.boxShadow = `0 2px 8px ${theme.border}20`;
533
+ btn.style.borderColor = theme.border;
534
+ }
535
+ });
536
+ // Add ripple animation
537
+ if (!document.head.querySelector('style[data-ripple-animation]')) {
538
+ const style = document.createElement('style');
539
+ style.setAttribute('data-ripple-animation', 'true');
540
+ style.textContent = `
541
+ @keyframes ripple {
542
+ to { transform: scale(4); opacity: 0; }
543
+ }
544
+ `;
545
+ document.head.appendChild(style);
546
+ }
547
+ return btn;
548
+ }
549
+ showLoadingState(message, theme) {
550
+ if (!this.root)
551
+ return;
552
+ this.root.innerHTML = "";
553
+ this.root.style.cssText = "display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:40px 20px;min-height:300px;";
554
+ // Enhanced loading spinner
555
+ const spinnerContainer = document.createElement("div");
556
+ spinnerContainer.style.cssText = "position:relative;width:80px;height:80px;";
557
+ const spinner = document.createElement("div");
558
+ spinner.style.cssText = `
559
+ position:absolute;inset:0;border-radius:50%;
560
+ border:4px solid ${theme.border};
561
+ border-top-color:${theme.primary};
562
+ animation:sc-spin 1s linear infinite;
563
+ `;
564
+ spinnerContainer.appendChild(spinner);
565
+ const centerIcon = document.createElement("div");
566
+ centerIcon.style.cssText = `
567
+ position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
568
+ font-size:28px;animation:pulse 2s ease-in-out infinite;
569
+ `;
570
+ centerIcon.textContent = "📄";
571
+ spinnerContainer.appendChild(centerIcon);
572
+ this.root.appendChild(spinnerContainer);
573
+ // Loading message
574
+ const loadingText = document.createElement("div");
575
+ loadingText.style.cssText = `
576
+ color:${theme.text};font-size:16px;font-weight:600;text-align:center;
577
+ animation:fadeIn 0.5s ease-out;
578
+ `;
579
+ loadingText.textContent = message;
580
+ this.root.appendChild(loadingText);
581
+ // Add pulse animation
582
+ if (!document.head.querySelector('style[data-pulse-animation]')) {
583
+ const style = document.createElement('style');
584
+ style.setAttribute('data-pulse-animation', 'true');
585
+ style.textContent = `
586
+ @keyframes pulse {
587
+ 0%, 100% { transform: scale(1); opacity: 1; }
588
+ 50% { transform: scale(1.1); opacity: 0.8; }
589
+ }
590
+ @keyframes fadeIn {
591
+ from { opacity: 0; transform: translateY(10px); }
592
+ to { opacity: 1; transform: translateY(0); }
593
+ }
594
+ `;
595
+ document.head.appendChild(style);
596
+ }
597
+ }
598
+ showError(message) {
599
+ if (!this.root)
600
+ return;
601
+ const errorContainer = document.createElement("div");
602
+ errorContainer.style.cssText = `
603
+ display:flex;align-items:center;gap:12px;padding:16px;
604
+ background:#ff444415;border:1px solid #ff444430;border-radius:12px;
605
+ color:#ff4444;font-size:14px;margin-top:16px;animation:shake 0.5s ease-in-out;
606
+ `;
607
+ errorContainer.innerHTML = `
608
+ <span style="font-size:18px;">⚠️</span>
609
+ <span>${message}</span>
610
+ `;
611
+ // Add shake animation
612
+ if (!document.head.querySelector('style[data-shake-animation]')) {
613
+ const style = document.createElement('style');
614
+ style.setAttribute('data-shake-animation', 'true');
615
+ style.textContent = `
616
+ @keyframes shake {
617
+ 0%, 100% { transform: translateX(0); }
618
+ 25% { transform: translateX(-5px); }
619
+ 75% { transform: translateX(5px); }
620
+ }
621
+ `;
622
+ document.head.appendChild(style);
623
+ }
624
+ this.root.appendChild(errorContainer);
625
+ setTimeout(() => errorContainer.remove(), 5000);
626
+ }
627
+ createButton(label, theme, isPrimary) {
628
+ const btn = document.createElement("button");
629
+ btn.textContent = label;
630
+ btn.style.cssText = `
631
+ padding:16px 24px;border-radius:12px;font-size:16px;font-weight:600;
632
+ cursor:pointer;transition:all 0.2s ease;border:none;width:100%;
633
+ min-height:52px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;
634
+ ${isPrimary
635
+ ? `background:${theme.primary};color:#fff;box-shadow:0 4px 12px ${theme.primary}30;`
636
+ : `background:${theme.inputBg};color:${theme.text};border:2px solid ${theme.border};`}
637
+ `;
638
+ btn.addEventListener("mouseenter", () => {
639
+ btn.style.transform = "translateY(-1px)";
640
+ if (isPrimary)
641
+ btn.style.background = theme.primaryHover;
642
+ });
643
+ btn.addEventListener("mouseleave", () => {
644
+ btn.style.transform = "translateY(0)";
645
+ if (isPrimary)
646
+ btn.style.background = theme.primary;
647
+ });
648
+ return btn;
649
+ }
650
+ compressImage(source, maxWidth, maxHeight, quality) {
651
+ return new Promise((resolve, reject) => {
652
+ // Read EXIF orientation FIRST so we can correct rotation before canvas draw.
653
+ // Canvas ignores EXIF metadata — without this, phone uploads arrive rotated.
654
+ this._readExifOrientation(source).then((orientation) => {
655
+ const url = URL.createObjectURL(source);
656
+ const imgEl = document.createElement("img");
657
+ imgEl.onload = () => {
658
+ URL.revokeObjectURL(url);
659
+ let { naturalWidth: w, naturalHeight: h } = imgEl;
660
+ // For orientations 5-8 the image is rotated 90°/270° — swap dimensions
661
+ const swapped = orientation >= 5 && orientation <= 8;
662
+ const srcW = swapped ? h : w;
663
+ const srcH = swapped ? w : h;
664
+ const ratio = Math.min(maxWidth / srcW, maxHeight / srcH, 1);
665
+ const outW = Math.round(srcW * ratio);
666
+ const outH = Math.round(srcH * ratio);
667
+ const canvas = document.createElement("canvas");
668
+ canvas.width = outW;
669
+ canvas.height = outH;
670
+ const ctx = canvas.getContext("2d");
671
+ if (!ctx) {
672
+ reject(new Error("Canvas unavailable"));
673
+ return;
674
+ }
675
+ // Apply EXIF rotation transform before drawing
676
+ ctx.save();
677
+ if (orientation === 2) {
678
+ ctx.transform(-1, 0, 0, 1, outW, 0);
679
+ }
680
+ else if (orientation === 3) {
681
+ ctx.transform(-1, 0, 0, -1, outW, outH);
682
+ }
683
+ else if (orientation === 4) {
684
+ ctx.transform(1, 0, 0, -1, 0, outH);
685
+ }
686
+ else if (orientation === 5) {
687
+ ctx.transform(0, 1, 1, 0, 0, 0);
688
+ }
689
+ else if (orientation === 6) {
690
+ ctx.transform(0, 1, -1, 0, outH, 0);
691
+ }
692
+ else if (orientation === 7) {
693
+ ctx.transform(0, -1, -1, 0, outH, outW);
694
+ }
695
+ else if (orientation === 8) {
696
+ ctx.transform(0, -1, 1, 0, 0, outW);
697
+ }
698
+ ctx.drawImage(imgEl, 0, 0, swapped ? outH : outW, swapped ? outW : outH);
699
+ ctx.restore();
700
+ canvas.toBlob((blob) => { blob ? resolve(blob) : reject(new Error("Compression failed")); }, "image/jpeg", quality);
701
+ };
702
+ imgEl.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Image load failed")); };
703
+ imgEl.src = url;
704
+ }).catch(() => {
705
+ // If EXIF read fails, fall back to simple compression without rotation
706
+ const url = URL.createObjectURL(source);
707
+ const imgEl = document.createElement("img");
708
+ imgEl.onload = () => {
709
+ URL.revokeObjectURL(url);
710
+ let { naturalWidth: w, naturalHeight: h } = imgEl;
711
+ const ratio = Math.min(maxWidth / w, maxHeight / h, 1);
712
+ const canvas = document.createElement("canvas");
713
+ canvas.width = Math.round(w * ratio);
714
+ canvas.height = Math.round(h * ratio);
715
+ const ctx = canvas.getContext("2d");
716
+ if (!ctx) {
717
+ reject(new Error("Canvas unavailable"));
718
+ return;
719
+ }
720
+ ctx.drawImage(imgEl, 0, 0, canvas.width, canvas.height);
721
+ canvas.toBlob((blob) => { blob ? resolve(blob) : reject(new Error("Compression failed")); }, "image/jpeg", quality);
722
+ };
723
+ imgEl.onerror = () => { URL.revokeObjectURL(url); reject(new Error("Image load failed")); };
724
+ imgEl.src = url;
725
+ });
726
+ });
727
+ }
728
+ /**
729
+ * Read JPEG EXIF orientation tag (1–8) from a Blob/File.
730
+ * Returns 1 (no rotation) if EXIF is absent or unreadable.
731
+ */
732
+ _readExifOrientation(source) {
733
+ return new Promise((resolve) => {
734
+ const reader = new FileReader();
735
+ reader.onload = (e) => {
736
+ try {
737
+ const buf = e.target?.result;
738
+ const view = new DataView(buf);
739
+ if (view.getUint16(0, false) !== 0xFFD8) {
740
+ resolve(1);
741
+ return;
742
+ } // not JPEG
743
+ let offset = 2;
744
+ while (offset < view.byteLength) {
745
+ const marker = view.getUint16(offset, false);
746
+ offset += 2;
747
+ if (marker === 0xFFE1) { // APP1 — may contain EXIF
748
+ if (view.getUint32(offset + 2, false) !== 0x45786966) {
749
+ resolve(1);
750
+ return;
751
+ }
752
+ const little = view.getUint16(offset + 8, false) === 0x4949;
753
+ const ifdOff = offset + 8 + view.getUint32(offset + 12, little);
754
+ const tags = view.getUint16(ifdOff, little);
755
+ for (let i = 0; i < tags; i++) {
756
+ if (view.getUint16(ifdOff + 2 + 12 * i, little) === 0x0112) {
757
+ resolve(view.getUint16(ifdOff + 2 + 12 * i + 8, little));
758
+ return;
759
+ }
760
+ }
761
+ resolve(1);
762
+ return;
763
+ }
764
+ else if ((marker & 0xFF00) !== 0xFF00) {
765
+ break;
766
+ }
767
+ else {
768
+ offset += view.getUint16(offset, false);
769
+ }
770
+ }
771
+ resolve(1);
772
+ }
773
+ catch {
774
+ resolve(1);
775
+ }
776
+ };
777
+ reader.onerror = () => resolve(1);
778
+ // Only need the first 64KB — EXIF is always in the first APP1 segment
779
+ reader.readAsArrayBuffer(source.slice(0, 65536));
780
+ });
781
+ }
782
+ getDocumentLabel(type) {
783
+ const t = type.toLowerCase();
784
+ if (t.includes("passport"))
785
+ return "passport";
786
+ if (t.includes("driver") || t.includes("licen"))
787
+ return "driver's license";
788
+ if (t.includes("voter") || t.includes("pvc"))
789
+ return "voter's card";
790
+ if (t.includes("national") || t.includes("nin") || t.includes("national id"))
791
+ return "national ID card";
792
+ if (t.includes("bvn"))
793
+ return "BVN card";
794
+ return "identity document";
795
+ }
796
+ }
797
+ //# sourceMappingURL=DocumentCapture.js.map