@smileid/web-components 1.0.0-beta

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 (73) hide show
  1. package/.eslintrc.cjs +72 -0
  2. package/components/README.md +14 -0
  3. package/components/attribution/PoweredBySmileId.js +42 -0
  4. package/components/camera-permission/CameraPermission.js +136 -0
  5. package/components/camera-permission/CameraPermission.stories.js +21 -0
  6. package/components/combobox/src/Combobox.js +586 -0
  7. package/components/combobox/src/index.js +1 -0
  8. package/components/document/src/DocumentCaptureScreens.js +317 -0
  9. package/components/document/src/DocumentCaptureScreens.stories.js +51 -0
  10. package/components/document/src/README.md +108 -0
  11. package/components/document/src/document-capture/DocumentCapture.js +677 -0
  12. package/components/document/src/document-capture/DocumentCapture.stories.js +71 -0
  13. package/components/document/src/document-capture/README.md +89 -0
  14. package/components/document/src/document-capture/index.js +3 -0
  15. package/components/document/src/document-capture-instructions/DocumentCaptureInstructions.js +499 -0
  16. package/components/document/src/document-capture-instructions/DocumentCaptureInstructions.stories.js +17 -0
  17. package/components/document/src/document-capture-instructions/README.md +56 -0
  18. package/components/document/src/document-capture-instructions/index.js +3 -0
  19. package/components/document/src/document-capture-review/DocumentCaptureReview.js +378 -0
  20. package/components/document/src/document-capture-review/DocumentCaptureReview.stories.js +14 -0
  21. package/components/document/src/document-capture-review/README.md +79 -0
  22. package/components/document/src/document-capture-review/index.js +3 -0
  23. package/components/document/src/index.js +3 -0
  24. package/components/end-user-consent/src/EndUserConsent.js +809 -0
  25. package/components/end-user-consent/src/EndUserConsent.stories.js +23 -0
  26. package/components/end-user-consent/src/index.js +4 -0
  27. package/components/navigation/src/Navigation.js +160 -0
  28. package/components/navigation/src/Navigation.stories.js +24 -0
  29. package/components/navigation/src/index.js +3 -0
  30. package/components/selfie/README.md +201 -0
  31. package/components/selfie/src/SelfieCaptureScreens.js +224 -0
  32. package/components/selfie/src/SelfieCaptureScreens.stories.js +21 -0
  33. package/components/selfie/src/index.js +5 -0
  34. package/components/selfie/src/selfie-capture/SelfieCapture.js +878 -0
  35. package/components/selfie/src/selfie-capture/SelfieCapture.stories.js +23 -0
  36. package/components/selfie/src/selfie-capture/index.js +3 -0
  37. package/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.js +638 -0
  38. package/components/selfie/src/selfie-capture-instructions/SelfieCaptureInstructions.stories.js +17 -0
  39. package/components/selfie/src/selfie-capture-instructions/index.js +3 -0
  40. package/components/selfie/src/selfie-capture-review/SelfieCaptureReview.js +348 -0
  41. package/components/selfie/src/selfie-capture-review/SelfieCaptureReview.stories.js +17 -0
  42. package/components/selfie/src/selfie-capture-review/index.js +3 -0
  43. package/components/signature-pad/package.json +30 -0
  44. package/components/signature-pad/src/SignaturePad.js +477 -0
  45. package/components/signature-pad/src/SignaturePad.stories.js +24 -0
  46. package/components/signature-pad/src/index.js +3 -0
  47. package/components/smart-camera-web/src/SmartCameraWeb.js +246 -0
  48. package/components/smart-camera-web/src/SmartCameraWeb.stories.js +35 -0
  49. package/components/totp-consent/src/TotpConsent.js +935 -0
  50. package/components/totp-consent/src/index.js +4 -0
  51. package/cypress/e2e/smart-camera-web-attribution.cy.js +21 -0
  52. package/cypress/e2e/smart-camera-web-back-press.cy.js +481 -0
  53. package/cypress/e2e/smart-camera-web-hide-instructions.cy.js +334 -0
  54. package/cypress/e2e/smart-camera-web.cy.js +309 -0
  55. package/cypress/fixtures/example.json +5 -0
  56. package/cypress/pages/capture-back-of-id-hide-attribution.html +44 -0
  57. package/cypress/pages/capture-back-of-id-navigation.html +72 -0
  58. package/cypress/pages/smart-camera-web-hide-instructions.html +38 -0
  59. package/cypress/pages/smart-camera-web.html +38 -0
  60. package/cypress/support/commands.js +144 -0
  61. package/cypress/support/e2e.js +20 -0
  62. package/cypress.config.js +11 -0
  63. package/domain/camera/src/README.md +38 -0
  64. package/domain/camera/src/SmartCamera.js +81 -0
  65. package/domain/constants/src/Constants.js +27 -0
  66. package/domain/file-upload/README.md +35 -0
  67. package/domain/file-upload/src/SmartFileUpload.js +65 -0
  68. package/esbuild.js +119 -0
  69. package/index.js +5 -0
  70. package/package.json +46 -0
  71. package/styles/README.md +3 -0
  72. package/styles/src/styles.js +348 -0
  73. package/styles/src/typography.js +52 -0
@@ -0,0 +1,878 @@
1
+ import { IMAGE_TYPE } from '../../../../domain/constants/src/Constants';
2
+ import SmartCamera from '../../../../domain/camera/src/SmartCamera';
3
+ import styles from '../../../../styles/src/styles';
4
+ import { version as COMPONENTS_VERSION } from '../../../../package.json';
5
+ import '../../../navigation/src';
6
+
7
+ const DEFAULT_NO_OF_LIVENESS_FRAMES = 8;
8
+
9
+ function hasMoreThanNColors(data, n = 16) {
10
+ const colors = new Set();
11
+ for (let i = 0; i < Math.min(data.length, 10000); i += 4) {
12
+ // eslint-disable-next-line no-bitwise
13
+ colors.add((data[i] << 16) | (data[i + 1] << 8) | data[i + 2]);
14
+ if (colors.size > n) {
15
+ return true;
16
+ }
17
+ }
18
+ return false;
19
+ }
20
+
21
+ function getLivenessFramesIndices(
22
+ totalNoOfFrames,
23
+ numberOfFramesRequired = DEFAULT_NO_OF_LIVENESS_FRAMES,
24
+ ) {
25
+ const selectedFrames = [];
26
+
27
+ if (totalNoOfFrames < numberOfFramesRequired) {
28
+ throw new Error(
29
+ 'SmartCameraWeb: Minimum required no of frames is ',
30
+ numberOfFramesRequired,
31
+ );
32
+ }
33
+
34
+ const frameDivisor = numberOfFramesRequired - 1;
35
+ const frameInterval = Math.floor(totalNoOfFrames / frameDivisor);
36
+
37
+ // NOTE: when we have satisfied our required 8 frames, but have good
38
+ // candidates, we need to start replacing from the second frame
39
+ let replacementFrameIndex = 1;
40
+
41
+ for (let i = 0; i < totalNoOfFrames; i += frameInterval) {
42
+ if (selectedFrames.length < 8) {
43
+ selectedFrames.push(i);
44
+ } else {
45
+ // ACTION: replace frame, then sort selectedframes
46
+ selectedFrames[replacementFrameIndex] = i;
47
+ selectedFrames.sort((a, b) => a - b);
48
+
49
+ // ACTION: update replacement frame index
50
+ replacementFrameIndex += 1;
51
+ }
52
+ }
53
+
54
+ // INFO: if we don't satisfy our requirement, we add the last index
55
+ const lastFrameIndex = totalNoOfFrames - 1;
56
+
57
+ if (selectedFrames.length < 8 && !selectedFrames.includes(lastFrameIndex)) {
58
+ selectedFrames.push(lastFrameIndex);
59
+ }
60
+
61
+ return selectedFrames;
62
+ }
63
+
64
+ function templateString() {
65
+ return `
66
+ ${styles}
67
+ <style>
68
+ :host {
69
+ --color-active: #2D2B2A;
70
+ --color-default: #001096;
71
+ --color-disabled: #848282;
72
+ }
73
+
74
+ * {
75
+ font-family: 'DM Sans', sans-serif;
76
+ }
77
+
78
+ [hidden] {
79
+ display: none !important;
80
+ }
81
+
82
+ [disabled] {
83
+ cursor: not-allowed !important;
84
+ filter: grayscale(75%);
85
+ }
86
+
87
+ .visually-hidden {
88
+ border: 0;
89
+ clip: rect(1px 1px 1px 1px);
90
+ clip: rect(1px, 1px, 1px, 1px);
91
+ height: auto;
92
+ margin: 0;
93
+ overflow: hidden;
94
+ padding: 0;
95
+ position: absolute;
96
+ white-space: nowrap;
97
+ width: 1px;
98
+ }
99
+
100
+ img {
101
+ height: auto;
102
+ max-width: 100%;
103
+ transform: scaleX(-1);
104
+ }
105
+
106
+ video {
107
+ background-color: black;
108
+ }
109
+
110
+ a {
111
+ color: currentColor;
112
+ text-decoration: none;
113
+ }
114
+
115
+ svg {
116
+ max-width: 100%;
117
+ }
118
+
119
+ .color-gray {
120
+ color: #797979;
121
+ }
122
+
123
+ .color-red {
124
+ color: red;
125
+ }
126
+
127
+ .color-richblue {
128
+ color: #4E6577;
129
+ }
130
+
131
+ .color-richblue-shade {
132
+ color: #0E1B42;
133
+ }
134
+
135
+ .color-digital-blue {
136
+ color: #001096 !important;
137
+ }
138
+
139
+ .color-deep-blue {
140
+ color: #001096;
141
+ }
142
+
143
+ .center {
144
+ text-align: center;
145
+ margin-left: auto;
146
+ margin-right: auto;
147
+ }
148
+
149
+ .font-size-small {
150
+ font-size: .75rem;
151
+ }
152
+
153
+ .font-size-large {
154
+ font-size: 1.5rem;
155
+ }
156
+
157
+ .text-transform-uppercase {
158
+ text-transform: uppercase;
159
+ }
160
+
161
+ [id*=-"screen"] {
162
+ min-block-size: 100%;
163
+ }
164
+
165
+ [data-variant~="full-width"] {
166
+ inline-size: 100%;
167
+ }
168
+
169
+ .flow > * + * {
170
+ margin-top: 1rem;
171
+ }
172
+
173
+ .button {
174
+ --button-color: var(--color-default);
175
+ -webkit-appearance: none;
176
+ appearance: none;
177
+ border-radius: 2.5rem;
178
+ border: 0;
179
+ background-color: transparent;
180
+ color: #fff;
181
+ cursor: pointer;
182
+ display: block;
183
+ font-size: 18px;
184
+ font-weight: 600;
185
+ padding: .75rem 1.5rem;
186
+ text-align: center;
187
+ }
188
+
189
+ .button:hover,
190
+ .button:focus,
191
+ .button:active {
192
+ --button-color: var(--color-active);
193
+ }
194
+
195
+ .button:disabled {
196
+ --button-color: var(--color-disabled);
197
+ }
198
+
199
+ .button[data-variant~='solid'] {
200
+ background-color: var(--button-color);
201
+ border: 2px solid var(--button-color);
202
+ }
203
+
204
+ .button[data-variant~='outline'] {
205
+ color: var(--button-color);
206
+ border: 2px solid var(--button-color);
207
+ }
208
+
209
+ .button[data-variant~='ghost'] {
210
+ padding: 0px;
211
+ color: var(--button-color);
212
+ background-color: transparent;
213
+ }
214
+
215
+ .icon-btn {
216
+ appearance: none;
217
+ background: none;
218
+ border: none;
219
+ color: hsl(0deg 0% 94%);
220
+ cursor: pointer;
221
+ display: flex;
222
+ align-items: center;
223
+ justify-content: center;
224
+ padding: 4px 8px;
225
+ }
226
+ .justify-right {
227
+ justify-content: end !important;
228
+ }
229
+ .nav {
230
+ display: flex;
231
+ justify-content: space-between;
232
+ }
233
+
234
+ .back-wrapper {
235
+ display: flex;
236
+ align-items: center;
237
+ }
238
+
239
+ .back-button {
240
+ display: block !important;
241
+ }
242
+ .back-button-text {
243
+ font-size: 11px;
244
+ line-height: 11px;
245
+ color: rgb(21, 31, 114);
246
+ }
247
+ .section {
248
+ border-radius: .5rem;
249
+ margin-left: auto;
250
+ margin-right: auto;
251
+ max-width: 35ch;
252
+ padding: 1rem;
253
+ }
254
+
255
+ .selfie-capture-review-image {
256
+ overflow: hidden;
257
+ aspect-ratio: 1/1;
258
+ }
259
+
260
+ #review-image {
261
+ scale: 1.75;
262
+ }
263
+
264
+ @media (max-aspect-ratio: 1/1) {
265
+ #review-image {
266
+ transform: scaleX(-1) translateY(-10%);
267
+ }
268
+ }
269
+
270
+ .tips,
271
+ .powered-by {
272
+ align-items: center;
273
+ border-radius: .25rem;
274
+ color: #4E6577;
275
+ display: flex;
276
+ justify-content: center;
277
+ letter-spacing: .075em;
278
+ }
279
+
280
+ .powered-by {
281
+ box-shadow: 0px 2.57415px 2.57415px rgba(0, 0, 0, 0.06);
282
+ display: inline-flex;
283
+ font-size: .5rem;
284
+ }
285
+
286
+ .tips {
287
+ margin-left: auto;
288
+ margin-right: auto;
289
+ max-width: 17rem;
290
+ }
291
+
292
+ .tips > * + *,
293
+ .powered-by > * + * {
294
+ display: inline-block;
295
+ margin-left: .5em;
296
+ }
297
+
298
+ .powered-by .company {
299
+ color: #18406D;
300
+ font-weight: 700;
301
+ letter-spacing: .15rem;
302
+ }
303
+
304
+ .logo-mark {
305
+ background-color: #004071;
306
+ display: inline-block;
307
+ padding: .25em .5em;
308
+ }
309
+
310
+ .logo-mark svg {
311
+ height: auto;
312
+ justify-self: center;
313
+ width: .75em;
314
+ }
315
+
316
+ @keyframes fadeInOut {
317
+ 12.5% {
318
+ opacity: 0;
319
+ }
320
+
321
+ 50% {
322
+ opacity: 1;
323
+ }
324
+
325
+ 87.5% {
326
+ opacity: 0;
327
+ }
328
+ }
329
+
330
+ .id-video-container.portrait {
331
+ width: 100%;
332
+ position: relative;
333
+ height: calc(200px * 1.4);
334
+ }
335
+
336
+ .id-video-container.portrait video {
337
+ width: calc(213px + 0.9rem);
338
+ height: 100%;
339
+ position: absolute;
340
+ top: 239px;
341
+ left: 161px;
342
+ padding-bottom: calc((214px * 1.4) / 3);
343
+ padding-top: calc((191px * 1.4) / 3);
344
+ object-fit: cover;
345
+
346
+ transform: translateX(-50%) translateY(-50%);
347
+ z-index: 1;
348
+ block-size: 100%;
349
+ }
350
+
351
+ .video-container,
352
+ .id-video-container.landscape {
353
+ position: relative;
354
+ z-index: 1;
355
+ width: 100%;
356
+ }
357
+
358
+ .video-container #smile-cta,
359
+ .video-container video,
360
+ .id-video-container.landscape video {
361
+ left: 50%;
362
+ min-width: auto;
363
+ position: absolute;
364
+ top: calc(50% - 3px);
365
+ transform: translateX(-50%) translateY(50%);
366
+ }
367
+
368
+ .video-container #smile-cta {
369
+ color: white;
370
+ font-size: 2rem;
371
+ font-weight: bold;
372
+ opacity: 0;
373
+ top: calc(50% - 3rem);
374
+ }
375
+
376
+ .video-container video {
377
+ min-height: 100%;
378
+ transform: scaleX(-1) translateX(50%) translateY(-50%);
379
+ }
380
+
381
+ .video-container .video {
382
+ background-color: black;
383
+ position: absolute;
384
+ left: 50%;
385
+ height: calc(100% - 6px);
386
+ clip-path: ellipse(101px 118px);
387
+ }
388
+
389
+ .id-video-container.landscape {
390
+ min-height: calc((2 * 10rem) + 198px);
391
+ height: auto;
392
+ }
393
+
394
+ .id-video-container.portrait .image-frame-portrait {
395
+ border-width: 0.9rem;
396
+ border-color: rgba(0, 0, 0, 0.7);
397
+ border-style: solid;
398
+ height: auto;
399
+ position: absolute;
400
+ top: 80px;
401
+ left: 47px;
402
+ z-index: 2;
403
+ width: 200px;
404
+ height: calc(200px * 1.4);
405
+ }
406
+
407
+ .id-video-container.landscape .image-frame {
408
+ border-width: 10rem 1rem;
409
+ border-color: rgba(0, 0, 0, 0.7);
410
+ border-style: solid;
411
+ height: auto;
412
+ width: 90%;
413
+ position: absolute;
414
+ top: 0;
415
+ left: 0;
416
+ z-index: 2;
417
+ }
418
+
419
+ .id-video-container.landscape video {
420
+ width: 100%;
421
+ transform: translateX(-50%) translateY(-50%);
422
+ z-index: 1;
423
+ height: 100%;
424
+ block-size: 100%;
425
+ }
426
+
427
+ .id-video-container.landscape img {
428
+ position: absolute;
429
+ top: 50%;
430
+ left: 50%;
431
+ transform: translateX(-50%) translateY(-50%);
432
+ max-width: 90%;
433
+ }
434
+
435
+ .actions {
436
+ background-color: rgba(0, 0, 0, .7);
437
+ bottom: 0;
438
+ display: flex;
439
+ justify-content: space-between;
440
+ padding: 1rem;
441
+ position: absolute;
442
+ width: 90%;
443
+ z-index: 2;
444
+ }
445
+
446
+ #back-of-id-camera-screen .id-video-container.portrait .actions,
447
+ #id-camera-screen .id-video-container.portrait .actions {
448
+ top: 145%;
449
+ width: calc(200px * 1.4);
450
+ }
451
+
452
+ #back-of-id-camera-screen .section.portrait, #id-camera-screen .section.portrait {
453
+ min-height: calc((200px * 1.4) + 260px);
454
+ }
455
+
456
+ #selfie-capture-screen,
457
+ #back-of-id-entry-screen {
458
+ block-size: 45rem;
459
+ padding-block: 2rem;
460
+ display: flex;
461
+ flex-direction: column;
462
+ max-block-size: 100%;
463
+ max-inline-size: 40ch;
464
+ }
465
+
466
+ #selfie-capture-screen header p {
467
+ margin-block: 0 !important;
468
+ }
469
+
470
+ .document-tips {
471
+ margin-block-start: 1.5rem;
472
+ display: flex;
473
+ align-items: center;
474
+ text-align: initial;
475
+ }
476
+
477
+ .document-tips svg {
478
+ flex-shrink: 0;
479
+ margin-inline-end: 1rem;
480
+ }
481
+
482
+ .document-tips p {
483
+ margin-block: 0;
484
+ }
485
+
486
+ .document-tips p:first-of-type {
487
+ font-size; 1.875rem;
488
+ font-weight: bold
489
+ }
490
+
491
+ [type='file'] {
492
+ display: none;
493
+ }
494
+
495
+ .document-tips > * + * {
496
+ margin-inline-start; 1em;
497
+ }
498
+ </style>
499
+ <div id='selfie-capture-screen' class='flow center'>
500
+ <smileid-navigation ${this.showNavigation ? 'show-navigation' : ''} ${this.hideBack ? 'hide-back' : ''}></smileid-navigation>
501
+ <h1 class='text-2xl color-digital-blue font-bold'>Take a Selfie</h1>
502
+
503
+ <div class='section | flow'>
504
+ <div class='video-container'>
505
+ <div class='video'>
506
+ </div>
507
+ <svg id="image-outline" width="215" height="245" viewBox="0 0 215 245" fill="none" xmlns="http://www.w3.org/2000/svg">
508
+ <path d="M210.981 122.838C210.981 188.699 164.248 241.268 107.55 241.268C50.853 241.268 4.12018 188.699 4.12018 122.838C4.12018 56.9763 50.853 4.40771 107.55 4.40771C164.248 4.40771 210.981 56.9763 210.981 122.838Z" stroke="var(--color-default)" stroke-width="7.13965"/>
509
+ </svg>
510
+ <p id='smile-cta' class='color-gray'>SMILE</p>
511
+ </div>
512
+
513
+ <small class='tips'>
514
+ <svg width='44' xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 40 40">
515
+ <path fill="#F8F8FA" fill-rule="evenodd" d="M17.44 0h4.2c4.92 0 7.56.68 9.95 1.96a13.32 13.32 0 015.54 5.54c1.27 2.39 1.95 5.02 1.95 9.94v4.2c0 4.92-.68 7.56-1.95 9.95a13.32 13.32 0 01-5.54 5.54c-2.4 1.27-5.03 1.95-9.95 1.95h-4.2c-4.92 0-7.55-.68-9.94-1.95a13.32 13.32 0 01-5.54-5.54C.68 29.19 0 26.56 0 21.64v-4.2C0 12.52.68 9.9 1.96 7.5A13.32 13.32 0 017.5 1.96C9.89.68 12.52 0 17.44 0z" clip-rule="evenodd"/>
516
+ <path fill="#AEB6CB" d="M19.95 10.58a.71.71 0 000 1.43.71.71 0 000-1.43zm-5.54 2.3a.71.71 0 000 1.43.71.71 0 000-1.43zm11.08 0a.71.71 0 000 1.43.71.71 0 000-1.43zm-5.63 1.27a4.98 4.98 0 00-2.05 9.48v1.2a2.14 2.14 0 004.28 0v-1.2a4.99 4.99 0 00-2.23-9.48zm-7.75 4.27a.71.71 0 000 1.43.71.71 0 000-1.43zm15.68 0a.71.71 0 000 1.43.71.71 0 000-1.43z"/>
517
+ </svg>
518
+ <span>Tips: Put your face inside the oval frame and click to "take selfie"</span> </small>
519
+
520
+ <button data-variant='solid' id='start-image-capture' class='button | center' type='button'>
521
+ Take Selfie
522
+ </button>
523
+
524
+ ${
525
+ this.hideAttribution
526
+ ? ''
527
+ : `
528
+ <powered-by-smile-id></powered-by-smile-id>
529
+ `
530
+ }
531
+ </div>
532
+ </div>
533
+ `;
534
+ }
535
+
536
+ async function getPermissions(captureScreen) {
537
+ try {
538
+ await SmartCamera.getMedia({
539
+ audio: false,
540
+ video: true,
541
+ });
542
+ captureScreen?.removeAttribute('data-camera-error');
543
+ captureScreen?.setAttribute('data-camera-ready', true);
544
+ } catch (error) {
545
+ captureScreen?.removeAttribute('data-camera-ready');
546
+ captureScreen?.setAttribute(
547
+ 'data-camera-error',
548
+ SmartCamera.handleCameraError(error),
549
+ );
550
+ }
551
+ }
552
+
553
+ class SelfieCaptureScreen extends HTMLElement {
554
+ constructor() {
555
+ super();
556
+ this.templateString = templateString.bind(this);
557
+ this.render = () => this.templateString();
558
+
559
+ this.attachShadow({ mode: 'open' });
560
+ }
561
+
562
+ connectedCallback() {
563
+ const template = document.createElement('template');
564
+ template.innerHTML = this.render();
565
+
566
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
567
+ this.videoContainer = this.shadowRoot.querySelector(
568
+ '.video-container > .video',
569
+ );
570
+ this.init();
571
+ }
572
+
573
+ init() {
574
+ this._videoStreamDurationInMS = 7800;
575
+ this._imageCaptureIntervalInMS = 200;
576
+
577
+ this._data = {
578
+ images: [],
579
+ meta: {
580
+ libraryVersion: COMPONENTS_VERSION,
581
+ },
582
+ };
583
+ this._rawImages = [];
584
+
585
+ this.setUpEventListeners();
586
+ }
587
+
588
+ reset() {
589
+ this.disconnectedCallback();
590
+ this.connectedCallback();
591
+ }
592
+
593
+ _startImageCapture() {
594
+ this.startImageCapture.disabled = true;
595
+
596
+ /**
597
+ * this was culled from https://jakearchibald.com/2013/animated-line-drawing-svg/
598
+ */
599
+ // NOTE: initialise image outline
600
+ const imageOutlineLength = this.imageOutline.getTotalLength();
601
+ // Clear any previous transition
602
+ this.imageOutline.style.transition = 'none';
603
+ // Set up the starting positions
604
+ this.imageOutline.style.strokeDasharray = `${imageOutlineLength} ${imageOutlineLength}`;
605
+ this.imageOutline.style.strokeDashoffset = imageOutlineLength;
606
+ // Trigger a layout so styles are calculated & the browser
607
+ // picks up the starting position before animating
608
+ this.imageOutline.getBoundingClientRect();
609
+ // Define our transition
610
+ this.imageOutline.style.transition = `stroke-dashoffset ${this._videoStreamDurationInMS / 1000}s ease-in-out`;
611
+ // Go!
612
+ this.imageOutline.style.strokeDashoffset = '0';
613
+
614
+ this.smileCTA.style.animation = `fadeInOut ease ${this._videoStreamDurationInMS / 1000}s`;
615
+
616
+ this._imageCaptureInterval = setInterval(() => {
617
+ this._capturePOLPhoto();
618
+ }, this._imageCaptureIntervalInMS);
619
+
620
+ this._videoStreamTimeout = setTimeout(() => {
621
+ this._stopVideoStream();
622
+ }, this._videoStreamDurationInMS);
623
+ }
624
+
625
+ _stopVideoStream() {
626
+ try {
627
+ clearTimeout(this._videoStreamTimeout);
628
+ clearInterval(this._imageCaptureInterval);
629
+ clearInterval(this._drawingInterval);
630
+ this.smileCTA.style.animation = 'none';
631
+
632
+ this._capturePOLPhoto(); // NOTE: capture the last photo
633
+ this._captureReferencePhoto();
634
+ SmartCamera.stopMedia();
635
+
636
+ const totalNoOfFrames = this._rawImages.length;
637
+ this._data.referenceImage = this._referenceImage;
638
+
639
+ const livenessFramesIndices = getLivenessFramesIndices(totalNoOfFrames);
640
+
641
+ this._data.images = this._data.images.concat(
642
+ livenessFramesIndices.map((imageIndex) => ({
643
+ image: this._rawImages[imageIndex].split(',')[1],
644
+ image_type_id: IMAGE_TYPE.LIVENESS_IMAGE_BASE64,
645
+ })),
646
+ );
647
+
648
+ this._publishImages();
649
+ } catch (error) {
650
+ console.error(error);
651
+ // Todo: handle error
652
+ }
653
+ }
654
+
655
+ _capturePOLPhoto() {
656
+ const canvas = document.createElement('canvas');
657
+ canvas.width = 240;
658
+ canvas.height =
659
+ (canvas.width * this._video.videoHeight) / this._video.videoWidth;
660
+
661
+ // NOTE: we do not want to test POL images
662
+ this._drawImage(canvas, false);
663
+
664
+ this._rawImages.push(canvas.toDataURL('image/jpeg'));
665
+ }
666
+
667
+ _captureReferencePhoto() {
668
+ const canvas = document.createElement('canvas');
669
+ canvas.width = 480;
670
+ canvas.height =
671
+ (canvas.width * this._video.videoHeight) / this._video.videoWidth;
672
+
673
+ // NOTE: we want to test the image quality of the reference photo
674
+ this._drawImage(canvas, !this.disableImageTests);
675
+
676
+ const image = canvas.toDataURL('image/jpeg');
677
+
678
+ this._referenceImage = image;
679
+
680
+ this._data.images.push({
681
+ image: image.split(',')[1],
682
+ image_type_id: IMAGE_TYPE.SELFIE_IMAGE_BASE64,
683
+ });
684
+ }
685
+
686
+ _publishImages() {
687
+ this.dispatchEvent(
688
+ new CustomEvent('selfie-capture.publish', {
689
+ detail: this._data,
690
+ }),
691
+ );
692
+ }
693
+
694
+ resetErrorMessage() {
695
+ this.errorMessage.textContent = '';
696
+ }
697
+
698
+ _drawImage(canvas, enableImageTests = true, video = this._video) {
699
+ // this.resetErrorMessage();
700
+ const context = canvas.getContext('2d');
701
+
702
+ context.drawImage(
703
+ video,
704
+ 0,
705
+ 0,
706
+ video.videoWidth,
707
+ video.videoHeight,
708
+ 0,
709
+ 0,
710
+ canvas.width,
711
+ canvas.height,
712
+ );
713
+
714
+ if (enableImageTests) {
715
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
716
+
717
+ const hasEnoughColors = hasMoreThanNColors(imageData.data);
718
+
719
+ if (hasEnoughColors) {
720
+ return context;
721
+ }
722
+ throw new Error(
723
+ 'Unable to capture webcam images - Please try another device',
724
+ );
725
+ } else {
726
+ return context;
727
+ }
728
+ }
729
+
730
+ handleStream(stream) {
731
+ const videoExists = this.shadowRoot.querySelector('video');
732
+ let video = null;
733
+ if (videoExists) {
734
+ video = this.shadowRoot.querySelector('video');
735
+ } else {
736
+ video = document.createElement('video');
737
+ }
738
+
739
+ video.autoplay = true;
740
+ video.playsInline = true;
741
+ video.muted = true;
742
+
743
+ if ('srcObject' in video) {
744
+ video.srcObject = stream;
745
+ } else {
746
+ video.src = window.URL.createObjectURL(stream);
747
+ }
748
+
749
+ video.onloadedmetadata = () => {
750
+ video.play();
751
+ };
752
+ this._video = video;
753
+ const videoContainer = this.shadowRoot.querySelector(
754
+ '.video-container > .video',
755
+ );
756
+ this._data.permissionGranted = true;
757
+
758
+ video.onloadedmetadata = () => {
759
+ // this.shadowRoot.querySelector('.actions').hidden = false;
760
+ // this.shadowRoot.querySelector('#loader').hidden = true;
761
+ // this.shadowRoot.querySelector('.video-section').hidden = false;
762
+ };
763
+
764
+ if (!videoExists) {
765
+ videoContainer.prepend(video);
766
+ }
767
+ }
768
+
769
+ setUpEventListeners() {
770
+ this.navigation = this.shadowRoot.querySelector('smileid-navigation');
771
+
772
+ this.startImageCapture = this.shadowRoot.querySelector(
773
+ '#start-image-capture',
774
+ );
775
+ this.imageOutline = this.shadowRoot.querySelector('#image-outline path');
776
+ this.smileCTA = this.shadowRoot.querySelector('#smile-cta');
777
+
778
+ this.startImageCapture.addEventListener('click', () => {
779
+ this._startImageCapture();
780
+ });
781
+
782
+ this.navigation.addEventListener('navigation.back', () => {
783
+ this.handleBackEvents();
784
+ });
785
+
786
+ this.navigation.addEventListener('navigation.close', () => {
787
+ this.closeWindow();
788
+ });
789
+
790
+ if (SmartCamera.stream) {
791
+ this.handleStream(SmartCamera.stream);
792
+ } else if (this.hasAttribute('data-camera-ready')) {
793
+ getPermissions(this);
794
+ }
795
+ }
796
+
797
+ disconnectedCallback() {
798
+ SmartCamera.stopMedia();
799
+ clearTimeout(this._videoStreamTimeout);
800
+ }
801
+
802
+ get hideBack() {
803
+ return this.hasAttribute('hide-back');
804
+ }
805
+
806
+ get showNavigation() {
807
+ return this.hasAttribute('show-navigation');
808
+ }
809
+
810
+ get themeColor() {
811
+ return this.getAttribute('theme-color') || '#043C93';
812
+ }
813
+
814
+ get hideAttribution() {
815
+ return this.hasAttribute('hide-attribution');
816
+ }
817
+
818
+ get supportBothCaptureModes() {
819
+ const value = this.documentCaptureModes;
820
+ return value.includes('camera') && value.includes('upload');
821
+ }
822
+
823
+ get title() {
824
+ return this.getAttribute('title') || 'Submit Front of ID';
825
+ }
826
+
827
+ get hidden() {
828
+ return this.getAttribute('hidden');
829
+ }
830
+
831
+ get cameraError() {
832
+ return this.getAttribute('data-camera-error');
833
+ }
834
+
835
+ get disableImageTests() {
836
+ return this.hasAttribute('disable-image-tests');
837
+ }
838
+
839
+ static get observedAttributes() {
840
+ return [
841
+ 'data-camera-error',
842
+ 'data-camera-ready',
843
+ 'hidden',
844
+ 'hide-back-to-host',
845
+ 'show-navigation',
846
+ 'title',
847
+ ];
848
+ }
849
+
850
+ attributeChangedCallback(name) {
851
+ switch (name) {
852
+ case 'data-camera-error':
853
+ case 'data-camera-ready':
854
+ case 'hidden':
855
+ case 'title':
856
+ this.shadowRoot.innerHTML = this.render();
857
+ this.init();
858
+ break;
859
+ default:
860
+ break;
861
+ }
862
+ }
863
+
864
+ handleBackEvents() {
865
+ SmartCamera.stopMedia();
866
+ this.dispatchEvent(new CustomEvent('selfie-capture.cancelled'));
867
+ }
868
+
869
+ closeWindow() {
870
+ this.dispatchEvent(new CustomEvent('selfie-capture.close'));
871
+ }
872
+ }
873
+
874
+ if ('customElements' in window && !customElements.get('selfie-capture')) {
875
+ window.customElements.define('selfie-capture', SelfieCaptureScreen);
876
+ }
877
+
878
+ export default SelfieCaptureScreen;