@tn3w/openage 1.0.5 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tn3w/openage",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Privacy-first age verification widget. On-device face analysis with liveness detection.",
5
5
  "type": "module",
6
6
  "main": "dist/openage.umd.js",
package/src/challenge.js CHANGED
@@ -143,9 +143,18 @@ function resolveChallengeErrorMessage(error) {
143
143
  return 'Verification failed. Please try again.';
144
144
  }
145
145
 
146
+ function isInlineLayout(widget) {
147
+ return widget?.params?.layout === 'inline';
148
+ }
149
+
146
150
  async function showErrorStep(widget, error) {
147
151
  const message = resolveChallengeErrorMessage(error);
148
152
 
153
+ if (isInlineLayout(widget)) {
154
+ widget.showResult?.('retry', message);
155
+ return;
156
+ }
157
+
149
158
  if (!widget.popup) {
150
159
  widget.openPopup?.();
151
160
  }
@@ -198,10 +207,14 @@ async function runServerless(widget, emitter) {
198
207
  await Promise.all([loadVision(), initAgeEstimator()]);
199
208
 
200
209
  modelBuffer = await loadModel();
201
- widget.showReady();
202
210
 
203
- await waitForStart(widget);
204
- await startCameraFlow(widget, modelBuffer);
211
+ if (isInlineLayout(widget)) {
212
+ await startCameraFlow(widget, modelBuffer);
213
+ } else {
214
+ widget.showReady();
215
+ await waitForStart(widget);
216
+ await startCameraFlow(widget, modelBuffer);
217
+ }
205
218
 
206
219
  const transport = createTransport('serverless', params);
207
220
 
@@ -290,10 +303,16 @@ async function runServer(widget, emitter) {
290
303
  captureFrame: () => (captureFrame() ? 'true' : 'null'),
291
304
  });
292
305
 
293
- widget.showReady();
294
- await waitForStart(widget);
306
+ let video;
307
+
308
+ if (isInlineLayout(widget)) {
309
+ video = widget.showCamera();
310
+ } else {
311
+ widget.showReady();
312
+ await waitForStart(widget);
313
+ video = widget.showCamera();
314
+ }
295
315
 
296
- const video = widget.showCamera();
297
316
  widget.setVideoStatus('Requesting camera…');
298
317
  await startCamera(video);
299
318
  exposeMirrorVideo(video);
package/src/constants.js CHANGED
@@ -11,8 +11,7 @@ export const MEDIAPIPE_MODEL =
11
11
  'face_landmarker/face_landmarker/float16/1/' +
12
12
  'face_landmarker.task';
13
13
 
14
- export const FACEAPI_CDN =
15
- 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
14
+ export const FACEAPI_CDN = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
16
15
 
17
16
  export const FACEAPI_MODEL_CDN =
18
17
  'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights';
package/src/index.d.ts CHANGED
@@ -82,6 +82,7 @@ declare global {
82
82
  sitekey?: string;
83
83
  server?: string;
84
84
  mode?: "serverless" | "sitekey" | "custom";
85
+ layout?: "widget" | "inline" | "embed" | "embedded";
85
86
  locale?: string;
86
87
  theme?: "light" | "dark" | "auto";
87
88
  render?: "explicit";
package/src/index.js CHANGED
@@ -52,8 +52,19 @@ export class EventEmitter {
52
52
  const emitter = new EventEmitter();
53
53
  const widgets = new Map();
54
54
 
55
+ function normalizeLayout(layout) {
56
+ if (layout === 'inline' || layout === 'embed' || layout === 'embedded') {
57
+ return 'inline';
58
+ }
59
+
60
+ return 'widget';
61
+ }
62
+
55
63
  function normalizeParams(params) {
56
64
  const globalConfig = typeof window !== 'undefined' ? window.openage || {} : {};
65
+ const layout = normalizeLayout(
66
+ params.layout ?? params.display ?? globalConfig.layout ?? globalConfig.display
67
+ );
57
68
 
58
69
  return {
59
70
  mode: 'serverless',
@@ -62,6 +73,7 @@ function normalizeParams(params) {
62
73
  minAge: 18,
63
74
  ...globalConfig,
64
75
  ...params,
76
+ layout,
65
77
  };
66
78
  }
67
79
 
@@ -70,6 +82,10 @@ function startWidget(widget) {
70
82
  emitter.emit('opened', widget.id);
71
83
  runChallenge(widget, emitter);
72
84
  };
85
+
86
+ if (widget.params.layout === 'inline') {
87
+ widget.startChallenge();
88
+ }
73
89
  }
74
90
 
75
91
  function render(container, params = {}) {
@@ -218,6 +234,7 @@ function autoRender() {
218
234
  size: element.dataset.size,
219
235
  action: element.dataset.action,
220
236
  mode: element.dataset.mode,
237
+ layout: element.dataset.layout || element.dataset.display,
221
238
  server: element.dataset.server,
222
239
  };
223
240
 
package/src/ui.js CHANGED
@@ -541,6 +541,20 @@ export const STYLES = `
541
541
  flex-direction: column;
542
542
  }
543
543
 
544
+ .oa-inline-shell {
545
+ background: var(--oa-bg);
546
+ border: 1px solid var(--oa-border);
547
+ border-radius: var(--oa-radius);
548
+ box-shadow: 0 20px 60px rgba(0,0,0,0.18),
549
+ 0 0 0 1px rgba(0,0,0,0.06);
550
+ width: min(100%, 360px);
551
+ max-width: 100%;
552
+ overflow: hidden;
553
+ display: flex;
554
+ flex-direction: column;
555
+ margin: 0 auto;
556
+ }
557
+
544
558
  .oa-modal-overlay {
545
559
  position: fixed;
546
560
  inset: 0;
package/src/widget.d.ts CHANGED
@@ -2,6 +2,7 @@ export interface WidgetParams {
2
2
  sitekey?: string;
3
3
  server?: string;
4
4
  mode?: "serverless" | "sitekey" | "custom";
5
+ layout?: "widget" | "inline" | "embed" | "embedded";
5
6
  theme?: "light" | "dark" | "auto";
6
7
  size?: "compact" | "normal" | "invisible";
7
8
  action?: string;
package/src/widget.js CHANGED
@@ -27,6 +27,7 @@ export class Widget {
27
27
  this.popup = null;
28
28
  this.shadow = null;
29
29
  this.elements = {};
30
+ this.popupElements = null;
30
31
  this.onChallenge = null;
31
32
  this.onStartClick = null;
32
33
  this.popupFrame = 0;
@@ -39,6 +40,7 @@ export class Widget {
39
40
  const host = document.createElement('div');
40
41
  host.id = this.id;
41
42
  this.shadow = host.attachShadow({ mode: 'open' });
43
+ this.host = host;
42
44
 
43
45
  const style = document.createElement('style');
44
46
  style.textContent = STYLES;
@@ -51,7 +53,12 @@ export class Widget {
51
53
  if (this.params.size === 'invisible') {
52
54
  host.style.display = 'none';
53
55
  this.container.appendChild(host);
54
- this.host = host;
56
+ return;
57
+ }
58
+
59
+ if (this.isInlineLayout()) {
60
+ this.renderInlineShell();
61
+ this.container.appendChild(host);
55
62
  return;
56
63
  }
57
64
 
@@ -87,7 +94,32 @@ export class Widget {
87
94
  this.elements.errorSlot = this.shadow.querySelector('.oa-error-slot');
88
95
 
89
96
  this.container.appendChild(host);
90
- this.host = host;
97
+ }
98
+
99
+ isInlineLayout() {
100
+ return this.params.layout === 'inline';
101
+ }
102
+
103
+ renderInlineShell() {
104
+ const inlineShell = document.createElement('div');
105
+ inlineShell.className = 'oa-inline-shell';
106
+ inlineShell.innerHTML = this.buildPopupContent({ closeable: false });
107
+ this.shadow.appendChild(inlineShell);
108
+
109
+ this.popup = {
110
+ host: this.host,
111
+ root: inlineShell,
112
+ inline: true,
113
+ };
114
+
115
+ this.bindPopupEvents(inlineShell);
116
+ }
117
+
118
+ resetInlineShell() {
119
+ if (!this.popup?.root) return;
120
+
121
+ this.popup.root.innerHTML = this.buildPopupContent({ closeable: false });
122
+ this.bindPopupEvents(this.popup.root);
91
123
  }
92
124
 
93
125
  startChallenge() {
@@ -120,6 +152,14 @@ export class Widget {
120
152
  }
121
153
 
122
154
  openPopup() {
155
+ if (this.isInlineLayout()) {
156
+ if (!this.popup) {
157
+ this.renderInlineShell();
158
+ }
159
+
160
+ return this.getVideo();
161
+ }
162
+
123
163
  if (this.popup) return this.getVideo();
124
164
 
125
165
  const anchor = this.getPopupAnchor();
@@ -297,7 +337,7 @@ export class Widget {
297
337
  this.popup.root.style.pointerEvents = 'auto';
298
338
  }
299
339
 
300
- buildPopupContent() {
340
+ buildPopupContent({ closeable = true } = {}) {
301
341
  return `
302
342
  <div class="oa-header">
303
343
  <div class="oa-title">
@@ -308,10 +348,14 @@ export class Widget {
308
348
  </a>
309
349
  <span class="oa-badge">on-device</span>
310
350
  </div>
311
- <button class="oa-close-btn"
312
- aria-label="Close">
313
- ${CLOSE_SVG}
314
- </button>
351
+ ${
352
+ closeable
353
+ ? `<button class="oa-close-btn"
354
+ aria-label="Close">
355
+ ${CLOSE_SVG}
356
+ </button>`
357
+ : ''
358
+ }
315
359
  </div>
316
360
  <div class="oa-body">
317
361
  ${heroTemplate('Initializing…')}
@@ -324,7 +368,7 @@ export class Widget {
324
368
  `;
325
369
  }
326
370
 
327
- bindPopupEvents(root, shadow) {
371
+ bindPopupEvents(root) {
328
372
  const closeBtn = root.querySelector('.oa-close-btn');
329
373
  if (closeBtn) {
330
374
  closeBtn.addEventListener('click', () => {
@@ -442,6 +486,22 @@ export class Widget {
442
486
  }
443
487
 
444
488
  showResult(outcome, message) {
489
+ if (this.isInlineLayout()) {
490
+ if (!this.popupElements?.body) return;
491
+
492
+ this.popupElements.body.innerHTML = resultTemplate(outcome, message);
493
+ this.hideActions();
494
+
495
+ if (outcome === 'pass') {
496
+ this.setState('verified');
497
+ return;
498
+ }
499
+
500
+ this.setState(outcome === 'fail' ? 'failed' : 'retry');
501
+ this.showActions('Try Again');
502
+ return;
503
+ }
504
+
445
505
  if (outcome === 'pass') {
446
506
  this.closePopup();
447
507
  this.setState('verified');
@@ -481,8 +541,9 @@ export class Widget {
481
541
  if (!this.popupElements?.body) return;
482
542
 
483
543
  this.popupElements.body.innerHTML = errorStepTemplate(message);
484
- this.popupElements.errorCountdown =
485
- this.popupElements.body.querySelector('.oa-error-step-countdown');
544
+ this.popupElements.errorCountdown = this.popupElements.body.querySelector(
545
+ '.oa-error-step-countdown'
546
+ );
486
547
  this.hideActions();
487
548
  }
488
549
 
@@ -501,6 +562,22 @@ export class Widget {
501
562
 
502
563
  closePopup() {
503
564
  if (!this.popup) return;
565
+
566
+ if (this.popup.inline) {
567
+ this.popup.cleanup?.();
568
+ if (this.popupFrame) {
569
+ cancelAnimationFrame(this.popupFrame);
570
+ this.popupFrame = 0;
571
+ }
572
+ this.resetInlineShell();
573
+
574
+ if (this.state === 'loading') {
575
+ this.setState('idle');
576
+ }
577
+
578
+ return;
579
+ }
580
+
504
581
  this.popup.cleanup?.();
505
582
  this.popup.themeCleanup?.();
506
583
  this.popup.host.remove();