@tn3w/openage 1.0.4 → 1.0.5

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/README.md CHANGED
@@ -95,6 +95,10 @@ OpenAge.execute(widgetId);
95
95
  await OpenAge.challenge(params);
96
96
  ```
97
97
 
98
+ Runtime errors keep the popup open long enough to explain what happened.
99
+ If no camera is available, OpenAge tells the user to plug one in and closes
100
+ the popup automatically after 5 seconds.
101
+
98
102
  ## Main Params
99
103
 
100
104
  | Param | Values |
@@ -893,6 +893,52 @@ const STYLES = `
893
893
  .oa-result-fail { color: var(--oa-danger); }
894
894
  .oa-result-retry { color: var(--oa-warn); }
895
895
 
896
+ .oa-error-step {
897
+ display: flex;
898
+ flex-direction: column;
899
+ align-items: center;
900
+ gap: 0.7rem;
901
+ padding: 2rem 1.2rem;
902
+ background: var(--oa-surface);
903
+ border: 1px solid var(--oa-border);
904
+ border-radius: var(--oa-radius);
905
+ margin: 10px;
906
+ animation: oa-fade-in 0.4s ease;
907
+ text-align: center;
908
+ }
909
+
910
+ .oa-error-step-icon {
911
+ width: 3rem;
912
+ height: 3rem;
913
+ display: flex;
914
+ align-items: center;
915
+ justify-content: center;
916
+ border-radius: 999px;
917
+ background: rgba(239, 68, 68, 0.12);
918
+ color: var(--oa-danger);
919
+ font-size: 1.7rem;
920
+ line-height: 1;
921
+ }
922
+
923
+ .oa-error-step-title {
924
+ font-size: 1rem;
925
+ font-weight: 700;
926
+ color: var(--oa-text);
927
+ }
928
+
929
+ .oa-error-step-message {
930
+ font-size: 0.84rem;
931
+ line-height: 1.5;
932
+ color: var(--oa-text-muted);
933
+ max-width: 260px;
934
+ }
935
+
936
+ .oa-error-step-countdown {
937
+ font-size: 0.75rem;
938
+ font-weight: 600;
939
+ color: var(--oa-danger);
940
+ }
941
+
896
942
  .oa-hidden { display: none !important; }
897
943
 
898
944
  /* ── Animations ──────────────────────────────── */
@@ -1019,6 +1065,17 @@ function challengeTemplate() {
1019
1065
  `;
1020
1066
  }
1021
1067
 
1068
+ function errorStepTemplate(message) {
1069
+ return `
1070
+ <div class="oa-error-step">
1071
+ <div class="oa-error-step-icon">✕</div>
1072
+ <div class="oa-error-step-title">Verification stopped</div>
1073
+ <div class="oa-error-step-message">${message}</div>
1074
+ <div class="oa-error-step-countdown"></div>
1075
+ </div>
1076
+ `;
1077
+ }
1078
+
1022
1079
  function resultTemplate(outcome, message) {
1023
1080
  const icons = {
1024
1081
  fail: '✕',
@@ -1040,7 +1097,7 @@ function resultTemplate(outcome, message) {
1040
1097
 
1041
1098
  const VERSION = '1.0.0';
1042
1099
 
1043
- const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/' + '@mediapipe/tasks-vision@0.10.17';
1100
+ const MEDIAPIPE_CDN = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.17';
1044
1101
 
1045
1102
  const MEDIAPIPE_WASM = `${MEDIAPIPE_CDN}/wasm`;
1046
1103
 
@@ -1052,12 +1109,13 @@ const MEDIAPIPE_MODEL =
1052
1109
  'face_landmarker.task';
1053
1110
 
1054
1111
  const FACEAPI_CDN =
1055
- 'https://cdn.jsdelivr.net/npm/' + 'face-api.js@0.22.2/dist/face-api.min.js';
1112
+ 'https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js';
1056
1113
 
1057
1114
  const FACEAPI_MODEL_CDN =
1058
- 'https://cdn.jsdelivr.net/gh/' + 'justadudewhohacks/face-api.js@master/weights';
1115
+ 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@master/weights';
1059
1116
 
1060
1117
  const MAX_RETRIES = 3;
1118
+ const ERROR_STEP_SECONDS = 5;
1061
1119
  const BURST_FRAMES = 5;
1062
1120
  const BURST_INTERVAL_MS = 200;
1063
1121
  const POSITION_CHECK_MS = 100;
@@ -1538,8 +1596,20 @@ class Widget {
1538
1596
  }
1539
1597
  }
1540
1598
 
1541
- showError() {
1542
- this.setState('retry');
1599
+ showError(message) {
1600
+ if (!this.popupElements?.body) return;
1601
+
1602
+ this.popupElements.body.innerHTML = errorStepTemplate(message);
1603
+ this.popupElements.errorCountdown =
1604
+ this.popupElements.body.querySelector('.oa-error-step-countdown');
1605
+ this.hideActions();
1606
+ }
1607
+
1608
+ setErrorCountdown(seconds) {
1609
+ if (!this.popupElements?.errorCountdown) return;
1610
+
1611
+ const unit = seconds === 1 ? 'second' : 'seconds';
1612
+ this.popupElements.errorCountdown.textContent = `Closing in ${seconds} ${unit}…`;
1543
1613
  }
1544
1614
 
1545
1615
  clearError() {
@@ -2717,6 +2787,64 @@ function sleep(ms) {
2717
2787
  return new Promise((r) => setTimeout(r, ms));
2718
2788
  }
2719
2789
 
2790
+ function resolveChallengeErrorMessage(error) {
2791
+ const name = typeof error?.name === 'string' ? error.name : '';
2792
+ const message = typeof error?.message === 'string' ? error.message : String(error || '');
2793
+ const normalized = `${name} ${message}`.toLowerCase();
2794
+
2795
+ if (
2796
+ name === 'NotFoundError' ||
2797
+ /request is not allowed by the user agent or the platform in the current context/.test(
2798
+ normalized
2799
+ ) ||
2800
+ /requested device not found|device not found|no camera|could not start video source/.test(
2801
+ normalized
2802
+ )
2803
+ ) {
2804
+ return 'No camera available. Plug in a camera and try again.';
2805
+ }
2806
+
2807
+ if (
2808
+ name === 'NotAllowedError' ||
2809
+ name === 'PermissionDeniedError' ||
2810
+ /permission|camera access|access denied/.test(normalized)
2811
+ ) {
2812
+ return 'Camera access was blocked. Allow camera access and try again.';
2813
+ }
2814
+
2815
+ if (/positioning timeout/.test(normalized)) {
2816
+ return 'Face positioning timed out. Reopen the check and try again.';
2817
+ }
2818
+
2819
+ return 'Verification failed. Please try again.';
2820
+ }
2821
+
2822
+ async function showErrorStep(widget, error) {
2823
+ const message = resolveChallengeErrorMessage(error);
2824
+
2825
+ if (!widget.popup) {
2826
+ widget.openPopup?.();
2827
+ }
2828
+
2829
+ widget.showError?.(message);
2830
+
2831
+ for (let seconds = ERROR_STEP_SECONDS; seconds > 0; seconds--) {
2832
+ if (!widget.popup) break;
2833
+ widget.setErrorCountdown?.(seconds);
2834
+ await sleep(1000);
2835
+ }
2836
+
2837
+ widget.closePopup?.();
2838
+ widget.setState?.('retry');
2839
+ }
2840
+
2841
+ async function handleChallengeError(widget, emitter, error) {
2842
+ console.log('Error during challenge:', error);
2843
+ await showErrorStep(widget, error);
2844
+ emitter.emit('error', error, widget.id);
2845
+ widget.params.errorCallback?.(error);
2846
+ }
2847
+
2720
2848
  async function runChallenge(widget, emitter) {
2721
2849
  const mode = widget.params.mode || 'serverless';
2722
2850
 
@@ -2797,10 +2925,7 @@ async function runServerless(widget, emitter) {
2797
2925
  emitResult(widget, emitter, result);
2798
2926
  } catch (error) {
2799
2927
  cleanupLocal();
2800
- console.log('Error during challenge:', error);
2801
- widget.showResult('fail', 'Verification failed');
2802
- emitter.emit('error', error, widget.id);
2803
- params.errorCallback?.(error);
2928
+ await handleChallengeError(widget, emitter, error);
2804
2929
  }
2805
2930
  }
2806
2931
 
@@ -2931,11 +3056,8 @@ async function runServer(widget, emitter) {
2931
3056
 
2932
3057
  cleanupVM(transport);
2933
3058
  } catch (error) {
2934
- console.log('Error during challenge:', error);
2935
3059
  cleanupVM();
2936
- widget.showResult('fail', 'Verification failed');
2937
- emitter.emit('error', error, widget.id);
2938
- params.errorCallback?.(error);
3060
+ await handleChallengeError(widget, emitter, error);
2939
3061
  }
2940
3062
  }
2941
3063