vishwam-react-ent-sdk 1.0.7 → 1.0.9-beta.0

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
@@ -1 +1,148 @@
1
- # vishwam-react-sdk
1
+ # vishwam-react-ent-sdk
2
+
3
+ React components for **ENT photo liveness** (`CheckLiveness`): in-browser face capture, on-device face detection, and backend liveness verification.
4
+
5
+ **Peer dependency:** React `^17.0.1`
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install vishwam-react-ent-sdk
13
+ # or
14
+ yarn add vishwam-react-ent-sdk
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Quick start
20
+
21
+ Integration values (`url`, `appId`, and related props) are issued for your deployment. Do not use example credentials in production.
22
+
23
+ ```jsx
24
+ import React from 'react'
25
+ import { CheckLiveness } from 'vishwam-react-ent-sdk'
26
+
27
+ function App() {
28
+ function onLiveness(response) {
29
+ // response.livenessImage — base64 (no data: prefix)
30
+ // response.livenessImageType — e.g. image/jpeg or image/png
31
+ }
32
+
33
+ return (
34
+ <CheckLiveness
35
+ url={apiBaseUrl}
36
+ appId={appId}
37
+ userId={userId}
38
+ referenceId={referenceId}
39
+ captureType={captureType}
40
+ eyesCheck={eyesCheck}
41
+ storeId={storeId}
42
+ whiteBackground={whiteBackground}
43
+ expiryIn={expiryIn}
44
+ getLivenessResponse={onLiveness}
45
+ />
46
+ )
47
+ }
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Exports
53
+
54
+ | Export | Description |
55
+ |--------|-------------|
56
+ | `CheckLiveness` | Photo liveness UI + API call |
57
+ | `ReadDocument` | Document capture flow |
58
+ | `Init` | Face detector preparation helper |
59
+ | `faceMatch` | Face match API helper |
60
+
61
+ ---
62
+
63
+ ## `CheckLiveness` props
64
+
65
+ | Prop | Type | Description |
66
+ |------|------|-------------|
67
+ | `url` | string | API base URL for your environment |
68
+ | `appId` | string | Application id |
69
+ | `userId` | string \| number | User id |
70
+ | `referenceId` | string | Reference / transaction id |
71
+ | `captureType` | string | Capture type sent to API |
72
+ | `eyesCheck` | string | Eyes-open check flag for API |
73
+ | `storeId` | string | Store id |
74
+ | `whiteBackground` | string | White background flag |
75
+ | `expiryIn` | string | Token expiry (e.g. `1h`) |
76
+ | `getLivenessResponse` | function | Callback with API response (see below) |
77
+
78
+ ---
79
+
80
+ ## Capture and images
81
+
82
+ On capture the SDK:
83
+
84
+ 1. Reads a frame from the camera (desktop: webcam screenshot; mobile: canvas snapshot).
85
+ 2. Builds two square images from the same frame:
86
+ - **450×450** — sent as the second image field in the liveness API request.
87
+ - **224×224** — sent as the first image field in the liveness API request.
88
+ 3. Calls the liveness endpoint with both images (after obtaining an auth token).
89
+
90
+ On-device face detection models are loaded by the SDK at runtime.
91
+
92
+ ---
93
+
94
+ ## `getLivenessResponse` callback
95
+
96
+ The SDK merges the API JSON with local image fields before calling your handler:
97
+
98
+ | Field | Description |
99
+ |-------|-------------|
100
+ | `status` | API status (e.g. `success` / `failed`) |
101
+ | `livenessImage` | Base64-encoded face image (**no** `data:` prefix) |
102
+ | `livenessImageType` | MIME type for decoding (typically `image/jpeg` on desktop, `image/png` on mobile) |
103
+ | … | Other fields returned by the liveness API |
104
+
105
+ **Display example:**
106
+
107
+ ```javascript
108
+ const src = `data:${response.livenessImageType};base64,${response.livenessImage}`
109
+ ```
110
+
111
+ Decode with `Image` / `naturalWidth` × `naturalHeight` if you need pixel dimensions.
112
+
113
+ ---
114
+
115
+ ## Changelog
116
+
117
+ ### 1.0.8
118
+
119
+ **Callback image quality**
120
+
121
+ - **`livenessImage` is now the 450×450 capture** (same asset previously sent only as the high-resolution API upload).
122
+ - In **1.0.7**, `livenessImage` was the **224×224** image. Integrators that assumed 224×224 in the callback should update display/storage logic; API upload sizes are unchanged.
123
+
124
+ **Desktop bug fix**
125
+
126
+ - On **desktop**, startup now runs the full face-detection initialization path (models, UI ready state, detection loop) instead of loading models only, which could leave laptop browsers stuck on “face detector is initializing”.
127
+ - **Mobile** flow is unchanged.
128
+
129
+ ### 1.0.7
130
+
131
+ - Updated on-device model loading for face detection.
132
+
133
+ ---
134
+
135
+ ## Build (maintainers)
136
+
137
+ ```bash
138
+ npm install
139
+ npm run build
140
+ ```
141
+
142
+ Published tarball includes `dist/` (`prepare` runs build on `npm publish`).
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ MIT
package/dist/index.js CHANGED
@@ -34,6 +34,25 @@ function _taggedTemplateLiteralLoose(strings, raw) {
34
34
  return strings;
35
35
  }
36
36
 
37
+ // A type of promise-like that resolves synchronously and supports only one observer
38
+
39
+ const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator";
40
+
41
+ const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator";
42
+
43
+ // Asynchronously call a function and send errors to recovery continuation
44
+ function _catch(body, recover) {
45
+ try {
46
+ var result = body();
47
+ } catch(e) {
48
+ return recover(e);
49
+ }
50
+ if (result && result.then) {
51
+ return result.then(void 0, recover);
52
+ }
53
+ return result;
54
+ }
55
+
37
56
  var requestToken = function requestToken(url, expiry) {
38
57
  try {
39
58
  var formData = new FormData();
@@ -399,6 +418,13 @@ var videoConstraints = {
399
418
  facingMode: 'user'
400
419
  }
401
420
  };
421
+ var FRONTAL_FACE_RX_MIN = 0.15;
422
+ var FRONTAL_FACE_RX_MAX = 2.53;
423
+ var FRONTAL_FACE_RY_MIN = 0.16;
424
+ var FRONTAL_FACE_RY_MAX = 2.56;
425
+ var FACE_TOO_FAR_RATIO = 6;
426
+ var FACE_TOO_CLOSE_RATIO = 3.2;
427
+ var FACE_MAX_ROLL_DEG = 12;
402
428
 
403
429
  function frontalFaceCheck(landmarks) {
404
430
  if (!landmarks || landmarks.length < 68) {
@@ -421,12 +447,10 @@ function frontalFaceCheck(landmarks) {
421
447
  x: (rightEyeCenter.x - landmarks[30].x) / (landmarks[30].x - leftEyeCenter.x),
422
448
  y: (landmarks[66].y - landmarks[30].y) / (landmarks[30].y - faceMiddlePoint.y)
423
449
  };
424
-
425
- if (ratio.x > 0.75 && ratio.x < 1.33 && ratio.y > 0.56 && ratio.y < 1.56) {
426
- return true;
427
- }
428
-
429
- return false;
450
+ var rollRad = Math.atan2(rightEyeCenter.y - leftEyeCenter.y, rightEyeCenter.x - leftEyeCenter.x);
451
+ var rollDeg = Math.abs(rollRad * 180 / Math.PI);
452
+ var rollOk = rollDeg <= FACE_MAX_ROLL_DEG || rollDeg >= 180 - FACE_MAX_ROLL_DEG;
453
+ return rollOk && ratio.x > FRONTAL_FACE_RX_MIN && ratio.x < FRONTAL_FACE_RX_MAX && ratio.y > FRONTAL_FACE_RY_MIN && ratio.y < FRONTAL_FACE_RY_MAX;
430
454
  }
431
455
 
432
456
  (function polyfillGetUserMedia() {
@@ -489,18 +513,53 @@ function CheckLiveness(_ref) {
489
513
 
490
514
  var _temp4 = function () {
491
515
  if (!stopFaceDetection) {
492
- return Promise.resolve(detectFace()).then(function (detection) {
493
- if (detection) {
494
- var isFrontal = frontalFaceCheck(detection.landmarks.positions);
495
-
496
- if (isFrontal) {
497
- setFrontal(true);
498
- } else {
516
+ return Promise.resolve(detectFace()).then(function (detections) {
517
+ if (detections.length > 1) {
518
+ setFrontal(false);
519
+ setIsTooFar(false);
520
+ setIsTooClose(false);
521
+ setGuidanceText(multipleFacesMessageText);
522
+ } else if (detections.length === 0) {
523
+ setFrontal(false);
524
+ setIsTooFar(false);
525
+ setIsTooClose(false);
526
+ setGuidanceText(noFaceMessageText);
527
+ } else {
528
+ var _detections$0$detecti = detections[0].detection.box,
529
+ width = _detections$0$detecti.width,
530
+ height = _detections$0$detecti.height;
531
+ var videoEl = webcamRef.current;
532
+ var faceArea = width * height;
533
+ var videoArea = (videoEl === null || videoEl === void 0 ? void 0 : videoEl.videoWidth) * (videoEl === null || videoEl === void 0 ? void 0 : videoEl.videoHeight);
534
+ var faceRatio = videoArea / faceArea;
535
+
536
+ if (faceRatio > FACE_TOO_FAR_RATIO) {
537
+ setIsTooFar(true);
538
+ setIsTooClose(false);
499
539
  setFrontal(false);
540
+ setGuidanceText('');
541
+ } else if (faceRatio < FACE_TOO_CLOSE_RATIO) {
542
+ setIsTooFar(false);
543
+ setIsTooClose(true);
544
+ setFrontal(false);
545
+ setGuidanceText('');
546
+ } else {
547
+ setIsTooFar(false);
548
+ setIsTooClose(false);
549
+ var isFrontal = frontalFaceCheck(detections[0].landmarks.positions);
550
+ setFrontal(isFrontal);
551
+ setGuidanceText(isFrontal ? '' : straightFaceMessageText);
500
552
  }
501
- } else {
553
+ }
554
+
555
+ if (!navigator.onLine) {
502
556
  setFrontal(false);
557
+ setIsTooFar(false);
558
+ setIsTooClose(false);
559
+ setGuidanceText('');
503
560
  }
561
+
562
+ setLoadingText('');
504
563
  });
505
564
  }
506
565
  }();
@@ -573,8 +632,42 @@ function CheckLiveness(_ref) {
573
632
  var detectFace = function detectFace() {
574
633
  try {
575
634
  var videoEl = document.getElementById('webcam');
635
+
636
+ if (!videoEl) {
637
+ return Promise.resolve([]);
638
+ }
639
+
576
640
  var options = new faceapi.TinyFaceDetectorOptions();
577
- return Promise.resolve(faceapi.detectSingleFace(videoEl, options).withFaceLandmarks(true));
641
+
642
+ if (videoEl.readyState !== HTMLVideoElement.HAVE_ENOUGH_DATA) {
643
+ return Promise.resolve(new Promise(function (resolve) {
644
+ videoEl.addEventListener('loadeddata', function () {
645
+ try {
646
+ var _temp6 = _catch(function () {
647
+ return Promise.resolve(faceapi.detectAllFaces(videoEl, options).withFaceLandmarks(true)).then(function (result) {
648
+ resolve(result);
649
+ });
650
+ }, function (error) {
651
+ console.error('Face detection error:', error);
652
+ resolve([]);
653
+ });
654
+
655
+ return Promise.resolve(_temp6 && _temp6.then ? _temp6.then(function () {}) : void 0);
656
+ } catch (e) {
657
+ return Promise.reject(e);
658
+ }
659
+ }, {
660
+ once: true
661
+ });
662
+ }));
663
+ }
664
+
665
+ return Promise.resolve(_catch(function () {
666
+ return Promise.resolve(faceapi.detectAllFaces(videoEl, options).withFaceLandmarks(true));
667
+ }, function (error) {
668
+ console.error('Face detection error:', error);
669
+ return [];
670
+ }));
578
671
  } catch (e) {
579
672
  return Promise.reject(e);
580
673
  }
@@ -589,6 +682,11 @@ function CheckLiveness(_ref) {
589
682
  storeId = _ref.storeId,
590
683
  whiteBackground = _ref.whiteBackground,
591
684
  expiryIn = _ref.expiryIn,
685
+ tooFarMessage = _ref.tooFarMessage,
686
+ tooCloseMessage = _ref.tooCloseMessage,
687
+ multipleFacesMessage = _ref.multipleFacesMessage,
688
+ noFaceMessage = _ref.noFaceMessage,
689
+ straightFaceMessage = _ref.straightFaceMessage,
592
690
  getLivenessResponse = _ref.getLivenessResponse;
593
691
  var webcamRef = React.useRef();
594
692
 
@@ -608,10 +706,27 @@ function CheckLiveness(_ref) {
608
706
  cameraAvailable = _React$useState[0],
609
707
  setCameraAvailable = _React$useState[1];
610
708
 
611
- var _useState4 = React.useState('Please wait, while the face detector is initializing'),
612
- loadingText = _useState4[0],
613
- setLoadingText = _useState4[1];
709
+ var _useState4 = React.useState(false),
710
+ isTooFar = _useState4[0],
711
+ setIsTooFar = _useState4[1];
712
+
713
+ var _useState5 = React.useState(false),
714
+ isTooClose = _useState5[0],
715
+ setIsTooClose = _useState5[1];
716
+
717
+ var _useState6 = React.useState(''),
718
+ guidanceText = _useState6[0],
719
+ setGuidanceText = _useState6[1];
720
+
721
+ var _useState7 = React.useState('Please wait, while the face detector is initializing'),
722
+ loadingText = _useState7[0],
723
+ setLoadingText = _useState7[1];
614
724
 
725
+ var tooFarMessageText = tooFarMessage || 'Move a little closer';
726
+ var tooCloseMessageText = tooCloseMessage || 'Move a little further away';
727
+ var multipleFacesMessageText = multipleFacesMessage || 'Multiple faces detected. Only one person is allowed.';
728
+ var noFaceMessageText = noFaceMessage || 'No face detected. Keep your face in the circle.';
729
+ var straightFaceMessageText = straightFaceMessage || 'Look straight at the camera.';
615
730
  var interval;
616
731
  var videoStream = null;
617
732
  var stopFaceDetection = false;
@@ -631,7 +746,7 @@ function CheckLiveness(_ref) {
631
746
  var img2 = imgStr.split(',')[1];
632
747
  clearInterval(interval);
633
748
  return Promise.resolve(callLiveness(url, appId, userId, referenceId, captureType, eyesCheck, storeId, whiteBackground, expiryIn, img, img2, mimeString)).then(function (response) {
634
- response.livenessImage = img2;
749
+ response.livenessImage = img;
635
750
  response.livenessImageType = mimeString;
636
751
  getLivenessResponse(response);
637
752
  setLoading(false);
@@ -650,7 +765,7 @@ function CheckLiveness(_ref) {
650
765
 
651
766
  console.log('came here return');
652
767
 
653
- if (!videoStream) {
768
+ if (videoStream) {
654
769
  videoStream.getTracks().forEach(function (track) {
655
770
  track.stop();
656
771
  });
@@ -659,20 +774,10 @@ function CheckLiveness(_ref) {
659
774
  }, []);
660
775
  React.useEffect(function () {
661
776
  setTimeout(function () {
662
- var initModels = function initModels() {
663
- try {
664
- return Promise.resolve(faceapi.nets.tinyFaceDetector.loadFromUri('https://skyc-cdn-prod.vishwamcorp.com/apis/models/')).then(function () {
665
- return Promise.resolve(faceapi.nets.faceLandmark68TinyNet.loadFromUri('https://skyc-cdn-prod.vishwamcorp.com/apis/models/')).then(function () {});
666
- });
667
- } catch (e) {
668
- return Promise.reject(e);
669
- }
670
- };
671
-
672
777
  if (reactDeviceDetect.isMobile) {
673
778
  startCamera();
674
779
  } else {
675
- initModels();
780
+ init();
676
781
  }
677
782
  }, 500);
678
783
  }, []);
@@ -827,7 +932,31 @@ function CheckLiveness(_ref) {
827
932
  marginBottom: '0',
828
933
  fontWeight: '400'
829
934
  }
830
- }, loadingText), /*#__PURE__*/React__default.createElement(Button, {
935
+ }, loadingText), isTooFar ? /*#__PURE__*/React__default.createElement(Text, {
936
+ style: {
937
+ width: '75%',
938
+ fontSize: '1rem',
939
+ marginTop: '1%',
940
+ marginBottom: '0',
941
+ fontWeight: '400'
942
+ }
943
+ }, tooFarMessageText) : null, isTooClose ? /*#__PURE__*/React__default.createElement(Text, {
944
+ style: {
945
+ width: '75%',
946
+ fontSize: '1rem',
947
+ marginTop: '1%',
948
+ marginBottom: '0',
949
+ fontWeight: '400'
950
+ }
951
+ }, tooCloseMessageText) : null, guidanceText ? /*#__PURE__*/React__default.createElement(Text, {
952
+ style: {
953
+ width: '75%',
954
+ fontSize: '1rem',
955
+ marginTop: '1%',
956
+ marginBottom: '0',
957
+ fontWeight: '400'
958
+ }
959
+ }, guidanceText) : null, /*#__PURE__*/React__default.createElement(Button, {
831
960
  disabled: disabled || !frontal,
832
961
  onClick: capture
833
962
  }, disabled || frontal ? /*#__PURE__*/React__default.createElement(SvgEnabled, {
@@ -875,7 +1004,31 @@ function CheckLiveness(_ref) {
875
1004
  marginBottom: '0',
876
1005
  fontWeight: '400'
877
1006
  }
878
- }, loadingText), cameraAvailable ? /*#__PURE__*/React__default.createElement("div", null) : /*#__PURE__*/React__default.createElement(Text, {
1007
+ }, loadingText), isTooFar ? /*#__PURE__*/React__default.createElement(Text, {
1008
+ style: {
1009
+ width: '75%',
1010
+ fontSize: '1rem',
1011
+ marginTop: '1%',
1012
+ marginBottom: '0',
1013
+ fontWeight: '400'
1014
+ }
1015
+ }, tooFarMessageText) : null, isTooClose ? /*#__PURE__*/React__default.createElement(Text, {
1016
+ style: {
1017
+ width: '75%',
1018
+ fontSize: '1rem',
1019
+ marginTop: '1%',
1020
+ marginBottom: '0',
1021
+ fontWeight: '400'
1022
+ }
1023
+ }, tooCloseMessageText) : null, guidanceText ? /*#__PURE__*/React__default.createElement(Text, {
1024
+ style: {
1025
+ width: '75%',
1026
+ fontSize: '1rem',
1027
+ marginTop: '1%',
1028
+ marginBottom: '0',
1029
+ fontWeight: '400'
1030
+ }
1031
+ }, guidanceText) : null, cameraAvailable ? /*#__PURE__*/React__default.createElement("div", null) : /*#__PURE__*/React__default.createElement(Text, {
879
1032
  style: {
880
1033
  width: '75%',
881
1034
  fontSize: '1rem',
@@ -923,25 +1076,6 @@ function dataURItoBlob(dataURI, imageType) {
923
1076
  return bb;
924
1077
  }
925
1078
 
926
- // A type of promise-like that resolves synchronously and supports only one observer
927
-
928
- const _iteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.iterator || (Symbol.iterator = Symbol("Symbol.iterator"))) : "@@iterator";
929
-
930
- const _asyncIteratorSymbol = /*#__PURE__*/ typeof Symbol !== "undefined" ? (Symbol.asyncIterator || (Symbol.asyncIterator = Symbol("Symbol.asyncIterator"))) : "@@asyncIterator";
931
-
932
- // Asynchronously call a function and send errors to recovery continuation
933
- function _catch(body, recover) {
934
- try {
935
- var result = body();
936
- } catch(e) {
937
- return recover(e);
938
- }
939
- if (result && result.then) {
940
- return result.then(void 0, recover);
941
- }
942
- return result;
943
- }
944
-
945
1079
  var _path$1, _path2$1;
946
1080
 
947
1081
  function _extends$4() {