pipwave-ekyc-uikit 3.0.6-beta.4 → 3.1.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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git branch *)"
5
+ ]
6
+ }
7
+ }
package/dist/pw-bundle.js CHANGED
@@ -4033,7 +4033,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4033
4033
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
4034
4034
 
4035
4035
  "use strict";
4036
- eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst face_detection_1 = __importDefault(__webpack_require__(/*! ./face-detection */ \"./src/plugins/face-detection.ts\"));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst baseDialog_1 = __importDefault(__webpack_require__(/*! ../components/baseDialog */ \"./src/components/baseDialog.ts\"));\nconst navigationBar_1 = __importDefault(__webpack_require__(/*! ../components/navigationBar */ \"./src/components/navigationBar.ts\"));\nconst footer_1 = __importDefault(__webpack_require__(/*! ../components/footer */ \"./src/components/footer.ts\"));\nconst imagesHelper_1 = __webpack_require__(/*! ../helper/imagesHelper */ \"./src/helper/imagesHelper.ts\");\nclass FaceCamera {\n constructor(cameraId, mainScreenId = \"\") {\n this.cameraTimeLimit = 90000;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.cameraId = cameraId;\n this.mainScreenId = mainScreenId;\n }\n isMobile() {\n // Use extracted helper function\n return (0, deviceDetection_1.isMobileDevice)();\n }\n captureImage() {\n var _a, _b, _c, _d, _e, _f;\n return __awaiter(this, void 0, void 0, function* () {\n const bitmap = this.videoElement;\n // 1. full image\n let canvas = document.createElement(\"canvas\");\n let context = canvas.getContext(\"2d\");\n canvas.width = bitmap.videoWidth;\n canvas.height = bitmap.videoHeight;\n context.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // 2. cropped image — region corresponds exactly to what the overlay shows\n let cropCanvas = document.createElement(\"canvas\");\n let cropContext = cropCanvas.getContext(\"2d\");\n cropCanvas.width = bitmap.videoWidth;\n cropCanvas.height = bitmap.videoHeight;\n cropContext.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // Scale factor: stream pixels per display pixel\n const scaleX = bitmap.videoWidth / ((_b = (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) !== null && _b !== void 0 ? _b : bitmap.videoWidth);\n const scaleY = bitmap.videoHeight / ((_d = (_c = this.videoElement) === null || _c === void 0 ? void 0 : _c.clientHeight) !== null && _d !== void 0 ? _d : bitmap.videoHeight);\n // Use overlay's actual on-screen position as the crop region\n const overlayRect = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.getBoundingClientRect();\n const videoRect = (_f = this.videoElement) === null || _f === void 0 ? void 0 : _f.getBoundingClientRect();\n let cropX, cropY, cropW, cropH;\n if (overlayRect && videoRect) {\n cropX = Math.round((overlayRect.left - videoRect.left) * scaleX);\n cropY = Math.round((overlayRect.top - videoRect.top) * scaleY);\n cropW = Math.round(overlayRect.width * scaleX);\n cropH = Math.round(overlayRect.height * scaleY);\n }\n else {\n // Fallback: derive from calculateHeightNWidth\n const { width, height } = this.faceDetectionSdk.calculateHeightNWidth(bitmap.videoWidth, bitmap.videoHeight);\n cropW = width;\n cropH = height;\n cropX = Math.round((bitmap.videoWidth - cropW) / 2);\n cropY = Math.round((bitmap.videoHeight - cropH) / 2);\n }\n // Clamp to stream bounds\n cropX = Math.max(0, cropX);\n cropY = Math.max(0, cropY);\n cropW = Math.min(cropW, bitmap.videoWidth - cropX);\n cropH = Math.min(cropH, bitmap.videoHeight - cropY);\n const imgData = cropContext.getImageData(cropX, cropY, cropW, cropH);\n cropCanvas.width = cropW;\n cropCanvas.height = cropH;\n cropContext.putImageData(imgData, 0, 0);\n // 3. resize the canvas\n try {\n canvas = (0, imagesHelper_1.resizeCanvas)(canvas, imagesHelper_1.maxFullImage.width, imagesHelper_1.maxFullImage.height);\n cropCanvas = (0, imagesHelper_1.resizeCanvas)(cropCanvas, imagesHelper_1.maxCroppedImage.width, imagesHelper_1.maxCroppedImage.height);\n }\n catch (err) {\n console.error(\"face resize image error\", err);\n }\n // 4. construct payload - convert to desired file format\n const payload = {\n fullImage: canvas.toDataURL(\"image/jpeg\"),\n croppedImage: cropCanvas.toDataURL(\"image/jpeg\"),\n };\n if (MainPage_1.enableImageDebug) {\n if (payload.fullImage)\n this.downloadImage(payload.fullImage, `selfie-full-${canvas.width}x${canvas.height}.jpg`);\n if (payload.croppedImage)\n this.downloadImage(payload.croppedImage, `selfie-cropped-${cropCanvas.width}x${cropCanvas.height}.jpg`);\n }\n return payload;\n });\n }\n init() {\n try {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n // 1. Draw the camera display\n this.drawCameraDisplay();\n // 2. Setup related SDK\n const { videoElement, canvasElement, overlayElement, titleElement } = this;\n this.faceDetectionSdk = new face_detection_1.default({\n cameraId: this.cameraId,\n videoElement,\n canvasElement,\n overlayElement,\n titleElement,\n });\n // 3. Get the camera permission and trigger the selfie detection\n this.cameraInit();\n // 4. Trigger event after 3 seconds of face detection\n this.faceDetectionSdk.setCaptureImage((livenessScore) => __awaiter(this, void 0, void 0, function* () {\n var _a;\n const image = yield this.captureImage();\n const event = new CustomEvent(\"selfieCapture\", {\n detail: { image, livenessScore },\n });\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);\n }));\n // 5. Face camera custom events listeners\n const description = document.getElementById(\"pw-camera-description\");\n this.faceDetectionSdk.listenToUserNoSmile(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_at_camera\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiled(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_seconds\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiledButNotLiveness(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_neutral_expression\");\n }\n });\n this.faceDetectionSdk.listenToLookStraight(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n }\n });\n this.faceDetectionSdk.listenToPlaceFace(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n }\n });\n this.faceDetectionSdk.listenToFaceTooFar(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_too_far\");\n }\n });\n // 6. Start timer to close camera\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n }\n finally {\n if (!this.faceDetectionSdk ||\n this.faceDetectionSdk.getLoadedModelState() !== \"LOADING\")\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n }\n drawCameraDisplay() {\n const mainBody = document.getElementById(this.cameraId);\n if (!mainBody)\n return;\n // 1. header section\n const header = document.createElement(\"div\");\n header.id = camera_1.ELEMENT_IDS.HEADER;\n // header.classList.add(\"camera-header-area\");\n header.classList.add(\"pw-absolute\", \"pw-z-[2]\", \"pw-w-full\", \"pw-text-white\");\n const navigationDiv = new navigationBar_1.default().render(() => {\n this.stopVideoStream();\n });\n header.appendChild(navigationDiv);\n const titleDiv = document.createElement(\"div\");\n titleDiv.id = \"pw-camera-title\";\n titleDiv.classList.add(\"pw-text-2xl\", \"pw-text-center\", \"pw-mb-4\", \"pw-px-5\");\n const body = document.createElement(\"div\");\n body.id = camera_1.ELEMENT_IDS.BODY;\n body.classList.add(\"camera-body-area\");\n const videoContainer = document.createElement(\"div\");\n videoContainer.id = camera_1.ELEMENT_IDS.VIDEO_CONTAINER;\n videoContainer.classList.add(\"video-container\");\n const overlayContainer = document.createElement(\"div\");\n overlayContainer.id = camera_1.ELEMENT_IDS.OVERLAY_CONTAINER;\n overlayContainer.classList.add(\"overlay-container\");\n const overlayElement = document.createElement(\"div\");\n overlayElement.id = camera_1.ELEMENT_IDS.OVERLAY;\n overlayElement.classList.add(\"overlay-element\");\n overlayElement.classList.add(\"pw-hidden\");\n overlayElement.classList.add(\"pw-relative\");\n // Set Description — full viewport width so text is not constrained by oval\n const descriptionDiv = document.createElement(\"div\");\n descriptionDiv.classList.add(\"pw-text-center\", \"pw-px-5\", \"pw-text-xl\", \"pw-mb-4\", \"pw-absolute\", \"pw-text-white\");\n descriptionDiv.style.zIndex = \"1\";\n descriptionDiv.style.left = \"50%\";\n descriptionDiv.style.marginLeft = \"-50vw\";\n descriptionDiv.style.bottom = \"100%\";\n descriptionDiv.style.transform = \"translateY(-20px)\";\n descriptionDiv.style.width = \"100vw\";\n descriptionDiv.style.boxSizing = \"border-box\";\n descriptionDiv.id = \"pw-camera-description\";\n descriptionDiv.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n this.descriptionElement = descriptionDiv;\n overlayElement.appendChild(descriptionDiv);\n const canvasElement = document.createElement(\"canvas\");\n canvasElement.id = camera_1.ELEMENT_IDS.CANVAS;\n canvasElement.classList.add(\"video-element\");\n canvasElement.classList.add(\"canvas-element\");\n canvasElement.style.transform = \"scaleX(-1)\";\n const videoElement = document.createElement(\"video\");\n videoElement.id = camera_1.ELEMENT_IDS.VIDEO;\n videoElement.autoplay = true;\n videoElement.playsInline = true;\n videoElement.preload = \"auto\";\n videoElement.loop = true;\n videoElement.classList.add(\"video-element\");\n videoElement.style.transform = \"scaleX(-1)\";\n // green tick icon - inside overlayElement so it aligns with overlay (same fix as card-camera)\n const iconElement = document.createElement(\"span\");\n iconElement.id = \"pw-camera-check-icon\";\n iconElement.classList.add(\"pw-hidden\", \"pw-absolute\", \"pw-inset-0\", \"pw-flex\", \"pw-items-center\", \"pw-justify-center\");\n const checkIcon = document.createElement(\"span\");\n checkIcon.classList.add(\"material-icons\", \"pw-text-green-500\", \"pw-text-4xl\", \"pw-rounded-full\");\n checkIcon.style.fontSize = \"48px\";\n checkIcon.innerHTML = \"check_circle\";\n iconElement.appendChild(checkIcon);\n // 2.1\n this.videoElement = videoElement;\n this.overlayElement = overlayElement;\n this.canvasElement = canvasElement;\n overlayElement.appendChild(iconElement);\n overlayContainer.appendChild(overlayElement);\n videoContainer.appendChild(overlayContainer);\n videoContainer.appendChild(canvasElement);\n videoContainer.appendChild(videoElement);\n body.appendChild(videoContainer);\n // 3. Footer - Powered by pipwave\n const footerWrapper = document.createElement(\"div\");\n footerWrapper.classList.add(\"pw-absolute\", \"pw-bottom-0\", \"pw-left-0\", \"pw-right-0\", \"pw-pb-4\", \"pw-z-[2]\", \"pw-flex\", \"pw-justify-center\", \"pw-items-center\", \"pw-text-white\");\n const footer = new footer_1.default().render(true);\n footerWrapper.appendChild(footer);\n mainBody.appendChild(footerWrapper);\n // 4. append child\n mainBody.classList.add(\"camera-bg\");\n mainBody.appendChild(header);\n mainBody.appendChild(body);\n }\n loadVideoStream(videoElement) {\n const initialConstraints = {\n facingMode: \"user\",\n width: {\n ideal: 1280, // 1280, 1024, 640\n },\n height: {\n ideal: 960, // 960, 768, 480\n },\n video: true,\n };\n // Load the video stream\n const nav = window.navigator.mediaDevices\n ? window.navigator.mediaDevices\n : window.navigator;\n if (!nav)\n return;\n nav\n .getUserMedia({ video: initialConstraints })\n .then((stream) => {\n videoElement.srcObject = stream;\n // iOS Safari sometimes leaves a getUserMedia <video> paused even with\n // autoplay+playsInline; an explicit play() avoids a frozen first frame\n // (the \"wait a long time before the camera starts\" symptom).\n const playResult = videoElement.play();\n if (playResult && typeof playResult.catch === \"function\") {\n playResult.catch((err) => console.error(\"Error starting video playback:\", err));\n }\n })\n .catch((err) => {\n console.error(\"Error accessing the camera:\", err);\n });\n }\n cameraInit() {\n var _a;\n if (!this.videoElement)\n return;\n this.loadVideoStream(this.videoElement);\n // video event listeners\n this.videoElement.addEventListener(\"loadedmetadata\", () => __awaiter(this, void 0, void 0, function* () {\n var _b;\n this.faceDetectionSdk.init();\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.classList.remove(\"pw-hidden\");\n // Set initial video dimensions based on orientation\n this.resizeElement();\n }));\n this.videoElement.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n window.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n (_a = screen.orientation) === null || _a === void 0 ? void 0 : _a.addEventListener(\"change\", () => {\n const orientationType = screen.orientation.type;\n if (!orientationType || !this.isMobile)\n return;\n if ([\"landscape-primary\", \"landscape-secondary\"].includes(orientationType)) {\n // TO ADD DIALOG ASK USER TO USE POTRAIT VIEW\n }\n else if ([\"portrait-secondary\", \"portrait-primary\"].includes(orientationType)) {\n // TO BE CONFIRM IF NEED CLOSE DIALOG\n }\n });\n }\n stopVideoStream() {\n var _a;\n // Stop the face-detection animation-frame loop FIRST. Paths that tear the\n // camera down without a preceding pauseSmileFace() (back button, idle\n // timeout, error redirects) would otherwise leave the requestAnimationFrame\n // loop running against a detached video element. Each leaked loop keeps\n // running face-api inference every frame; re-entering the selfie screen\n // stacks more loops and starves the iOS Safari main thread — which freezes\n // the countdown and makes detection unresponsive.\n (_a = this.faceDetectionSdk) === null || _a === void 0 ? void 0 : _a.disableDetection();\n if (this.videoElement && this.videoElement.srcObject) {\n const stream = this.videoElement.srcObject;\n const tracks = stream.getTracks();\n tracks.forEach(function (track) {\n track.stop();\n });\n this.videoElement.srcObject = null;\n }\n if (this.timer)\n clearTimeout(this.timer);\n const cameraDiv = document.getElementById(this.cameraId);\n cameraDiv === null || cameraDiv === void 0 ? void 0 : cameraDiv.remove();\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n resizeElement() {\n const videoContainer = document.getElementById(camera_1.ELEMENT_IDS.VIDEO_CONTAINER);\n const videoElement = this.videoElement;\n if (videoElement && videoElement.videoWidth && videoElement.videoHeight) {\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const videoAspect = videoElement.videoWidth / videoElement.videoHeight;\n // Fit the container to the camera's native aspect ratio within the viewport\n let containerW;\n let containerH;\n if (vpW / vpH > videoAspect) {\n // Viewport is wider than video — constrain by height\n containerH = vpH;\n containerW = vpH * videoAspect;\n }\n else {\n // Viewport is taller than video — constrain by width\n containerW = vpW;\n containerH = vpW / videoAspect;\n }\n if (videoContainer) {\n videoContainer.style.width = `${containerW}px`;\n videoContainer.style.height = `${containerH}px`;\n }\n videoElement.style.width = \"100%\";\n videoElement.style.height = \"100%\";\n videoElement.style.minWidth = \"\";\n videoElement.style.minHeight = \"\";\n }\n this.faceDetectionSdk.resizeElement();\n }\n listenToSelfieCapture(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"selfieCapture\", (e) => {\n cb(e);\n });\n }\n pauseSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // stop the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.pause();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.disableDetection());\n // stop idle timeout\n if (this.timer) {\n clearTimeout(this.timer);\n }\n // clear description\n const description = document.getElementById(\"pw-camera-description\");\n if (description)\n description.innerHTML = \"\";\n // show green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.remove(\"pw-hidden\");\n });\n }\n resumeSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // resume the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.play();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.startDetection());\n // start idle timeout\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n // hide green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.add(\"pw-hidden\");\n // remove green circle\n const overlayElement = document.getElementById(\"overlayElement\");\n if (overlayElement)\n overlayElement.classList.remove(\"overlay-element--active\");\n // close all the background loading\n (0, components_1.hideFullLoading)(this.cameraId);\n });\n }\n downloadImage(dataUrl, filename) {\n const link = document.createElement(\"a\");\n link.href = dataUrl;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n }\n}\nexports[\"default\"] = FaceCamera;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-camera.ts?");
4036
+ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __importDefault = (this && this.__importDefault) || function (mod) {\n return (mod && mod.__esModule) ? mod : { \"default\": mod };\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst face_detection_1 = __importDefault(__webpack_require__(/*! ./face-detection */ \"./src/plugins/face-detection.ts\"));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst baseDialog_1 = __importDefault(__webpack_require__(/*! ../components/baseDialog */ \"./src/components/baseDialog.ts\"));\nconst navigationBar_1 = __importDefault(__webpack_require__(/*! ../components/navigationBar */ \"./src/components/navigationBar.ts\"));\nconst footer_1 = __importDefault(__webpack_require__(/*! ../components/footer */ \"./src/components/footer.ts\"));\nconst imagesHelper_1 = __webpack_require__(/*! ../helper/imagesHelper */ \"./src/helper/imagesHelper.ts\");\nclass FaceCamera {\n constructor(cameraId, mainScreenId = \"\") {\n this.cameraTimeLimit = 90000;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.cameraId = cameraId;\n this.mainScreenId = mainScreenId;\n }\n isMobile() {\n // Use extracted helper function\n return (0, deviceDetection_1.isMobileDevice)();\n }\n captureImage() {\n var _a, _b, _c, _d, _e, _f;\n return __awaiter(this, void 0, void 0, function* () {\n const bitmap = this.videoElement;\n // 1. full image\n let canvas = document.createElement(\"canvas\");\n let context = canvas.getContext(\"2d\");\n canvas.width = bitmap.videoWidth;\n canvas.height = bitmap.videoHeight;\n context.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // 2. cropped image — region corresponds exactly to what the overlay shows\n let cropCanvas = document.createElement(\"canvas\");\n let cropContext = cropCanvas.getContext(\"2d\");\n cropCanvas.width = bitmap.videoWidth;\n cropCanvas.height = bitmap.videoHeight;\n cropContext.drawImage(bitmap, 0, 0, bitmap.videoWidth, bitmap.videoHeight);\n // Scale factor: stream pixels per display pixel\n const scaleX = bitmap.videoWidth / ((_b = (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) !== null && _b !== void 0 ? _b : bitmap.videoWidth);\n const scaleY = bitmap.videoHeight / ((_d = (_c = this.videoElement) === null || _c === void 0 ? void 0 : _c.clientHeight) !== null && _d !== void 0 ? _d : bitmap.videoHeight);\n // Use overlay's actual on-screen position as the crop region\n const overlayRect = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.getBoundingClientRect();\n const videoRect = (_f = this.videoElement) === null || _f === void 0 ? void 0 : _f.getBoundingClientRect();\n let cropX, cropY, cropW, cropH;\n if (overlayRect && videoRect) {\n cropX = Math.round((overlayRect.left - videoRect.left) * scaleX);\n cropY = Math.round((overlayRect.top - videoRect.top) * scaleY);\n cropW = Math.round(overlayRect.width * scaleX);\n cropH = Math.round(overlayRect.height * scaleY);\n }\n else {\n // Fallback: derive from calculateHeightNWidth\n const { width, height } = this.faceDetectionSdk.calculateHeightNWidth(bitmap.videoWidth, bitmap.videoHeight);\n cropW = width;\n cropH = height;\n cropX = Math.round((bitmap.videoWidth - cropW) / 2);\n cropY = Math.round((bitmap.videoHeight - cropH) / 2);\n }\n // Clamp to stream bounds\n cropX = Math.max(0, cropX);\n cropY = Math.max(0, cropY);\n cropW = Math.min(cropW, bitmap.videoWidth - cropX);\n cropH = Math.min(cropH, bitmap.videoHeight - cropY);\n const imgData = cropContext.getImageData(cropX, cropY, cropW, cropH);\n cropCanvas.width = cropW;\n cropCanvas.height = cropH;\n cropContext.putImageData(imgData, 0, 0);\n // 3. resize the canvas\n try {\n canvas = (0, imagesHelper_1.resizeCanvas)(canvas, imagesHelper_1.maxFullImage.width, imagesHelper_1.maxFullImage.height);\n cropCanvas = (0, imagesHelper_1.resizeCanvas)(cropCanvas, imagesHelper_1.maxCroppedImage.width, imagesHelper_1.maxCroppedImage.height);\n }\n catch (err) {\n console.error(\"face resize image error\", err);\n }\n // 4. construct payload - convert to desired file format\n const payload = {\n fullImage: canvas.toDataURL(\"image/jpeg\"),\n croppedImage: cropCanvas.toDataURL(\"image/jpeg\"),\n };\n if (MainPage_1.enableImageDebug) {\n if (payload.fullImage)\n this.downloadImage(payload.fullImage, `selfie-full-${canvas.width}x${canvas.height}.jpg`);\n if (payload.croppedImage)\n this.downloadImage(payload.croppedImage, `selfie-cropped-${cropCanvas.width}x${cropCanvas.height}.jpg`);\n }\n return payload;\n });\n }\n init() {\n try {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n // 1. Draw the camera display\n this.drawCameraDisplay();\n // 2. Setup related SDK\n const { videoElement, canvasElement, overlayElement, titleElement } = this;\n this.faceDetectionSdk = new face_detection_1.default({\n cameraId: this.cameraId,\n videoElement,\n canvasElement,\n overlayElement,\n titleElement,\n });\n // 3. Get the camera permission and trigger the selfie detection\n this.cameraInit();\n // 4. Trigger event after 3 seconds of face detection\n this.faceDetectionSdk.setCaptureImage((livenessScore) => __awaiter(this, void 0, void 0, function* () {\n var _a;\n const image = yield this.captureImage();\n const event = new CustomEvent(\"selfieCapture\", {\n detail: { image, livenessScore },\n });\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);\n }));\n // 5. Face camera custom events listeners\n const description = document.getElementById(\"pw-camera-description\");\n this.faceDetectionSdk.listenToUserNoSmile(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_at_camera\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiled(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_smile_seconds\");\n }\n });\n this.faceDetectionSdk.listenToUserSmiledButNotLiveness(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_neutral_expression\");\n }\n });\n this.faceDetectionSdk.listenToLookStraight(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n }\n });\n this.faceDetectionSdk.listenToPlaceFace(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n }\n });\n this.faceDetectionSdk.listenToFaceTooFar(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_too_far\");\n }\n });\n this.faceDetectionSdk.listenToFaceTooClose(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_too_close\");\n }\n });\n this.faceDetectionSdk.listenToFaceNotCentered(() => {\n if (description) {\n description.innerHTML = (0, translations_1.$t)(\"face_not_centered\");\n }\n });\n // 6. Start timer to close camera\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n }\n finally {\n if (!this.faceDetectionSdk ||\n this.faceDetectionSdk.getLoadedModelState() !== \"LOADING\")\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n }\n drawCameraDisplay() {\n const mainBody = document.getElementById(this.cameraId);\n if (!mainBody)\n return;\n // 1. header section\n const header = document.createElement(\"div\");\n header.id = camera_1.ELEMENT_IDS.HEADER;\n // header.classList.add(\"camera-header-area\");\n header.classList.add(\"pw-absolute\", \"pw-z-[2]\", \"pw-w-full\", \"pw-text-white\");\n const navigationDiv = new navigationBar_1.default().render(() => {\n this.stopVideoStream();\n });\n header.appendChild(navigationDiv);\n const titleDiv = document.createElement(\"div\");\n titleDiv.id = \"pw-camera-title\";\n titleDiv.classList.add(\"pw-text-2xl\", \"pw-text-center\", \"pw-mb-4\", \"pw-px-5\");\n const body = document.createElement(\"div\");\n body.id = camera_1.ELEMENT_IDS.BODY;\n body.classList.add(\"camera-body-area\");\n const videoContainer = document.createElement(\"div\");\n videoContainer.id = camera_1.ELEMENT_IDS.VIDEO_CONTAINER;\n videoContainer.classList.add(\"video-container\");\n const overlayContainer = document.createElement(\"div\");\n overlayContainer.id = camera_1.ELEMENT_IDS.OVERLAY_CONTAINER;\n overlayContainer.classList.add(\"overlay-container\");\n const overlayElement = document.createElement(\"div\");\n overlayElement.id = camera_1.ELEMENT_IDS.OVERLAY;\n overlayElement.classList.add(\"overlay-element\");\n overlayElement.classList.add(\"pw-hidden\");\n overlayElement.classList.add(\"pw-relative\");\n // Set Description — full viewport width so text is not constrained by oval\n const descriptionDiv = document.createElement(\"div\");\n descriptionDiv.classList.add(\"pw-text-center\", \"pw-px-5\", \"pw-text-xl\", \"pw-mb-4\", \"pw-absolute\", \"pw-text-white\");\n descriptionDiv.style.zIndex = \"1\";\n descriptionDiv.style.left = \"50%\";\n descriptionDiv.style.marginLeft = \"-50vw\";\n descriptionDiv.style.bottom = \"100%\";\n descriptionDiv.style.transform = \"translateY(-20px)\";\n descriptionDiv.style.width = \"100vw\";\n descriptionDiv.style.boxSizing = \"border-box\";\n descriptionDiv.id = \"pw-camera-description\";\n descriptionDiv.innerHTML = (0, translations_1.$t)(\"face_within_frame\");\n this.descriptionElement = descriptionDiv;\n overlayElement.appendChild(descriptionDiv);\n const canvasElement = document.createElement(\"canvas\");\n canvasElement.id = camera_1.ELEMENT_IDS.CANVAS;\n canvasElement.classList.add(\"video-element\");\n canvasElement.classList.add(\"canvas-element\");\n canvasElement.style.transform = \"scaleX(-1)\";\n const videoElement = document.createElement(\"video\");\n videoElement.id = camera_1.ELEMENT_IDS.VIDEO;\n videoElement.autoplay = true;\n videoElement.playsInline = true;\n videoElement.preload = \"auto\";\n videoElement.loop = true;\n videoElement.classList.add(\"video-element\");\n videoElement.style.transform = \"scaleX(-1)\";\n // green tick icon - inside overlayElement so it aligns with overlay (same fix as card-camera)\n const iconElement = document.createElement(\"span\");\n iconElement.id = \"pw-camera-check-icon\";\n iconElement.classList.add(\"pw-hidden\", \"pw-absolute\", \"pw-inset-0\", \"pw-flex\", \"pw-items-center\", \"pw-justify-center\");\n const checkIcon = document.createElement(\"span\");\n checkIcon.classList.add(\"material-icons\", \"pw-text-green-500\", \"pw-text-4xl\", \"pw-rounded-full\");\n checkIcon.style.fontSize = \"48px\";\n checkIcon.innerHTML = \"check_circle\";\n iconElement.appendChild(checkIcon);\n // 2.1\n this.videoElement = videoElement;\n this.overlayElement = overlayElement;\n this.canvasElement = canvasElement;\n overlayElement.appendChild(iconElement);\n overlayContainer.appendChild(overlayElement);\n videoContainer.appendChild(overlayContainer);\n videoContainer.appendChild(canvasElement);\n videoContainer.appendChild(videoElement);\n body.appendChild(videoContainer);\n // 3. Footer - Powered by pipwave\n const footerWrapper = document.createElement(\"div\");\n footerWrapper.classList.add(\"pw-absolute\", \"pw-bottom-0\", \"pw-left-0\", \"pw-right-0\", \"pw-pb-4\", \"pw-z-[2]\", \"pw-flex\", \"pw-justify-center\", \"pw-items-center\", \"pw-text-white\");\n const footer = new footer_1.default().render(true);\n footerWrapper.appendChild(footer);\n mainBody.appendChild(footerWrapper);\n // 4. append child\n mainBody.classList.add(\"camera-bg\");\n mainBody.appendChild(header);\n mainBody.appendChild(body);\n }\n loadVideoStream(videoElement) {\n const initialConstraints = {\n facingMode: \"user\",\n width: {\n ideal: 1280, // 1280, 1024, 640\n },\n height: {\n ideal: 960, // 960, 768, 480\n },\n video: true,\n };\n // Load the video stream\n const nav = window.navigator.mediaDevices\n ? window.navigator.mediaDevices\n : window.navigator;\n if (!nav)\n return;\n nav\n .getUserMedia({ video: initialConstraints })\n .then((stream) => {\n videoElement.srcObject = stream;\n // iOS Safari sometimes leaves a getUserMedia <video> paused even with\n // autoplay+playsInline; an explicit play() avoids a frozen first frame\n // (the \"wait a long time before the camera starts\" symptom).\n const playResult = videoElement.play();\n if (playResult && typeof playResult.catch === \"function\") {\n playResult.catch((err) => console.error(\"Error starting video playback:\", err));\n }\n })\n .catch((err) => {\n console.error(\"Error accessing the camera:\", err);\n });\n }\n cameraInit() {\n var _a;\n if (!this.videoElement)\n return;\n this.loadVideoStream(this.videoElement);\n // video event listeners\n this.videoElement.addEventListener(\"loadedmetadata\", () => __awaiter(this, void 0, void 0, function* () {\n var _b;\n this.faceDetectionSdk.init();\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.classList.remove(\"pw-hidden\");\n // Set initial video dimensions based on orientation\n this.resizeElement();\n }));\n this.videoElement.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n window.addEventListener(\"resize\", () => {\n this.resizeElement();\n });\n (_a = screen.orientation) === null || _a === void 0 ? void 0 : _a.addEventListener(\"change\", () => {\n const orientationType = screen.orientation.type;\n if (!orientationType || !this.isMobile)\n return;\n if ([\"landscape-primary\", \"landscape-secondary\"].includes(orientationType)) {\n // TO ADD DIALOG ASK USER TO USE POTRAIT VIEW\n }\n else if ([\"portrait-secondary\", \"portrait-primary\"].includes(orientationType)) {\n // TO BE CONFIRM IF NEED CLOSE DIALOG\n }\n });\n }\n stopVideoStream() {\n var _a;\n // Stop the face-detection animation-frame loop FIRST. Paths that tear the\n // camera down without a preceding pauseSmileFace() (back button, idle\n // timeout, error redirects) would otherwise leave the requestAnimationFrame\n // loop running against a detached video element. Each leaked loop keeps\n // running face-api inference every frame; re-entering the selfie screen\n // stacks more loops and starves the iOS Safari main thread — which freezes\n // the countdown and makes detection unresponsive.\n (_a = this.faceDetectionSdk) === null || _a === void 0 ? void 0 : _a.disableDetection();\n if (this.videoElement && this.videoElement.srcObject) {\n const stream = this.videoElement.srcObject;\n const tracks = stream.getTracks();\n tracks.forEach(function (track) {\n track.stop();\n });\n this.videoElement.srcObject = null;\n }\n if (this.timer)\n clearTimeout(this.timer);\n const cameraDiv = document.getElementById(this.cameraId);\n cameraDiv === null || cameraDiv === void 0 ? void 0 : cameraDiv.remove();\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n resizeElement() {\n const videoContainer = document.getElementById(camera_1.ELEMENT_IDS.VIDEO_CONTAINER);\n const videoElement = this.videoElement;\n if (videoElement && videoElement.videoWidth && videoElement.videoHeight) {\n const vpW = window.innerWidth;\n const vpH = window.innerHeight;\n const videoAspect = videoElement.videoWidth / videoElement.videoHeight;\n // Fit the container to the camera's native aspect ratio within the viewport\n let containerW;\n let containerH;\n if (vpW / vpH > videoAspect) {\n // Viewport is wider than video — constrain by height\n containerH = vpH;\n containerW = vpH * videoAspect;\n }\n else {\n // Viewport is taller than video — constrain by width\n containerW = vpW;\n containerH = vpW / videoAspect;\n }\n if (videoContainer) {\n videoContainer.style.width = `${containerW}px`;\n videoContainer.style.height = `${containerH}px`;\n }\n videoElement.style.width = \"100%\";\n videoElement.style.height = \"100%\";\n videoElement.style.minWidth = \"\";\n videoElement.style.minHeight = \"\";\n }\n this.faceDetectionSdk.resizeElement();\n }\n listenToSelfieCapture(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"selfieCapture\", (e) => {\n cb(e);\n });\n }\n pauseSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // stop the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.pause();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.disableDetection());\n // stop idle timeout\n if (this.timer) {\n clearTimeout(this.timer);\n }\n // clear description\n const description = document.getElementById(\"pw-camera-description\");\n if (description)\n description.innerHTML = \"\";\n // show green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.remove(\"pw-hidden\");\n });\n }\n resumeSmileFace() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n // resume the video\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.play();\n yield ((_b = this.faceDetectionSdk) === null || _b === void 0 ? void 0 : _b.startDetection());\n // start idle timeout\n this.timer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {\n this.stopVideoStream();\n // open idle dialog\n const idleDialog = new baseDialog_1.default();\n idleDialog.render(this.mainScreenId, (0, translations_1.$t)(\"idle_message\"), (0, translations_1.$t)(\"btn_okay\"));\n }), this.cameraTimeLimit);\n // hide green tick icon\n const checkIcon = document.getElementById(\"pw-camera-check-icon\");\n checkIcon === null || checkIcon === void 0 ? void 0 : checkIcon.classList.add(\"pw-hidden\");\n // remove green circle\n const overlayElement = document.getElementById(\"overlayElement\");\n if (overlayElement)\n overlayElement.classList.remove(\"overlay-element--active\");\n // close all the background loading\n (0, components_1.hideFullLoading)(this.cameraId);\n });\n }\n downloadImage(dataUrl, filename) {\n const link = document.createElement(\"a\");\n link.href = dataUrl;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n }\n}\nexports[\"default\"] = FaceCamera;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-camera.ts?");
4037
4037
 
4038
4038
  /***/ }),
4039
4039
 
@@ -4044,7 +4044,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4044
4044
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
4045
4045
 
4046
4046
  "use strict";
4047
- eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\n// Gated diagnostic logger. Enable by starting the SDK with enableImageDebug:true\n// (or ticking the debug checkbox on the demo page). Prefix makes the selfie /\n// face-detection logs easy to filter in the console: filter on \"[pw-face]\".\nfunction faceLog(...args) {\n if (MainPage_1.enableImageDebug)\n console.log(\"[pw-face]\", ...args);\n}\nlet loadedModelState = \"NOT_LOADED\";\n// face-api's FIRST inference compiles its WebGL shaders and is dramatically\n// slower than steady-state. We pay that cost once, up front, behind the loading\n// spinner (see warmUpDetection). Module-level so it persists across re-entries.\nlet warmedUp = false;\nlet faceApiInstance;\n// direct use faceapi if it is already import in client's project (especially angular)\nfaceApiInstance =\n typeof faceapi !== \"undefined\" ? faceapi : __webpack_require__(/*! face-api.js */ \"./node_modules/face-api.js/build/es6/index.js\");\nclass FACEDETECTIONSDK {\n constructor(val) {\n this.cameraId = \"\";\n this.isDone = false; // use to check if the face detection is done\n // true from the moment startDetection() begins until disableDetection() runs.\n // `iFrameRef` is only set AFTER the countdown ends, so it can't guard the\n // model-load + countdown window; this flag does, preventing stacked loops.\n this.isStartingDetection = false;\n this.prevEmotionalState = \"\";\n this.currenEmotionaltState = \"\";\n this.isLiveness = false;\n // --- hint debounce ---\n // detectFace() runs on every animation frame, so the raw hint can flip many\n // times a second near a threshold, making the on-screen message flicker.\n // emitHint() suppresses same-as-last hints and throttles genuine changes to\n // at most one per HINT_MIN_INTERVAL_MS.\n this.lastHintEvent = \"\";\n this.lastHintAt = 0;\n this.HINT_MIN_INTERVAL_MS = 500;\n // --- detection throttle ---\n // detectFace() re-fires the moment each inference resolves, so on fast\n // devices it runs inferences back-to-back (15-30/sec), burning GPU/battery\n // for samples no one perceives. Cap it at ~5fps — plenty for smile/liveness/\n // angle, which change over hundreds of ms. This is a floor, not a cap: on\n // slow devices where one inference already takes >200ms it's a no-op.\n this.lastDetectAt = 0;\n this.DETECT_MIN_INTERVAL_MS = 200;\n // set to true to enforce face-fill-ratio check (user must be close enough to camera)\n this.enforceFaceFillRatio = true;\n // set to true to enforce straight-look check during smile capture\n this.enforceLookStraight = true;\n // constant variable\n this.face_api_detection_options = new faceApiInstance.TinyFaceDetectorOptions({\n inputSize: 224,\n });\n this.faceTrack_settings = {\n contrast: 3,\n brightness: 0.5,\n threshold: 100,\n minCorrelation: 0.17,\n minScore: 0.6,\n };\n // html element\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.instructionElement = null;\n Object.assign(this, val);\n }\n getLoadedModelState() {\n return loadedModelState;\n }\n setCaptureImage(captureImage) {\n this.captureImage = captureImage;\n }\n // get the face position\n getFaceXY(pos) {\n const posF = {\n top: Math.min.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n bottom: Math.max.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n left: Math.min.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n right: Math.max.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n };\n return {\n x: posF[\"left\"],\n y: posF[\"top\"],\n w: posF[\"right\"] - posF[\"left\"],\n h: posF[\"bottom\"] - posF[\"top\"],\n };\n }\n // get the circle area position\n getCircleXY() {\n if (!this.canvasElement || !this.overlayElement)\n return { x: 0, y: 0, w: 0, h: 0 };\n // canvas rectangle position\n const posR = this.canvasElement.getBoundingClientRect();\n // circle position\n const posC = this.overlayElement.getBoundingClientRect();\n return {\n // get the coord x and y of the circle inside the canvas rectangle\n x: posC.x - posR.x,\n y: posC.y - posR.y,\n w: posC.width,\n h: posC.height,\n };\n }\n // to detect if your face is looking straight at the camera\n isAngleDetected(pos) {\n const angleOne = (Math.atan2(Math.abs(pos[7][\"y\"] - pos[33][\"y\"]), Math.abs(pos[7][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const angleTwo = (Math.atan2(Math.abs(pos[60][\"y\"] - pos[33][\"y\"]), Math.abs(pos[60][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const distance1 = Math.abs(pos[0][\"x\"] - pos[33][\"x\"]);\n const distance2 = Math.abs(pos[14][\"x\"] - pos[33][\"x\"]);\n const isAngle = !((angleOne > 40 && angleOne < 93) ||\n (angleTwo > 40 && angleTwo < 93));\n const isDistance = Math.abs(distance1 - distance2) / distance1 > 0.5;\n return isAngle || isDistance;\n }\n // check if the face is inside the circle area\n isFaceDetected(pos) {\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n // face-api landmarks stop at the eyebrows — the forehead above is not covered.\n // Add ~60% of landmark height as top padding to account for the unseen forehead,\n // so the oval check requires the full face (including forehead) to be inside.\n const foreheadPadding = posFace.h * 0.6;\n return (posFace.x + posFace.w < posCircle.x + posCircle.w &&\n posFace.x > posCircle.x &&\n posFace.y - foreheadPadding > posCircle.y &&\n posFace.y + posFace.h < posCircle.y + posCircle.h);\n }\n // check if the face fills enough of the oval (user is close enough to camera).\n // Face width and estimated full-face height (landmarks + forehead) must each\n // cover at least MIN_FACE_FILL_RATIO of the oval's dimensions.\n isFaceLargeEnough(pos) {\n const MIN_FACE_FILL_RATIO = 0.60;\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n // landmarks height + 60% forehead. Higher multiplier = less strict (face\n // counts as \"large enough\" from farther away); lower = stricter (user must\n // move closer to pass the fill-ratio check).\n const estimatedFaceHeight = posFace.h * 1.6;\n return (posFace.w / posCircle.w >= MIN_FACE_FILL_RATIO &&\n estimatedFaceHeight / posCircle.h >= MIN_FACE_FILL_RATIO);\n }\n // check if the face emotional is changing\n detectLiveness(current) {\n this.prevEmotionalState = this.currenEmotionaltState;\n this.currenEmotionaltState = current;\n return (this.prevEmotionalState &&\n this.currenEmotionaltState &&\n this.prevEmotionalState != this.currenEmotionaltState);\n }\n // check if the face in the video is similar to the previous face\n detectSimilarFace(current) {\n this.prevFaceDescriptor = this.currentFaceDescriptor;\n this.currentFaceDescriptor = current;\n if (!this.prevFaceDescriptor || !this.currentFaceDescriptor)\n return 1;\n // the smaller the distance, the more similar the faces are\n return faceApiInstance.euclideanDistance(this.prevFaceDescriptor, this.currentFaceDescriptor);\n }\n // reset instruction html text and all those checking\n resetInstructions() {\n this.isLiveness = false;\n this.currenEmotionaltState = \"\";\n this.prevEmotionalState = \"\";\n this.prevFaceDescriptor = undefined;\n this.currentFaceDescriptor = undefined;\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n // Dispatch a hint event, debounced. Skips repeats of the current hint and\n // ignores changes that arrive sooner than HINT_MIN_INTERVAL_MS after the last\n // one, so the guidance message can't flicker frame-to-frame.\n emitHint(eventName) {\n var _a;\n if (eventName === this.lastHintEvent)\n return;\n const now = performance.now();\n if (now - this.lastHintAt < this.HINT_MIN_INTERVAL_MS) {\n faceLog(`hint suppressed (throttled): \"${this.lastHintEvent}\" -> \"${eventName}\"`, `${Math.round(now - this.lastHintAt)}ms < ${this.HINT_MIN_INTERVAL_MS}ms`);\n return;\n }\n faceLog(`hint change: \"${this.lastHintEvent || \"—\"}\" -> \"${eventName}\"`);\n this.lastHintEvent = eventName;\n this.lastHintAt = now;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(new CustomEvent(eventName));\n }\n checkFace(livenessScore, pos, currentExpression, currentFaceDescriptor) {\n const isFace = this.isFaceDetected(pos);\n // 1. check if the face is inside the circle area\n if (!isFace) {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n return;\n }\n // 1b. check if the face is large enough (user not too far from camera)\n if (this.enforceFaceFillRatio && !this.isFaceLargeEnough(pos)) {\n this.resetInstructions();\n this.emitHint(\"faceTooFar\");\n return;\n }\n // 2. check if the face is looking straight at the camera\n const isLookingStraight = !this.isAngleDetected(pos);\n if (this.enforceLookStraight && !isLookingStraight) {\n this.resetInstructions();\n this.emitHint(\"lookStraight\");\n if (this.instructionElement)\n this.instructionElement.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n return;\n }\n // 3. check the liveness of the face\n if (this.detectLiveness(currentExpression))\n this.isLiveness = true;\n // 4. check the similarity of the face\n const isSimilarFace = this.detectSimilarFace(currentFaceDescriptor) < 0.6;\n if (this.isLiveness && !isSimilarFace) {\n this.resetInstructions();\n return;\n }\n if (currentExpression !== \"happy\") {\n this.emitHint(\"userNoSmile\");\n // this.overlayElement?.classList.add(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n else if (this.isLiveness) {\n // emit event here STAY 3 seconds\n this.emitHint(\"userSmiled\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.add(\"overlay-element--active\");\n if (isSimilarFace &&\n this.isLiveness &&\n this.currenEmotionaltState === \"happy\" &&\n (!this.enforceLookStraight || isLookingStraight) &&\n this.captureImage) {\n this.captureImage(livenessScore);\n }\n }\n else if (!this.isDone) {\n this.emitHint(\"userSmiledButNotLiveness\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.add(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n }\n detectFace() {\n return __awaiter(this, void 0, void 0, function* () {\n if (!this.videoElement || !this.canvasElement || !this.overlayElement)\n return;\n // throttle inference to ~DETECT_MIN_INTERVAL_MS. Re-schedule the loop\n // without running detection until enough time has elapsed.\n const now = performance.now();\n if (now - this.lastDetectAt < this.DETECT_MIN_INTERVAL_MS) {\n if (!this.isDone)\n this.iFrameRef = requestAnimationFrame(() => this.detectFace());\n return;\n }\n this.lastDetectAt = now;\n const displaySize = {\n width: this.canvasElement.width,\n height: this.canvasElement.height,\n };\n const detections = yield faceApiInstance\n .detectSingleFace(this.videoElement, this.face_api_detection_options)\n .withFaceLandmarks()\n .withFaceExpressions()\n .withFaceDescriptor();\n if (typeof detections !== \"undefined\") {\n const expressions = detections.expressions;\n const currentExpression = Object.keys(expressions).reduce((a, b) => {\n return expressions[a] >\n expressions[b]\n ? a\n : b;\n });\n const resizedDetections = faceApiInstance.resizeResults(detections, displaySize);\n const pos = resizedDetections.landmarks.positions;\n const score = detections.detection.score;\n if (score > this.faceTrack_settings.minScore && pos) {\n this.checkFace(score, pos, currentExpression, detections.descriptor);\n }\n else {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n }\n // draw canvas points for developer view only\n // if (typeof resizedDetections !== \"undefined\" && this.canvasElement) {\n // this.canvasElement\n // .getContext(\"2d\")\n // ?.clearRect(\n // 0,\n // 0,\n // this.canvasElement.width,\n // this.canvasElement.height\n // );\n // if (resizedDetections) {\n // faceApiInstance.draw.drawDetections(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceLandmarks(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceExpressions(\n // this.canvasElement,\n // resizedDetections\n // );\n // }\n // }\n }\n else {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n }\n if (!this.isDone)\n this.iFrameRef = requestAnimationFrame(() => this.detectFace());\n });\n }\n disableDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n faceLog(\"disableDetection: stopping loop + resetting state\", {\n hadLoop: this.iFrameRef !== undefined,\n });\n this.isDone = true;\n this.isStartingDetection = false;\n // reset hint debounce so the next session emits its first hint immediately\n this.lastHintEvent = \"\";\n this.lastHintAt = 0;\n // reset detection throttle so the next session's first inference runs at once\n this.lastDetectAt = 0;\n // stop face detection\n if (this.iFrameRef)\n cancelAnimationFrame(this.iFrameRef);\n this.iFrameRef = undefined;\n // reset all the instructions text\n this.resetInstructions();\n });\n }\n startDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n // Guard against stacking detection loops. A second entry can happen via\n // repeated `loadedmetadata`, a retry, or a resume without a matching pause.\n // `iFrameRef` is only set AFTER the 5s countdown finishes, so it alone\n // leaves the model-load + countdown window unguarded — two \"camera ready\"\n // signals in that window would each start their own countdown + loop and\n // run face-api inference 2x/3x per frame, starving the main thread (the\n // \"countdown stuck / detection unresponsive\" bug). `isStartingDetection`\n // closes that gap by marking detection active the moment we start.\n if (this.iFrameRef !== undefined || this.isStartingDetection) {\n faceLog(\"startDetection blocked: loop already starting/running\", {\n iFrameRef: this.iFrameRef,\n isStartingDetection: this.isStartingDetection,\n });\n return;\n }\n this.isStartingDetection = true;\n this.isDone = false;\n faceLog(\"startDetection: countdown started\");\n // start face detection\n (0, components_1.createCountdown)(this.cameraId, 5, () => __awaiter(this, void 0, void 0, function* () {\n faceLog(\"countdown finished: starting detection loop\");\n yield this.detectFace();\n }));\n });\n }\n // use to calculate the cropped image size / ratio\n // from ui or exact image src\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || 0;\n // calculate the circle size for the image\n // based on which has the largest size: height or width\n // then the circle will be 5/6 from that smaller size\n const circleSize = videoHeight > videoWidth ? width - width / 6 : height - height / 6;\n const newWidth = (circleSize / 5) * 4.5;\n const newHeight = circleSize;\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoW = this.videoElement.clientWidth;\n const videoH = this.videoElement.clientHeight;\n // 1. resize canvas to match video size\n this.canvasElement.width = videoW;\n this.canvasElement.height = videoH;\n // 2. desired oval size (9:10 width/height)\n const OVERLAY_ASPECT_RATIO = 9 / 10; // width / height\n const OVERLAY_MAX_HEIGHT = 500;\n const MOBILE_PADDING_VW = 0.08;\n let width;\n let height;\n if (window.screen.width >= 475) {\n height = OVERLAY_MAX_HEIGHT;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n else {\n const paddingPx = window.innerWidth * MOBILE_PADDING_VW;\n width = Math.round(window.innerWidth - 2 * paddingPx);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n }\n // 3. clamp oval to video bounds (oval must not exceed video width or height)\n if (videoW > 0 && videoH > 0) {\n const maxWidthByHeight = videoH * OVERLAY_ASPECT_RATIO;\n width = Math.min(width, videoW, maxWidthByHeight);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n if (height > videoH) {\n height = videoH;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n }\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"50%\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"solid white 3px\");\n (_c = this.overlayElement) === null || _c === void 0 ? void 0 : _c.style.setProperty(\"width\", `${width}px`);\n (_d = this.overlayElement) === null || _d === void 0 ? void 0 : _d.style.setProperty(\"height\", `${height}px`);\n }\n // train face-api models\n loadFaceModel() {\n return __awaiter(this, void 0, void 0, function* () {\n try {\n if (loadedModelState !== \"NOT_LOADED\")\n return;\n const url = \"https://assets.pipwave.com/ekyc/sdk/face-models\" || 0;\n loadedModelState = \"LOADING\";\n yield Promise.all([\n faceApiInstance.loadTinyFaceDetectorModel(url),\n faceApiInstance.loadFaceLandmarkModel(url),\n faceApiInstance.loadFaceRecognitionModel(url),\n faceApiInstance.loadFaceExpressionModel(url),\n ]);\n loadedModelState = \"LOADED\";\n }\n catch (err) {\n loadedModelState = \"NOT_LOADED\";\n return Promise.reject(err);\n }\n });\n }\n // Run one throwaway inference so face-api compiles its WebGL shaders / warms\n // the GPU pipeline now — while the loading spinner is still showing — instead\n // of on the first real frame right after the countdown. Without this, that\n // first inference blocks the main thread for seconds: the countdown appears\n // \"stuck at 1\" and the hint stays frozen until warmup finishes.\n warmUpDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n if (warmedUp) {\n faceLog(\"warmup skipped: already warmed up\");\n return;\n }\n const startedAt = performance.now();\n try {\n // Prefer the live video: if a face is visible it warms the FULL pipeline\n // (detector + landmarks + expressions + descriptor). The blank-canvas\n // fallback only warms the detector — with no face, detectSingleFace\n // returns undefined and the chained landmark/expression/descriptor steps\n // short-circuit, so their shaders aren't compiled until the first real\n // frame containing a face.\n let input;\n if (this.videoElement && this.videoElement.readyState >= 2) {\n input = this.videoElement;\n }\n else {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 224;\n canvas.height = 224;\n input = canvas;\n }\n faceLog(`warmup started (input: ${input instanceof HTMLVideoElement ? \"video\" : \"canvas\"})`);\n yield faceApiInstance\n .detectSingleFace(input, this.face_api_detection_options)\n .withFaceLandmarks()\n .withFaceExpressions()\n .withFaceDescriptor();\n warmedUp = true;\n faceLog(`warmup done in ${Math.round(performance.now() - startedAt)}ms`);\n }\n catch (err) {\n // best-effort: never block camera start if warmup fails\n faceLog(\"warmup failed (ignored):\", err);\n }\n });\n }\n init() {\n return __awaiter(this, void 0, void 0, function* () {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n this.isDone = false;\n this.resizeElement();\n if (loadedModelState === \"NOT_LOADED\")\n yield this.loadFaceModel();\n // Wait until loadFaceModel is LOADED\n while (loadedModelState === \"LOADING\") {\n yield new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (!this.iFrameRef && !this.isStartingDetection) {\n // warm up the inference pipeline while the spinner is still showing, so\n // detection is responsive the instant the countdown ends\n yield this.warmUpDetection();\n (0, components_1.hideFullLoading)(this.cameraId);\n yield this.startDetection();\n }\n });\n }\n listenToUserNoSmile(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userNoSmile\", () => {\n cb();\n });\n }\n listenToUserSmiled(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiled\", () => {\n cb();\n });\n }\n listenToUserSmiledButNotLiveness(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiledButNotLiveness\", () => {\n cb();\n });\n }\n listenToLookStraight(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"lookStraight\", () => {\n cb();\n });\n }\n listenToPlaceFace(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"placeFace\", () => {\n cb();\n });\n }\n listenToFaceTooFar(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"faceTooFar\", () => {\n cb();\n });\n }\n}\nexports[\"default\"] = FACEDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-detection.ts?");
4047
+ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\n// Gated diagnostic logger. Enable by starting the SDK with enableImageDebug:true\n// (or ticking the debug checkbox on the demo page). Prefix makes the selfie /\n// face-detection logs easy to filter in the console: filter on \"[pw-face]\".\nfunction faceLog(...args) {\n if (MainPage_1.enableImageDebug)\n console.log(\"[pw-face]\", ...args);\n}\n// Fill-ratio thresholds shared by the too-far / too-close checks (fraction of\n// the oval the face must cover). Below MIN -> \"move closer\"; above MAX -> \"move\n// further\". FACE_HEIGHT_MULTIPLIER estimates the full-face height from the\n// landmark height (which stops at the eyebrows) by adding ~60% for the forehead.\nconst MIN_FACE_FILL_RATIO = 0.6;\nconst MAX_FACE_FILL_RATIO = 0.95;\n// face-api landmarks stop at the eyebrows; add this fraction of the landmark\n// height back as \"forehead\" so size/position estimates cover the whole face.\nconst FOREHEAD_PADDING_RATIO = 0.6;\nconst FACE_HEIGHT_MULTIPLIER = 1 + FOREHEAD_PADDING_RATIO;\n// max allowed offset of the (full-face) center from the oval center, as a\n// fraction of the oval's width/height. Beyond this the face is off-center and\n// positioning fails — which also blocks auto-capture. Higher = more lenient.\nconst MAX_FACE_CENTER_OFFSET = 0.12;\nlet loadedModelState = \"NOT_LOADED\";\n// face-api's FIRST inference compiles its WebGL shaders and is dramatically\n// slower than steady-state. We pay that cost once, up front, behind the loading\n// spinner (see warmUpDetection). Module-level so it persists across re-entries.\nlet warmedUp = false;\nlet faceApiInstance;\n// direct use faceapi if it is already import in client's project (especially angular)\nfaceApiInstance =\n typeof faceapi !== \"undefined\" ? faceapi : __webpack_require__(/*! face-api.js */ \"./node_modules/face-api.js/build/es6/index.js\");\nclass FACEDETECTIONSDK {\n constructor(val) {\n this.cameraId = \"\";\n this.isDone = false; // use to check if the face detection is done\n // true from the moment startDetection() begins until disableDetection() runs.\n // `iFrameRef` is only set AFTER the countdown ends, so it can't guard the\n // model-load + countdown window; this flag does, preventing stacked loops.\n this.isStartingDetection = false;\n this.prevEmotionalState = \"\";\n this.currenEmotionaltState = \"\";\n this.isLiveness = false;\n // --- hint debounce ---\n // detectFace() runs on every animation frame, so the raw hint can flip many\n // times a second near a threshold, making the on-screen message flicker.\n // emitHint() suppresses same-as-last hints and throttles genuine changes to\n // at most one per HINT_MIN_INTERVAL_MS.\n this.lastHintEvent = \"\";\n this.lastHintAt = 0;\n this.HINT_MIN_INTERVAL_MS = 500;\n // Hints shown only once the face is correctly framed and steady. Entering this\n // set (from a positioning hint) bypasses the throttle in emitHint so the smile\n // prompt appears instantly; transitions within the set stay throttled.\n this.SMILE_STAGE_HINTS = [\"userNoSmile\", \"userSmiled\", \"userSmiledButNotLiveness\"];\n // --- detection throttle ---\n // detectFace() re-fires the moment each inference resolves, so on fast\n // devices it runs inferences back-to-back (15-30/sec), burning GPU/battery\n // for samples no one perceives. Cap it at ~5fps — plenty for smile/liveness/\n // angle, which change over hundreds of ms. This is a floor, not a cap: on\n // slow devices where one inference already takes >200ms it's a no-op.\n this.lastDetectAt = 0;\n this.DETECT_MIN_INTERVAL_MS = 200;\n // set to true to enforce face-fill-ratio check (user must be close enough to camera)\n this.enforceFaceFillRatio = true;\n // set to true to enforce straight-look check during smile capture\n this.enforceLookStraight = true;\n // constant variable\n this.face_api_detection_options = new faceApiInstance.TinyFaceDetectorOptions({\n inputSize: 224,\n });\n this.faceTrack_settings = {\n contrast: 3,\n brightness: 0.5,\n threshold: 100,\n minCorrelation: 0.17,\n minScore: 0.6,\n };\n // html element\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.instructionElement = null;\n Object.assign(this, val);\n }\n getLoadedModelState() {\n return loadedModelState;\n }\n setCaptureImage(captureImage) {\n this.captureImage = captureImage;\n }\n // get the face position\n getFaceXY(pos) {\n const posF = {\n top: Math.min.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n bottom: Math.max.apply(Math, pos.map(function (o) {\n return o.y;\n })),\n left: Math.min.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n right: Math.max.apply(Math, pos.map(function (o) {\n return o.x;\n })),\n };\n return {\n x: posF[\"left\"],\n y: posF[\"top\"],\n w: posF[\"right\"] - posF[\"left\"],\n h: posF[\"bottom\"] - posF[\"top\"],\n };\n }\n // get the circle area position\n getCircleXY() {\n if (!this.canvasElement || !this.overlayElement)\n return { x: 0, y: 0, w: 0, h: 0 };\n // canvas rectangle position\n const posR = this.canvasElement.getBoundingClientRect();\n // circle position\n const posC = this.overlayElement.getBoundingClientRect();\n return {\n // get the coord x and y of the circle inside the canvas rectangle\n x: posC.x - posR.x,\n y: posC.y - posR.y,\n w: posC.width,\n h: posC.height,\n };\n }\n // to detect if your face is looking straight at the camera\n isAngleDetected(pos) {\n const angleOne = (Math.atan2(Math.abs(pos[7][\"y\"] - pos[33][\"y\"]), Math.abs(pos[7][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const angleTwo = (Math.atan2(Math.abs(pos[60][\"y\"] - pos[33][\"y\"]), Math.abs(pos[60][\"x\"] - pos[33][\"x\"])) *\n 180) /\n Math.PI;\n const distance1 = Math.abs(pos[0][\"x\"] - pos[33][\"x\"]);\n const distance2 = Math.abs(pos[14][\"x\"] - pos[33][\"x\"]);\n const isAngle = !((angleOne > 40 && angleOne < 93) ||\n (angleTwo > 40 && angleTwo < 93));\n const isDistance = Math.abs(distance1 - distance2) / distance1 > 0.5;\n return isAngle || isDistance;\n }\n // check if the face is inside the circle area\n isFaceDetected(pos) {\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n // face-api landmarks stop at the eyebrows — the forehead above is not covered.\n // Add the forehead padding so the oval check requires the full face (including\n // forehead) to be inside.\n const foreheadPadding = posFace.h * FOREHEAD_PADDING_RATIO;\n return (posFace.x + posFace.w < posCircle.x + posCircle.w &&\n posFace.x > posCircle.x &&\n posFace.y - foreheadPadding > posCircle.y &&\n posFace.y + posFace.h < posCircle.y + posCircle.h);\n }\n // check if the face is centered in the oval. isFaceDetected only tests that the\n // face *fits* inside the oval, so a face slid to one side still passes it — and\n // would auto-capture off-center. This requires the full-face center (landmark\n // box adjusted upward for the unseen forehead) to sit near the oval center,\n // within MAX_FACE_CENTER_OFFSET of it on each axis.\n isFaceCentered(pos) {\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n const foreheadPadding = posFace.h * FOREHEAD_PADDING_RATIO;\n const faceCenterX = posFace.x + posFace.w / 2;\n // shift up by half the forehead padding so the center reflects the full face\n const faceCenterY = posFace.y - foreheadPadding / 2 + posFace.h / 2;\n const circleCenterX = posCircle.x + posCircle.w / 2;\n const circleCenterY = posCircle.y + posCircle.h / 2;\n const offsetX = Math.abs(faceCenterX - circleCenterX) / posCircle.w;\n const offsetY = Math.abs(faceCenterY - circleCenterY) / posCircle.h;\n faceLog(`center offset: x=${offsetX.toFixed(2)} y=${offsetY.toFixed(2)} ` +\n `(max ${MAX_FACE_CENTER_OFFSET})`);\n return (offsetX <= MAX_FACE_CENTER_OFFSET && offsetY <= MAX_FACE_CENTER_OFFSET);\n }\n // check if the face fills enough of the oval (user is close enough to camera).\n // Face width and estimated full-face height (landmarks + forehead) must each\n // cover at least MIN_FACE_FILL_RATIO of the oval's dimensions.\n isFaceLargeEnough(pos) {\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n const estimatedFaceHeight = posFace.h * FACE_HEIGHT_MULTIPLIER;\n return (posFace.w / posCircle.w >= MIN_FACE_FILL_RATIO &&\n estimatedFaceHeight / posCircle.h >= MIN_FACE_FILL_RATIO);\n }\n // check if the face fills too much of the oval (user is too close to camera).\n // Mirror of isFaceLargeEnough: if either the face width or the estimated\n // full-face height covers more than MAX_FACE_FILL_RATIO of the oval, the user\n // should move further back. Using OR (not AND) so we catch the user before the\n // face spills outside the oval entirely (which would otherwise read as \"no face\").\n isFaceTooClose(pos) {\n const posCircle = this.getCircleXY();\n const posFace = this.getFaceXY(pos);\n const estimatedFaceHeight = posFace.h * FACE_HEIGHT_MULTIPLIER;\n const wRatio = posFace.w / posCircle.w;\n const hRatio = estimatedFaceHeight / posCircle.h;\n // Live readout for tuning the too-close / too-far thresholds on-device.\n // Runs every frame the fill-ratio check is active (debug checkbox only).\n faceLog(`fill ratio: w=${wRatio.toFixed(2)} h=${hRatio.toFixed(2)} ` +\n `(tooClose>${MAX_FACE_FILL_RATIO}, tooFar<${MIN_FACE_FILL_RATIO})`);\n return wRatio > MAX_FACE_FILL_RATIO || hRatio > MAX_FACE_FILL_RATIO;\n }\n // check if the face emotional is changing\n detectLiveness(current) {\n this.prevEmotionalState = this.currenEmotionaltState;\n this.currenEmotionaltState = current;\n return (this.prevEmotionalState &&\n this.currenEmotionaltState &&\n this.prevEmotionalState != this.currenEmotionaltState);\n }\n // check if the face in the video is similar to the previous face\n detectSimilarFace(current) {\n this.prevFaceDescriptor = this.currentFaceDescriptor;\n this.currentFaceDescriptor = current;\n if (!this.prevFaceDescriptor || !this.currentFaceDescriptor)\n return 1;\n // the smaller the distance, the more similar the faces are\n return faceApiInstance.euclideanDistance(this.prevFaceDescriptor, this.currentFaceDescriptor);\n }\n // reset instruction html text and all those checking\n resetInstructions() {\n this.isLiveness = false;\n this.currenEmotionaltState = \"\";\n this.prevEmotionalState = \"\";\n this.prevFaceDescriptor = undefined;\n this.currentFaceDescriptor = undefined;\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n // Dispatch a hint event, debounced. Skips repeats of the current hint and\n // ignores changes that arrive sooner than HINT_MIN_INTERVAL_MS after the last\n // one, so the guidance message can't flicker frame-to-frame.\n //\n // Exception: entering the smile stage bypasses the throttle. Positioning hints\n // (placeFace / faceTooFar / faceTooClose / lookStraight) flip rapidly near\n // their thresholds and must stay throttled, but the smile-stage hints only\n // fire once the face is correctly framed and steady — so the moment the user\n // is in position we tell them to smile immediately, instead of letting the\n // prompt lag up to HINT_MIN_INTERVAL_MS and making them miss the timing.\n // Flicker *within* the smile stage (happy <-> not-happy) is still throttled.\n emitHint(eventName) {\n var _a;\n if (eventName === this.lastHintEvent)\n return;\n const enteringSmileStage = this.SMILE_STAGE_HINTS.includes(eventName) &&\n !this.SMILE_STAGE_HINTS.includes(this.lastHintEvent);\n const now = performance.now();\n if (!enteringSmileStage &&\n now - this.lastHintAt < this.HINT_MIN_INTERVAL_MS) {\n faceLog(`hint suppressed (throttled): \"${this.lastHintEvent}\" -> \"${eventName}\"`, `${Math.round(now - this.lastHintAt)}ms < ${this.HINT_MIN_INTERVAL_MS}ms`);\n return;\n }\n faceLog(`hint change: \"${this.lastHintEvent || \"—\"}\" -> \"${eventName}\"`);\n this.lastHintEvent = eventName;\n this.lastHintAt = now;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.dispatchEvent(new CustomEvent(eventName));\n }\n checkFace(livenessScore, pos, currentExpression, currentFaceDescriptor) {\n const isFace = this.isFaceDetected(pos);\n // 1. check if the face is too close (user must move further from camera).\n // Checked before the in-circle test so a face that has grown past the oval\n // reports \"too close\" instead of falling through to \"no face\".\n if (this.enforceFaceFillRatio && this.isFaceTooClose(pos)) {\n this.resetInstructions();\n this.emitHint(\"faceTooClose\");\n return;\n }\n // 2. check if the face is inside the circle area\n if (!isFace) {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n return;\n }\n // 2a. check if the face is centered. Re-checked every frame, so sliding\n // off-center during the smile stage drops back to \"center your face\" and\n // blocks the off-center auto-capture below.\n if (!this.isFaceCentered(pos)) {\n this.resetInstructions();\n this.emitHint(\"faceNotCentered\");\n return;\n }\n // 2b. check if the face is large enough (user not too far from camera)\n if (this.enforceFaceFillRatio && !this.isFaceLargeEnough(pos)) {\n this.resetInstructions();\n this.emitHint(\"faceTooFar\");\n return;\n }\n // 2. check if the face is looking straight at the camera\n const isLookingStraight = !this.isAngleDetected(pos);\n if (this.enforceLookStraight && !isLookingStraight) {\n this.resetInstructions();\n this.emitHint(\"lookStraight\");\n if (this.instructionElement)\n this.instructionElement.innerHTML = (0, translations_1.$t)(\"face_looks_straight\");\n return;\n }\n // 3. check the liveness of the face\n if (this.detectLiveness(currentExpression))\n this.isLiveness = true;\n // 4. check the similarity of the face\n const isSimilarFace = this.detectSimilarFace(currentFaceDescriptor) < 0.6;\n if (this.isLiveness && !isSimilarFace) {\n this.resetInstructions();\n return;\n }\n if (currentExpression !== \"happy\") {\n this.emitHint(\"userNoSmile\");\n // this.overlayElement?.classList.add(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n else if (this.isLiveness) {\n // emit event here STAY 3 seconds\n this.emitHint(\"userSmiled\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.add(\"overlay-element--active\");\n if (isSimilarFace &&\n this.isLiveness &&\n this.currenEmotionaltState === \"happy\" &&\n (!this.enforceLookStraight || isLookingStraight) &&\n this.captureImage) {\n this.captureImage(livenessScore);\n }\n }\n else if (!this.isDone) {\n this.emitHint(\"userSmiledButNotLiveness\");\n // this.overlayElement?.classList.remove(\"overlay-element--selfie-smile\");\n // this.overlayElement?.classList.add(\"overlay-element--selfie-neutral\");\n // this.overlayElement?.classList.remove(\"overlay-element--active\");\n }\n }\n detectFace() {\n return __awaiter(this, void 0, void 0, function* () {\n if (!this.videoElement || !this.canvasElement || !this.overlayElement)\n return;\n // throttle inference to ~DETECT_MIN_INTERVAL_MS. Re-schedule the loop\n // without running detection until enough time has elapsed.\n const now = performance.now();\n if (now - this.lastDetectAt < this.DETECT_MIN_INTERVAL_MS) {\n if (!this.isDone)\n this.iFrameRef = requestAnimationFrame(() => this.detectFace());\n return;\n }\n this.lastDetectAt = now;\n const displaySize = {\n width: this.canvasElement.width,\n height: this.canvasElement.height,\n };\n const detections = yield faceApiInstance\n .detectSingleFace(this.videoElement, this.face_api_detection_options)\n .withFaceLandmarks()\n .withFaceExpressions()\n .withFaceDescriptor();\n if (typeof detections !== \"undefined\") {\n const expressions = detections.expressions;\n const currentExpression = Object.keys(expressions).reduce((a, b) => {\n return expressions[a] >\n expressions[b]\n ? a\n : b;\n });\n const resizedDetections = faceApiInstance.resizeResults(detections, displaySize);\n const pos = resizedDetections.landmarks.positions;\n const score = detections.detection.score;\n if (score > this.faceTrack_settings.minScore && pos) {\n this.checkFace(score, pos, currentExpression, detections.descriptor);\n }\n else {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n }\n // draw canvas points for developer view only\n // if (typeof resizedDetections !== \"undefined\" && this.canvasElement) {\n // this.canvasElement\n // .getContext(\"2d\")\n // ?.clearRect(\n // 0,\n // 0,\n // this.canvasElement.width,\n // this.canvasElement.height\n // );\n // if (resizedDetections) {\n // faceApiInstance.draw.drawDetections(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceLandmarks(this.canvasElement, resizedDetections);\n // faceApiInstance.draw.drawFaceExpressions(\n // this.canvasElement,\n // resizedDetections\n // );\n // }\n // }\n }\n else {\n this.resetInstructions();\n this.emitHint(\"placeFace\");\n }\n if (!this.isDone)\n this.iFrameRef = requestAnimationFrame(() => this.detectFace());\n });\n }\n disableDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n faceLog(\"disableDetection: stopping loop + resetting state\", {\n hadLoop: this.iFrameRef !== undefined,\n });\n this.isDone = true;\n this.isStartingDetection = false;\n // reset hint debounce so the next session emits its first hint immediately\n this.lastHintEvent = \"\";\n this.lastHintAt = 0;\n // reset detection throttle so the next session's first inference runs at once\n this.lastDetectAt = 0;\n // stop face detection\n if (this.iFrameRef)\n cancelAnimationFrame(this.iFrameRef);\n this.iFrameRef = undefined;\n // reset all the instructions text\n this.resetInstructions();\n });\n }\n startDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n // Guard against stacking detection loops. A second entry can happen via\n // repeated `loadedmetadata`, a retry, or a resume without a matching pause.\n // `iFrameRef` is only set AFTER the 5s countdown finishes, so it alone\n // leaves the model-load + countdown window unguarded — two \"camera ready\"\n // signals in that window would each start their own countdown + loop and\n // run face-api inference 2x/3x per frame, starving the main thread (the\n // \"countdown stuck / detection unresponsive\" bug). `isStartingDetection`\n // closes that gap by marking detection active the moment we start.\n if (this.iFrameRef !== undefined || this.isStartingDetection) {\n faceLog(\"startDetection blocked: loop already starting/running\", {\n iFrameRef: this.iFrameRef,\n isStartingDetection: this.isStartingDetection,\n });\n return;\n }\n this.isStartingDetection = true;\n this.isDone = false;\n faceLog(\"startDetection: countdown started\");\n // start face detection\n (0, components_1.createCountdown)(this.cameraId, 5, () => __awaiter(this, void 0, void 0, function* () {\n faceLog(\"countdown finished: starting detection loop\");\n yield this.detectFace();\n }));\n });\n }\n // use to calculate the cropped image size / ratio\n // from ui or exact image src\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || 0;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || 0;\n // calculate the circle size for the image\n // based on which has the largest size: height or width\n // then the circle will be 5/6 from that smaller size\n const circleSize = videoHeight > videoWidth ? width - width / 6 : height - height / 6;\n const newWidth = (circleSize / 5) * 4.5;\n const newHeight = circleSize;\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoW = this.videoElement.clientWidth;\n const videoH = this.videoElement.clientHeight;\n // 1. resize canvas to match video size\n this.canvasElement.width = videoW;\n this.canvasElement.height = videoH;\n // 2. desired oval size (9:10 width/height)\n const OVERLAY_ASPECT_RATIO = 9 / 10; // width / height\n const OVERLAY_MAX_HEIGHT = 500;\n const MOBILE_PADDING_VW = 0.08;\n let width;\n let height;\n if (window.screen.width >= 475) {\n height = OVERLAY_MAX_HEIGHT;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n else {\n const paddingPx = window.innerWidth * MOBILE_PADDING_VW;\n width = Math.round(window.innerWidth - 2 * paddingPx);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n }\n // 3. clamp oval to video bounds (oval must not exceed video width or height)\n if (videoW > 0 && videoH > 0) {\n const maxWidthByHeight = videoH * OVERLAY_ASPECT_RATIO;\n width = Math.min(width, videoW, maxWidthByHeight);\n height = Math.round(width / OVERLAY_ASPECT_RATIO);\n if (height > videoH) {\n height = videoH;\n width = Math.round(height * OVERLAY_ASPECT_RATIO);\n }\n }\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"50%\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"solid white 3px\");\n (_c = this.overlayElement) === null || _c === void 0 ? void 0 : _c.style.setProperty(\"width\", `${width}px`);\n (_d = this.overlayElement) === null || _d === void 0 ? void 0 : _d.style.setProperty(\"height\", `${height}px`);\n }\n // train face-api models\n loadFaceModel() {\n return __awaiter(this, void 0, void 0, function* () {\n try {\n if (loadedModelState !== \"NOT_LOADED\")\n return;\n const url = \"https://assets.pipwave.com/ekyc/sdk/face-models\" || 0;\n loadedModelState = \"LOADING\";\n yield Promise.all([\n faceApiInstance.loadTinyFaceDetectorModel(url),\n faceApiInstance.loadFaceLandmarkModel(url),\n faceApiInstance.loadFaceRecognitionModel(url),\n faceApiInstance.loadFaceExpressionModel(url),\n ]);\n loadedModelState = \"LOADED\";\n }\n catch (err) {\n loadedModelState = \"NOT_LOADED\";\n return Promise.reject(err);\n }\n });\n }\n // Run one throwaway inference so face-api compiles its WebGL shaders / warms\n // the GPU pipeline now — while the loading spinner is still showing — instead\n // of on the first real frame right after the countdown. Without this, that\n // first inference blocks the main thread for seconds: the countdown appears\n // \"stuck at 1\" and the hint stays frozen until warmup finishes.\n warmUpDetection() {\n return __awaiter(this, void 0, void 0, function* () {\n if (warmedUp) {\n faceLog(\"warmup skipped: already warmed up\");\n return;\n }\n const startedAt = performance.now();\n try {\n // Prefer the live video: if a face is visible it warms the FULL pipeline\n // (detector + landmarks + expressions + descriptor). The blank-canvas\n // fallback only warms the detector — with no face, detectSingleFace\n // returns undefined and the chained landmark/expression/descriptor steps\n // short-circuit, so their shaders aren't compiled until the first real\n // frame containing a face.\n let input;\n if (this.videoElement && this.videoElement.readyState >= 2) {\n input = this.videoElement;\n }\n else {\n const canvas = document.createElement(\"canvas\");\n canvas.width = 224;\n canvas.height = 224;\n input = canvas;\n }\n faceLog(`warmup started (input: ${input instanceof HTMLVideoElement ? \"video\" : \"canvas\"})`);\n yield faceApiInstance\n .detectSingleFace(input, this.face_api_detection_options)\n .withFaceLandmarks()\n .withFaceExpressions()\n .withFaceDescriptor();\n warmedUp = true;\n faceLog(`warmup done in ${Math.round(performance.now() - startedAt)}ms`);\n }\n catch (err) {\n // best-effort: never block camera start if warmup fails\n faceLog(\"warmup failed (ignored):\", err);\n }\n });\n }\n init() {\n return __awaiter(this, void 0, void 0, function* () {\n // show loading spinner in full pg\n (0, components_1.createFullLoading)(this.cameraId, \"Initializing Camera...\");\n this.isDone = false;\n this.resizeElement();\n if (loadedModelState === \"NOT_LOADED\")\n yield this.loadFaceModel();\n // Wait until loadFaceModel is LOADED\n while (loadedModelState === \"LOADING\") {\n yield new Promise((resolve) => setTimeout(resolve, 500));\n }\n if (!this.iFrameRef && !this.isStartingDetection) {\n // warm up the inference pipeline while the spinner is still showing, so\n // detection is responsive the instant the countdown ends\n yield this.warmUpDetection();\n (0, components_1.hideFullLoading)(this.cameraId);\n yield this.startDetection();\n }\n });\n }\n listenToUserNoSmile(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userNoSmile\", () => {\n cb();\n });\n }\n listenToUserSmiled(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiled\", () => {\n cb();\n });\n }\n listenToUserSmiledButNotLiveness(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"userSmiledButNotLiveness\", () => {\n cb();\n });\n }\n listenToLookStraight(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"lookStraight\", () => {\n cb();\n });\n }\n listenToPlaceFace(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"placeFace\", () => {\n cb();\n });\n }\n listenToFaceTooFar(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"faceTooFar\", () => {\n cb();\n });\n }\n listenToFaceTooClose(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"faceTooClose\", () => {\n cb();\n });\n }\n listenToFaceNotCentered(cb) {\n var _a;\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.addEventListener(\"faceNotCentered\", () => {\n cb();\n });\n }\n}\nexports[\"default\"] = FACEDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/face-detection.ts?");
4048
4048
 
4049
4049
  /***/ }),
4050
4050
 
@@ -4055,7 +4055,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4055
4055
  /***/ ((__unused_webpack_module, exports) => {
4056
4056
 
4057
4057
  "use strict";
4058
- eval("\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports[\"default\"] = {\n // btn\n btn_start_now: \"Start Now\",\n btn_continue_on_mobile: \"Continue on Mobile\",\n btn_close: \"Close\",\n btn_okay: \"Okay\",\n btn_retake: \"Retake\",\n btn_proceed: \"Proceed\",\n btn_try_again: \"Try Again\",\n btn_submit: \"Submit\",\n btn_done: \"Done\",\n btn_capture_now: \"Capture now\",\n // common\n loading: \"Loading\",\n loading_dots: \"Loading...\",\n search: \"Search\",\n type_to_search: \"Type to search\",\n no_result_found: \"No result found\",\n // qr code dialog\n qr_dialog_title: \"Continue verification on mobile\",\n qr_instruction: \"Scan the QR code below using your mobile device to initiate the verification process.\",\n qr_browser_notice: \"Verification is supported only in Chrome, Firefox, and Safari (iOS). The WeChat QR Scanner is not supported.\",\n // first page\n national_identity_document_title: \"Capture ID photo\",\n national_identity_document_desc: \"Please ensure your document is clear, readable and valid.\",\n selfie_title: \"Face scan\",\n selfie_desc: \"Position your face in the frame and follow the instructions given. Do not wear sunglasses, hat or accessories. Background must be light and neutral.\",\n cc_title: \"Credit card verification\",\n cc_desc: \"\",\n poa_title: \"Utility bill verification\",\n poa_desc: \"Please ensure your document is clear, readable and valid.\",\n // national identity document\n select_id_type: \"Select ID type\",\n issuing_country: \"Issuing country/region\",\n document_type: \"Document type\",\n id_card: \"ID Card\",\n driver_license: \"Driver's License\",\n passport: \"Passport\",\n error_id_glare: \"The image has glare issues, please take a clear photo in a well-lit area.\",\n error_id_empty: \"No document detected in the image. Please ensure your document is clearly visible within the frame and properly lit.\",\n error_id_blur: \"The image is blurry. Please hold steady and take a clear photo.\",\n error_id_not_positioned: \"The document is not positioned properly. Please align it within the frame.\",\n error_id_unpair: \"The front and back images don't match. Please capture the same document.\",\n error_id_unsimilar: \"The front and back images appear to be from different documents.\",\n error_id_generic: \"Unable to verify the document. Please ensure it is clear, valid, and properly positioned.\",\n // selfie\n error_selfie: \"The image is blur, please take a clear photo.\",\n // credit card\n cc_missing_info_title: \"Missing card info.\",\n cc_missing_info_desc: \"Capture the side of the card with:\",\n // - cc_missing_content_* (dont change the id_key)\n cc_missing_content_full_name: \"Card owner's name\",\n cc_missing_content_cc_number: \"Card number\",\n cc_missing_content_cc_number_first_6: \"First 6 digits of card number\",\n cc_missing_content_cc_number_last_4: \"Last 4 digits of card number\",\n cc_missing_content_date_of_expiry: \"Expiry date\",\n cc_expired_with_retry: \"Credit card expired. Please provide a new card to continue.\",\n cc_expired_without_retry: \"Credit card expired. Please ensure your credit card is valid.\",\n error_cc_glare: \"The image has glare issues, please take a clear photo in a well-lit area.\",\n error_cc_empty: \"No card detected in the image. Please ensure your card is clearly visible within the frame and properly lit.\",\n error_cc_blur: \"The image is blurry. Please hold steady and take a clear photo.\",\n error_cc_generic: \"Unable to verify the card. Please ensure it is clear, valid, and properly positioned.\",\n // proof of address\n proof_of_address_title: \"Upload utility bill\",\n proof_of_address_desc: \"Please upload your utility bill\",\n image_format_label: \"Format: PNG, JPG, JPEG or PDF ({size}MB max)\",\n click_to_take_photo: \"Click to take photo\",\n poa_capture_or_choose: \"Capture or choose from gallery\",\n poa_doc_tip_title: \"Document tips:\",\n poa_doc_tip_1: \"Your name and address should be clearly visible.\",\n poa_doc_tip_2: \"Use PDF, JPEG, PNG files.\",\n poa_doc_tip_3: \"Max file size: 2MB.\",\n error_poa_file_missing: \"Please upload your utility bill to continue.\",\n error_poa_file_invalid: \"Only files in this extensions are allowed to be upload: PNG, JPG, JPEG or PDF ({size}MB max)\",\n error_poa_idle_long: \"The uploaded file is invalid due to you have been idle for too long.\",\n error_poa_file_generic: \"Unable to verify the document. Please ensure it is clear, valid, and properly uploaded.\",\n error_poa: \"Something went wrong. Please contact customer support for assistance or retry.\",\n // camera instruction\n id_front_title: \"ID Card | Front\",\n id_front_outline: \"Front of ID\",\n id_back_title: \"ID Card | Back\",\n id_back_outline: \"Back of ID\",\n license_front_title: \"License | Front\",\n license_back_title: \"License | Back\",\n passport_title: \"Passport\",\n cc_front_title: \"Credit Card\",\n cc_back_title: \"Credit Card\",\n camera_desc: \"Place your document within the frame and take photo.\",\n id_camera_dec: \"Please capture the front of your ID card, where your photo is visible. Ensure all details are clear.\",\n idle_message: \"You have been idle for too long.\",\n tips: \"Tips\",\n card: \"Card\",\n id_tip_1: \"Place your {cardType} within the frame and take picture.\",\n id_tip_2: \"Make sure the photo is not blur or cropped.\",\n id_tip_3: \"Avoid reflection and glare.\",\n selfie_tip_1: \"Position your face within the frame and follow the instructions given.\",\n selfie_tip_2: \"Make sure your face is not blur or cropped.\",\n selfie_tip_3: \"Do not wear sunglasses, hat or accessories.\",\n selfie_tip_4: \"Background must be light and neutral.\",\n cc_tip_title: \"Please show the info below:\",\n cc_tip_1: \"Card owner's name\",\n cc_tip_2: \"First 6 and last 4 digits ONLY\",\n cc_tip_3: \"Expiry date\",\n cc_tip_bottom_1: \"Some credit/debit cards may require capturing both front and back to obtain all mandatory information.\",\n cc_tip_bottom_2: \"You may cover the middle digits and CVV on the card manually or cover it later.\",\n flip_other_side: \"Flip the other side\",\n doc_align_card: \"Align your card within the frame\",\n doc_hold_steady: \"Hold steady…\",\n face_smile_at_camera: \"Please smile at the camera.\",\n face_smile_seconds: \"Please keep smiling for 3 seconds.\",\n face_neutral_expression: \"Please show a neutral expression at the camera.\",\n face_looks_straight: \"Please look straight at the camera.\",\n face_within_frame: \"Place your face within the rounded frame.\",\n face_too_far: \"Move closer to the camera.\",\n card_masking_title: \"Hide your info\",\n card_masking_desc: \"Please cover the middle digits and CVV on the card.\",\n // submission limit\n submission_limit_title: \"Submission limit reached\",\n submission_limit_desc: \"Your submission couldn't be processed as it has reached the maximum number of attempts.\",\n // analyze page\n analyze_title: \"Analyzing the pictures\",\n analyze_desc: \"Please do not close your browser, this may take a moment.\",\n error_analyze: \"Unable to verify your documents. Please check your documents and resubmit again.\",\n verification_failed: \"Verification unsuccessful\",\n verification_failed_desc: \"Something went wrong while processing your document. Please contact customer support for assistance or retry.\",\n // summary\n review_verification_details: \"Review your verification details\",\n // - form label - label_* (dont change the id_key)\n label_full_name: \"Full name\",\n // - credit card\n label_cc_number: \"Card number\",\n label_cc_number_first_6: \"First 6 Card Number\",\n label_cc_number_last_4: \"Last 4 Card Number\",\n label_doe_year: \"Expiry date\",\n label_doe_month: \"Expiry date\",\n label_date_of_expiry: \"Expiry date\",\n // - national identity document\n label_given_name: \"Given name\",\n label_surname: \"Surname\",\n label_id_number: \"Identity card number\",\n label_date_of_birth: \"Date of birth\",\n label_gender: \"Gender\",\n label_line1: \"Address\",\n label_city: \"City\",\n label_zip: \"Zip code\",\n label_country_iso2: \"Country\",\n label_state_iso: \"State/Region\",\n // - proof of address\n label_receiver_name: \"Full Name\",\n label_receiver_address: \"Address\",\n label_receiver_city: \"City\",\n label_receiver_zip: \"Zip code\",\n label_receiver_country_iso2: \"Country\",\n label_receiver_state_iso: \"State/Region\",\n complete_verification: \"Complete verification\",\n // - images\n img_label_front: \"Front\",\n img_label_back: \"Back\",\n img_label_first: \"Capture 1\",\n img_label_second: \"Capture 2\",\n // complete page\n submission_received: \"Submission has been received\",\n count_down_message: \"Redirecting you to merchant page in {seconds} seconds.\",\n return_to_desktop_message: \"You can now return to your desktop to continue.\",\n // error page\n smtg_went_wrong: \"Something went wrong\",\n fail_to_locate_verification: \"We could not locate a suitable verification type for you. Please contact customer support for assistance.\",\n contact_support: \"Please contact customer support for assistance or retry.\",\n back_to_merchant: \"Back to merchant\",\n // error message - error_* (dont change the id_key)\n error_expiry_date: \"The expiry date must be today or in the future.\",\n error_invalid_month: \"Month must be between 1 and 12.\",\n error_required: \"This field is required.\",\n error_invalid_field: \"Invalid {field}.\",\n error_min_char: \"This field requires min {n} characters length.\",\n error_max_char: \"This field requires max {n} characters length.\",\n error_between_char: \"This field requires min {min} and max {max} characters length.\",\n error_invalid_card: \"This field requires first 6 and last 4 digits.\",\n};\n\n\n//# sourceURL=webpack://PWUISDK/./src/translations/en.ts?");
4058
+ eval("\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports[\"default\"] = {\n // btn\n btn_start_now: \"Start Now\",\n btn_continue_on_mobile: \"Continue on Mobile\",\n btn_close: \"Close\",\n btn_okay: \"Okay\",\n btn_retake: \"Retake\",\n btn_proceed: \"Proceed\",\n btn_try_again: \"Try Again\",\n btn_submit: \"Submit\",\n btn_done: \"Done\",\n btn_capture_now: \"Capture now\",\n // common\n loading: \"Loading\",\n loading_dots: \"Loading...\",\n search: \"Search\",\n type_to_search: \"Type to search\",\n no_result_found: \"No result found\",\n // qr code dialog\n qr_dialog_title: \"Continue verification on mobile\",\n qr_instruction: \"Scan the QR code below using your mobile device to initiate the verification process.\",\n qr_browser_notice: \"Verification is supported only in Chrome, Firefox, and Safari (iOS). The WeChat QR Scanner is not supported.\",\n // first page\n national_identity_document_title: \"Capture ID photo\",\n national_identity_document_desc: \"Please ensure your document is clear, readable and valid.\",\n selfie_title: \"Face scan\",\n selfie_desc: \"Position your face in the frame and follow the instructions given. Do not wear sunglasses, hat or accessories. Background must be light and neutral.\",\n cc_title: \"Credit card verification\",\n cc_desc: \"\",\n poa_title: \"Utility bill verification\",\n poa_desc: \"Please ensure your document is clear, readable and valid.\",\n // national identity document\n select_id_type: \"Select ID type\",\n issuing_country: \"Issuing country/region\",\n document_type: \"Document type\",\n id_card: \"ID Card\",\n driver_license: \"Driver's License\",\n passport: \"Passport\",\n error_id_glare: \"The image has glare issues, please take a clear photo in a well-lit area.\",\n error_id_empty: \"No document detected in the image. Please ensure your document is clearly visible within the frame and properly lit.\",\n error_id_blur: \"The image is blurry. Please hold steady and take a clear photo.\",\n error_id_not_positioned: \"The document is not positioned properly. Please align it within the frame.\",\n error_id_unpair: \"The front and back images don't match. Please capture the same document.\",\n error_id_unsimilar: \"The front and back images appear to be from different documents.\",\n error_id_generic: \"Unable to verify the document. Please ensure it is clear, valid, and properly positioned.\",\n // selfie\n error_selfie: \"The image is blur, please take a clear photo.\",\n // credit card\n cc_missing_info_title: \"Missing card info.\",\n cc_missing_info_desc: \"Capture the side of the card with:\",\n // - cc_missing_content_* (dont change the id_key)\n cc_missing_content_full_name: \"Card owner's name\",\n cc_missing_content_cc_number: \"Card number\",\n cc_missing_content_cc_number_first_6: \"First 6 digits of card number\",\n cc_missing_content_cc_number_last_4: \"Last 4 digits of card number\",\n cc_missing_content_date_of_expiry: \"Expiry date\",\n cc_expired_with_retry: \"Credit card expired. Please provide a new card to continue.\",\n cc_expired_without_retry: \"Credit card expired. Please ensure your credit card is valid.\",\n error_cc_glare: \"The image has glare issues, please take a clear photo in a well-lit area.\",\n error_cc_empty: \"No card detected in the image. Please ensure your card is clearly visible within the frame and properly lit.\",\n error_cc_blur: \"The image is blurry. Please hold steady and take a clear photo.\",\n error_cc_generic: \"Unable to verify the card. Please ensure it is clear, valid, and properly positioned.\",\n // proof of address\n proof_of_address_title: \"Upload utility bill\",\n proof_of_address_desc: \"Please upload your utility bill\",\n image_format_label: \"Format: PNG, JPG, JPEG or PDF ({size}MB max)\",\n click_to_take_photo: \"Click to take photo\",\n poa_capture_or_choose: \"Capture or choose from gallery\",\n poa_doc_tip_title: \"Document tips:\",\n poa_doc_tip_1: \"Your name and address should be clearly visible.\",\n poa_doc_tip_2: \"Use PDF, JPEG, PNG files.\",\n poa_doc_tip_3: \"Max file size: 2MB.\",\n error_poa_file_missing: \"Please upload your utility bill to continue.\",\n error_poa_file_invalid: \"Only files in this extensions are allowed to be upload: PNG, JPG, JPEG or PDF ({size}MB max)\",\n error_poa_idle_long: \"The uploaded file is invalid due to you have been idle for too long.\",\n error_poa_file_generic: \"Unable to verify the document. Please ensure it is clear, valid, and properly uploaded.\",\n error_poa: \"Something went wrong. Please contact customer support for assistance or retry.\",\n // camera instruction\n id_front_title: \"ID Card | Front\",\n id_front_outline: \"Front of ID\",\n id_back_title: \"ID Card | Back\",\n id_back_outline: \"Back of ID\",\n license_front_title: \"License | Front\",\n license_back_title: \"License | Back\",\n passport_title: \"Passport\",\n cc_front_title: \"Credit Card\",\n cc_back_title: \"Credit Card\",\n camera_desc: \"Place your document within the frame and take photo.\",\n id_camera_dec: \"Please capture the front of your ID card, where your photo is visible. Ensure all details are clear.\",\n idle_message: \"You have been idle for too long.\",\n tips: \"Tips\",\n card: \"Card\",\n id_tip_1: \"Place your {cardType} within the frame and take picture.\",\n id_tip_2: \"Make sure the photo is not blur or cropped.\",\n id_tip_3: \"Avoid reflection and glare.\",\n selfie_tip_1: \"Position your face within the frame and follow the instructions given.\",\n selfie_tip_2: \"Make sure your face is not blur or cropped.\",\n selfie_tip_3: \"Do not wear sunglasses, hat or accessories.\",\n selfie_tip_4: \"Background must be light and neutral.\",\n cc_tip_title: \"Please show the info below:\",\n cc_tip_1: \"Card owner's name\",\n cc_tip_2: \"First 6 and last 4 digits ONLY\",\n cc_tip_3: \"Expiry date\",\n cc_tip_bottom_1: \"Some credit/debit cards may require capturing both front and back to obtain all mandatory information.\",\n cc_tip_bottom_2: \"You may cover the middle digits and CVV on the card manually or cover it later.\",\n flip_other_side: \"Flip the other side\",\n doc_align_card: \"Align your card within the frame\",\n doc_hold_steady: \"Hold steady…\",\n face_smile_at_camera: \"Please smile at the camera.\",\n face_smile_seconds: \"Please keep smiling for 3 seconds.\",\n face_neutral_expression: \"Please show a neutral expression at the camera.\",\n face_looks_straight: \"Please look straight at the camera.\",\n face_within_frame: \"Place your face within the rounded frame.\",\n face_too_far: \"Move closer to the camera.\",\n face_too_close: \"Move further from the camera.\",\n face_not_centered: \"Center your face in the frame.\",\n card_masking_title: \"Hide your info\",\n card_masking_desc: \"Please cover the middle digits and CVV on the card.\",\n // submission limit\n submission_limit_title: \"Submission limit reached\",\n submission_limit_desc: \"Your submission couldn't be processed as it has reached the maximum number of attempts.\",\n // analyze page\n analyze_title: \"Analyzing the pictures\",\n analyze_desc: \"Please do not close your browser, this may take a moment.\",\n error_analyze: \"Unable to verify your documents. Please check your documents and resubmit again.\",\n verification_failed: \"Verification unsuccessful\",\n verification_failed_desc: \"Something went wrong while processing your document. Please contact customer support for assistance or retry.\",\n // summary\n review_verification_details: \"Review your verification details\",\n // - form label - label_* (dont change the id_key)\n label_full_name: \"Full name\",\n // - credit card\n label_cc_number: \"Card number\",\n label_cc_number_first_6: \"First 6 Card Number\",\n label_cc_number_last_4: \"Last 4 Card Number\",\n label_doe_year: \"Expiry date\",\n label_doe_month: \"Expiry date\",\n label_date_of_expiry: \"Expiry date\",\n // - national identity document\n label_given_name: \"Given name\",\n label_surname: \"Surname\",\n label_id_number: \"Identity card number\",\n label_date_of_birth: \"Date of birth\",\n label_gender: \"Gender\",\n label_line1: \"Address\",\n label_city: \"City\",\n label_zip: \"Zip code\",\n label_country_iso2: \"Country\",\n label_state_iso: \"State/Region\",\n // - proof of address\n label_receiver_name: \"Full Name\",\n label_receiver_address: \"Address\",\n label_receiver_city: \"City\",\n label_receiver_zip: \"Zip code\",\n label_receiver_country_iso2: \"Country\",\n label_receiver_state_iso: \"State/Region\",\n complete_verification: \"Complete verification\",\n // - images\n img_label_front: \"Front\",\n img_label_back: \"Back\",\n img_label_first: \"Capture 1\",\n img_label_second: \"Capture 2\",\n // complete page\n submission_received: \"Submission has been received\",\n count_down_message: \"Redirecting you to merchant page in {seconds} seconds.\",\n return_to_desktop_message: \"You can now return to your desktop to continue.\",\n // error page\n smtg_went_wrong: \"Something went wrong\",\n fail_to_locate_verification: \"We could not locate a suitable verification type for you. Please contact customer support for assistance.\",\n contact_support: \"Please contact customer support for assistance or retry.\",\n back_to_merchant: \"Back to merchant\",\n // error message - error_* (dont change the id_key)\n error_expiry_date: \"The expiry date must be today or in the future.\",\n error_invalid_month: \"Month must be between 1 and 12.\",\n error_required: \"This field is required.\",\n error_invalid_field: \"Invalid {field}.\",\n error_min_char: \"This field requires min {n} characters length.\",\n error_max_char: \"This field requires max {n} characters length.\",\n error_between_char: \"This field requires min {min} and max {max} characters length.\",\n error_invalid_card: \"This field requires first 6 and last 4 digits.\",\n};\n\n\n//# sourceURL=webpack://PWUISDK/./src/translations/en.ts?");
4059
4059
 
4060
4060
  /***/ }),
4061
4061
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipwave-ekyc-uikit",
3
- "version": "3.0.6-beta.4",
3
+ "version": "3.1.0",
4
4
  "description": "",
5
5
  "main": "dist/pw-bundle.js",
6
6
  "scripts": {