@tn3w/openage 1.0.4 → 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.
@@ -547,6 +547,20 @@
547
547
  flex-direction: column;
548
548
  }
549
549
 
550
+ .oa-inline-shell {
551
+ background: var(--oa-bg);
552
+ border: 1px solid var(--oa-border);
553
+ border-radius: var(--oa-radius);
554
+ box-shadow: 0 20px 60px rgba(0,0,0,0.18),
555
+ 0 0 0 1px rgba(0,0,0,0.06);
556
+ width: min(100%, 360px);
557
+ max-width: 100%;
558
+ overflow: hidden;
559
+ display: flex;
560
+ flex-direction: column;
561
+ margin: 0 auto;
562
+ }
563
+
550
564
  .oa-modal-overlay {
551
565
  position: fixed;
552
566
  inset: 0;
@@ -899,6 +913,52 @@
899
913
  .oa-result-fail { color: var(--oa-danger); }
900
914
  .oa-result-retry { color: var(--oa-warn); }
901
915
 
916
+ .oa-error-step {
917
+ display: flex;
918
+ flex-direction: column;
919
+ align-items: center;
920
+ gap: 0.7rem;
921
+ padding: 2rem 1.2rem;
922
+ background: var(--oa-surface);
923
+ border: 1px solid var(--oa-border);
924
+ border-radius: var(--oa-radius);
925
+ margin: 10px;
926
+ animation: oa-fade-in 0.4s ease;
927
+ text-align: center;
928
+ }
929
+
930
+ .oa-error-step-icon {
931
+ width: 3rem;
932
+ height: 3rem;
933
+ display: flex;
934
+ align-items: center;
935
+ justify-content: center;
936
+ border-radius: 999px;
937
+ background: rgba(239, 68, 68, 0.12);
938
+ color: var(--oa-danger);
939
+ font-size: 1.7rem;
940
+ line-height: 1;
941
+ }
942
+
943
+ .oa-error-step-title {
944
+ font-size: 1rem;
945
+ font-weight: 700;
946
+ color: var(--oa-text);
947
+ }
948
+
949
+ .oa-error-step-message {
950
+ font-size: 0.84rem;
951
+ line-height: 1.5;
952
+ color: var(--oa-text-muted);
953
+ max-width: 260px;
954
+ }
955
+
956
+ .oa-error-step-countdown {
957
+ font-size: 0.75rem;
958
+ font-weight: 600;
959
+ color: var(--oa-danger);
960
+ }
961
+
902
962
  .oa-hidden { display: none !important; }
903
963
 
904
964
  /* ── Animations ──────────────────────────────── */
@@ -1025,6 +1085,17 @@
1025
1085
  `;
1026
1086
  }
1027
1087
 
1088
+ function errorStepTemplate(message) {
1089
+ return `
1090
+ <div class="oa-error-step">
1091
+ <div class="oa-error-step-icon">✕</div>
1092
+ <div class="oa-error-step-title">Verification stopped</div>
1093
+ <div class="oa-error-step-message">${message}</div>
1094
+ <div class="oa-error-step-countdown"></div>
1095
+ </div>
1096
+ `;
1097
+ }
1098
+
1028
1099
  function resultTemplate(outcome, message) {
1029
1100
  const icons = {
1030
1101
  fail: '✕',
@@ -1046,7 +1117,7 @@
1046
1117
 
1047
1118
  const VERSION = '1.0.0';
1048
1119
 
1049
- const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/' + '@mediapipe/tasks-vision@0.10.17';
1120
+ const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.17';
1050
1121
 
1051
1122
  const MEDIAPIPE_WASM = `${MEDIAPIPE_CDN}/wasm`;
1052
1123
 
@@ -1057,13 +1128,13 @@
1057
1128
  'face_landmarker/face_landmarker/float16/1/' +
1058
1129
  'face_landmarker.task';
1059
1130
 
1060
- const FACEAPI_CDN =
1061
- 'https://cdn.jsdelivr.net/npm/' + 'face-api.js@0.22.2/dist/face-api.min.js';
1131
+ const FACEAPI_CDN = 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
1062
1132
 
1063
1133
  const FACEAPI_MODEL_CDN =
1064
- 'https://cdn.jsdelivr.net/gh/' + 'justadudewhohacks/face-api.js@master/weights';
1134
+ 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights';
1065
1135
 
1066
1136
  const MAX_RETRIES = 3;
1137
+ const ERROR_STEP_SECONDS = 5;
1067
1138
  const BURST_FRAMES = 5;
1068
1139
  const BURST_INTERVAL_MS = 200;
1069
1140
  const POSITION_CHECK_MS = 100;
@@ -1094,6 +1165,7 @@
1094
1165
  this.popup = null;
1095
1166
  this.shadow = null;
1096
1167
  this.elements = {};
1168
+ this.popupElements = null;
1097
1169
  this.onChallenge = null;
1098
1170
  this.onStartClick = null;
1099
1171
  this.popupFrame = 0;
@@ -1106,6 +1178,7 @@
1106
1178
  const host = document.createElement('div');
1107
1179
  host.id = this.id;
1108
1180
  this.shadow = host.attachShadow({ mode: 'open' });
1181
+ this.host = host;
1109
1182
 
1110
1183
  const style = document.createElement('style');
1111
1184
  style.textContent = STYLES;
@@ -1118,7 +1191,12 @@
1118
1191
  if (this.params.size === 'invisible') {
1119
1192
  host.style.display = 'none';
1120
1193
  this.container.appendChild(host);
1121
- this.host = host;
1194
+ return;
1195
+ }
1196
+
1197
+ if (this.isInlineLayout()) {
1198
+ this.renderInlineShell();
1199
+ this.container.appendChild(host);
1122
1200
  return;
1123
1201
  }
1124
1202
 
@@ -1154,7 +1232,32 @@
1154
1232
  this.elements.errorSlot = this.shadow.querySelector('.oa-error-slot');
1155
1233
 
1156
1234
  this.container.appendChild(host);
1157
- this.host = host;
1235
+ }
1236
+
1237
+ isInlineLayout() {
1238
+ return this.params.layout === 'inline';
1239
+ }
1240
+
1241
+ renderInlineShell() {
1242
+ const inlineShell = document.createElement('div');
1243
+ inlineShell.className = 'oa-inline-shell';
1244
+ inlineShell.innerHTML = this.buildPopupContent({ closeable: false });
1245
+ this.shadow.appendChild(inlineShell);
1246
+
1247
+ this.popup = {
1248
+ host: this.host,
1249
+ root: inlineShell,
1250
+ inline: true,
1251
+ };
1252
+
1253
+ this.bindPopupEvents(inlineShell);
1254
+ }
1255
+
1256
+ resetInlineShell() {
1257
+ if (!this.popup?.root) return;
1258
+
1259
+ this.popup.root.innerHTML = this.buildPopupContent({ closeable: false });
1260
+ this.bindPopupEvents(this.popup.root);
1158
1261
  }
1159
1262
 
1160
1263
  startChallenge() {
@@ -1187,6 +1290,14 @@
1187
1290
  }
1188
1291
 
1189
1292
  openPopup() {
1293
+ if (this.isInlineLayout()) {
1294
+ if (!this.popup) {
1295
+ this.renderInlineShell();
1296
+ }
1297
+
1298
+ return this.getVideo();
1299
+ }
1300
+
1190
1301
  if (this.popup) return this.getVideo();
1191
1302
 
1192
1303
  const anchor = this.getPopupAnchor();
@@ -1364,7 +1475,7 @@
1364
1475
  this.popup.root.style.pointerEvents = 'auto';
1365
1476
  }
1366
1477
 
1367
- buildPopupContent() {
1478
+ buildPopupContent({ closeable = true } = {}) {
1368
1479
  return `
1369
1480
  <div class="oa-header">
1370
1481
  <div class="oa-title">
@@ -1375,10 +1486,14 @@
1375
1486
  </a>
1376
1487
  <span class="oa-badge">on-device</span>
1377
1488
  </div>
1378
- <button class="oa-close-btn"
1379
- aria-label="Close">
1380
- ${CLOSE_SVG}
1381
- </button>
1489
+ ${
1490
+ closeable
1491
+ ? `<button class="oa-close-btn"
1492
+ aria-label="Close">
1493
+ ${CLOSE_SVG}
1494
+ </button>`
1495
+ : ''
1496
+ }
1382
1497
  </div>
1383
1498
  <div class="oa-body">
1384
1499
  ${heroTemplate('Initializing…')}
@@ -1391,7 +1506,7 @@
1391
1506
  `;
1392
1507
  }
1393
1508
 
1394
- bindPopupEvents(root, shadow) {
1509
+ bindPopupEvents(root) {
1395
1510
  const closeBtn = root.querySelector('.oa-close-btn');
1396
1511
  if (closeBtn) {
1397
1512
  closeBtn.addEventListener('click', () => {
@@ -1509,6 +1624,22 @@
1509
1624
  }
1510
1625
 
1511
1626
  showResult(outcome, message) {
1627
+ if (this.isInlineLayout()) {
1628
+ if (!this.popupElements?.body) return;
1629
+
1630
+ this.popupElements.body.innerHTML = resultTemplate(outcome, message);
1631
+ this.hideActions();
1632
+
1633
+ if (outcome === 'pass') {
1634
+ this.setState('verified');
1635
+ return;
1636
+ }
1637
+
1638
+ this.setState(outcome === 'fail' ? 'failed' : 'retry');
1639
+ this.showActions('Try Again');
1640
+ return;
1641
+ }
1642
+
1512
1643
  if (outcome === 'pass') {
1513
1644
  this.closePopup();
1514
1645
  this.setState('verified');
@@ -1544,8 +1675,21 @@
1544
1675
  }
1545
1676
  }
1546
1677
 
1547
- showError() {
1548
- this.setState('retry');
1678
+ showError(message) {
1679
+ if (!this.popupElements?.body) return;
1680
+
1681
+ this.popupElements.body.innerHTML = errorStepTemplate(message);
1682
+ this.popupElements.errorCountdown = this.popupElements.body.querySelector(
1683
+ '.oa-error-step-countdown'
1684
+ );
1685
+ this.hideActions();
1686
+ }
1687
+
1688
+ setErrorCountdown(seconds) {
1689
+ if (!this.popupElements?.errorCountdown) return;
1690
+
1691
+ const unit = seconds === 1 ? 'second' : 'seconds';
1692
+ this.popupElements.errorCountdown.textContent = `Closing in ${seconds} ${unit}…`;
1549
1693
  }
1550
1694
 
1551
1695
  clearError() {
@@ -1556,6 +1700,22 @@
1556
1700
 
1557
1701
  closePopup() {
1558
1702
  if (!this.popup) return;
1703
+
1704
+ if (this.popup.inline) {
1705
+ this.popup.cleanup?.();
1706
+ if (this.popupFrame) {
1707
+ cancelAnimationFrame(this.popupFrame);
1708
+ this.popupFrame = 0;
1709
+ }
1710
+ this.resetInlineShell();
1711
+
1712
+ if (this.state === 'loading') {
1713
+ this.setState('idle');
1714
+ }
1715
+
1716
+ return;
1717
+ }
1718
+
1559
1719
  this.popup.cleanup?.();
1560
1720
  this.popup.themeCleanup?.();
1561
1721
  this.popup.host.remove();
@@ -2723,6 +2883,73 @@
2723
2883
  return new Promise((r) => setTimeout(r, ms));
2724
2884
  }
2725
2885
 
2886
+ function resolveChallengeErrorMessage(error) {
2887
+ const name = typeof error?.name === 'string' ? error.name : '';
2888
+ const message = typeof error?.message === 'string' ? error.message : String(error || '');
2889
+ const normalized = `${name} ${message}`.toLowerCase();
2890
+
2891
+ if (
2892
+ name === 'NotFoundError' ||
2893
+ /request is not allowed by the user agent or the platform in the current context/.test(
2894
+ normalized
2895
+ ) ||
2896
+ /requested device not found|device not found|no camera|could not start video source/.test(
2897
+ normalized
2898
+ )
2899
+ ) {
2900
+ return 'No camera available. Plug in a camera and try again.';
2901
+ }
2902
+
2903
+ if (
2904
+ name === 'NotAllowedError' ||
2905
+ name === 'PermissionDeniedError' ||
2906
+ /permission|camera access|access denied/.test(normalized)
2907
+ ) {
2908
+ return 'Camera access was blocked. Allow camera access and try again.';
2909
+ }
2910
+
2911
+ if (/positioning timeout/.test(normalized)) {
2912
+ return 'Face positioning timed out. Reopen the check and try again.';
2913
+ }
2914
+
2915
+ return 'Verification failed. Please try again.';
2916
+ }
2917
+
2918
+ function isInlineLayout(widget) {
2919
+ return widget?.params?.layout === 'inline';
2920
+ }
2921
+
2922
+ async function showErrorStep(widget, error) {
2923
+ const message = resolveChallengeErrorMessage(error);
2924
+
2925
+ if (isInlineLayout(widget)) {
2926
+ widget.showResult?.('retry', message);
2927
+ return;
2928
+ }
2929
+
2930
+ if (!widget.popup) {
2931
+ widget.openPopup?.();
2932
+ }
2933
+
2934
+ widget.showError?.(message);
2935
+
2936
+ for (let seconds = ERROR_STEP_SECONDS; seconds > 0; seconds--) {
2937
+ if (!widget.popup) break;
2938
+ widget.setErrorCountdown?.(seconds);
2939
+ await sleep(1000);
2940
+ }
2941
+
2942
+ widget.closePopup?.();
2943
+ widget.setState?.('retry');
2944
+ }
2945
+
2946
+ async function handleChallengeError(widget, emitter, error) {
2947
+ console.log('Error during challenge:', error);
2948
+ await showErrorStep(widget, error);
2949
+ emitter.emit('error', error, widget.id);
2950
+ widget.params.errorCallback?.(error);
2951
+ }
2952
+
2726
2953
  async function runChallenge(widget, emitter) {
2727
2954
  const mode = widget.params.mode || 'serverless';
2728
2955
 
@@ -2752,10 +2979,14 @@
2752
2979
  await Promise.all([loadVision(), initAgeEstimator()]);
2753
2980
 
2754
2981
  modelBuffer = await loadModel();
2755
- widget.showReady();
2756
2982
 
2757
- await waitForStart(widget);
2758
- await startCameraFlow(widget, modelBuffer);
2983
+ if (isInlineLayout(widget)) {
2984
+ await startCameraFlow(widget, modelBuffer);
2985
+ } else {
2986
+ widget.showReady();
2987
+ await waitForStart(widget);
2988
+ await startCameraFlow(widget, modelBuffer);
2989
+ }
2759
2990
 
2760
2991
  const transport = createTransport('serverless', params);
2761
2992
 
@@ -2803,10 +3034,7 @@
2803
3034
  emitResult(widget, emitter, result);
2804
3035
  } catch (error) {
2805
3036
  cleanupLocal();
2806
- console.log('Error during challenge:', error);
2807
- widget.showResult('fail', 'Verification failed');
2808
- emitter.emit('error', error, widget.id);
2809
- params.errorCallback?.(error);
3037
+ await handleChallengeError(widget, emitter, error);
2810
3038
  }
2811
3039
  }
2812
3040
 
@@ -2847,10 +3075,16 @@
2847
3075
  captureFrame: () => (captureFrame() ? 'true' : 'null'),
2848
3076
  });
2849
3077
 
2850
- widget.showReady();
2851
- await waitForStart(widget);
3078
+ let video;
3079
+
3080
+ if (isInlineLayout(widget)) {
3081
+ video = widget.showCamera();
3082
+ } else {
3083
+ widget.showReady();
3084
+ await waitForStart(widget);
3085
+ video = widget.showCamera();
3086
+ }
2852
3087
 
2853
- const video = widget.showCamera();
2854
3088
  widget.setVideoStatus('Requesting camera…');
2855
3089
  await startCamera(video);
2856
3090
  exposeMirrorVideo(video);
@@ -2937,11 +3171,8 @@
2937
3171
 
2938
3172
  cleanupVM(transport);
2939
3173
  } catch (error) {
2940
- console.log('Error during challenge:', error);
2941
3174
  cleanupVM();
2942
- widget.showResult('fail', 'Verification failed');
2943
- emitter.emit('error', error, widget.id);
2944
- params.errorCallback?.(error);
3175
+ await handleChallengeError(widget, emitter, error);
2945
3176
  }
2946
3177
  }
2947
3178
 
@@ -3156,8 +3387,19 @@
3156
3387
  const emitter = new EventEmitter();
3157
3388
  const widgets = new Map();
3158
3389
 
3390
+ function normalizeLayout(layout) {
3391
+ if (layout === 'inline' || layout === 'embed' || layout === 'embedded') {
3392
+ return 'inline';
3393
+ }
3394
+
3395
+ return 'widget';
3396
+ }
3397
+
3159
3398
  function normalizeParams(params) {
3160
3399
  const globalConfig = typeof window !== 'undefined' ? window.openage || {} : {};
3400
+ const layout = normalizeLayout(
3401
+ params.layout ?? params.display ?? globalConfig.layout ?? globalConfig.display
3402
+ );
3161
3403
 
3162
3404
  return {
3163
3405
  mode: 'serverless',
@@ -3166,6 +3408,7 @@
3166
3408
  minAge: 18,
3167
3409
  ...globalConfig,
3168
3410
  ...params,
3411
+ layout,
3169
3412
  };
3170
3413
  }
3171
3414
 
@@ -3174,6 +3417,10 @@
3174
3417
  emitter.emit('opened', widget.id);
3175
3418
  runChallenge(widget, emitter);
3176
3419
  };
3420
+
3421
+ if (widget.params.layout === 'inline') {
3422
+ widget.startChallenge();
3423
+ }
3177
3424
  }
3178
3425
 
3179
3426
  function render(container, params = {}) {
@@ -3322,6 +3569,7 @@
3322
3569
  size: element.dataset.size,
3323
3570
  action: element.dataset.action,
3324
3571
  mode: element.dataset.mode,
3572
+ layout: element.dataset.layout || element.dataset.display,
3325
3573
  server: element.dataset.server,
3326
3574
  };
3327
3575