pipwave-ekyc-uikit 3.1.1 → 3.1.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.production CHANGED
@@ -1,3 +1,3 @@
1
1
  FACE_MODEL=https://assets.pipwave.com/ekyc/sdk/face-models
2
2
  CARD_MODEL=https://assets.pipwave.com/ekyc/sdk/card-models
3
- ORT=https://assets.pipwave.com/ekyc/sdk/ort
3
+ ORT=https://staging-assets.pipwave.com/ekyc/sdk/ort
package/dist/pw-bundle.js CHANGED
@@ -4000,7 +4000,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4000
4000
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
4001
4001
 
4002
4002
  "use strict";
4003
- 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 card_detection_1 = __importDefault(__webpack_require__(/*! ./card-detection */ \"./src/plugins/card-detection.ts\"));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst camera_instruction_1 = __webpack_require__(/*! ../constant/camera-instruction */ \"./src/constant/camera-instruction.ts\");\nconst baseDialog_1 = __importDefault(__webpack_require__(/*! ../components/baseDialog */ \"./src/components/baseDialog.ts\"));\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst navigationBar_1 = __importDefault(__webpack_require__(/*! ../components/navigationBar */ \"./src/components/navigationBar.ts\"));\nconst cameraViewport_1 = __importDefault(__webpack_require__(/*! ../components/cameraViewport */ \"./src/components/cameraViewport.ts\"));\nconst footer_1 = __importDefault(__webpack_require__(/*! ../components/footer */ \"./src/components/footer.ts\"));\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst imagesHelper_1 = __webpack_require__(/*! ../helper/imagesHelper */ \"./src/helper/imagesHelper.ts\");\nclass CardCamera {\n constructor(cameraId, cardType, mainScreenId = \"\") {\n this.cameraTimeLimit = 90000;\n this.holdRafId = null;\n this.hasAutoCaptured = false;\n this.isFramed = false;\n this.holdProgress = 0;\n this.lastHoldTick = 0;\n this.pendingCapture = null;\n this.tickTimer = null;\n this._onWindowResize = () => { this.resizeElement(); };\n this._onBeforeUnload = () => { this._cleanup(); };\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.footerElement = null;\n this.cameraId = cameraId;\n this.cardType = cardType;\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, _g;\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 + manual scale\n const { width, height } = this.cardDetectionSdk.calculateHeightNWidth(bitmap.videoWidth, bitmap.videoHeight);\n cropW = Math.round(width * scaleX);\n cropH = Math.round(height * scaleY);\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 // crop the selected part image (viewport frame region)\n const imgData = cropContext.getImageData(cropX, cropY, cropW, cropH);\n cropCanvas.width = cropW;\n cropCanvas.height = cropH;\n cropContext.putImageData(imgData, 0, 0);\n // Determine card type prefix for file naming\n const cardTypePrefix = this.cardType === camera_1.CARD_TYPES.ID ? 'id-front' :\n this.cardType === camera_1.CARD_TYPES.ID_BACK ? 'id-back' :\n this.cardType === camera_1.CARD_TYPES.LICENSE ? 'license-front' :\n this.cardType === camera_1.CARD_TYPES.LICENSE_BACK ? 'license-back' :\n this.cardType === camera_1.CARD_TYPES.PASSPORT ? 'passport' :\n this.cardType === camera_1.CARD_TYPES.CC ? 'cc-front' : 'cc-back';\n // TEMP: Download BEFORE resize for comparison (only if debug enabled)\n if (MainPage_1.enableImageDebug) {\n const beforeResizeFullCanvas = canvas.cloneNode();\n const beforeResizeFullCtx = beforeResizeFullCanvas.getContext(\"2d\");\n beforeResizeFullCanvas.width = canvas.width;\n beforeResizeFullCanvas.height = canvas.height;\n beforeResizeFullCtx.drawImage(canvas, 0, 0);\n const beforeResizeCropCanvas = cropCanvas.cloneNode();\n const beforeResizeCropCtx = beforeResizeCropCanvas.getContext(\"2d\");\n beforeResizeCropCanvas.width = cropCanvas.width;\n beforeResizeCropCanvas.height = cropCanvas.height;\n beforeResizeCropCtx.drawImage(cropCanvas, 0, 0);\n console.log(`[DEBUG] ${cardTypePrefix} BEFORE resize - Full: ${beforeResizeFullCanvas.width}x${beforeResizeFullCanvas.height}, Cropped: ${beforeResizeCropCanvas.width}x${beforeResizeCropCanvas.height}`);\n this.downloadImage(beforeResizeFullCanvas.toDataURL(\"image/jpeg\"), `${cardTypePrefix}-full-BEFORE-${beforeResizeFullCanvas.width}x${beforeResizeFullCanvas.height}.jpg`);\n this.downloadImage(beforeResizeCropCanvas.toDataURL(\"image/jpeg\"), `${cardTypePrefix}-cropped-BEFORE-${beforeResizeCropCanvas.width}x${beforeResizeCropCanvas.height}.jpg`);\n }\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(\"doc resize image error\", err);\n }\n // 4. construct payload - convert to desired file format\n const { width: displayWidth = \"\", height: displayHeight = \"\" } = ((_g = this.overlayElement) === null || _g === void 0 ? void 0 : _g.style) || {};\n const payload = {\n fullImage: canvas.toDataURL(\"image/jpeg\"),\n croppedImage: cropCanvas.toDataURL(\"image/jpeg\"),\n displaySize: {\n width: Number(displayWidth.replace(\"px\", \"\")),\n height: Number(displayHeight.replace(\"px\", \"\")),\n },\n };\n // TEMP: Download AFTER resize for comparison (only if debug enabled)\n if (MainPage_1.enableImageDebug) {\n console.log(`[DEBUG] ${cardTypePrefix} AFTER resize - Full: ${canvas.width}x${canvas.height}, Cropped: ${cropCanvas.width}x${cropCanvas.height}`);\n if (payload.fullImage)\n this.downloadImage(payload.fullImage, `${cardTypePrefix}-full-AFTER-${canvas.width}x${canvas.height}.jpg`);\n if (payload.croppedImage)\n this.downloadImage(payload.croppedImage, `${cardTypePrefix}-cropped-AFTER-${cropCanvas.width}x${cropCanvas.height}.jpg`);\n }\n return payload;\n });\n }\n // TEMP: Helper method to download image\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 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.cardDetectionSdk = new card_detection_1.default({\n videoElement,\n canvasElement,\n overlayElement,\n titleElement,\n });\n // 3. Get the camera permission and trigger the card detection\n this.cameraInit();\n // 4. Start timer to close camera\n this.timer = setTimeout(() => {\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 // hide loading spinner in full pg\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n }\n drawCameraDisplay() {\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 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 // Use CameraViewport component for consistent viewport design\n const cameraViewport = new cameraViewport_1.default(this.cardType);\n const overlayElement = cameraViewport.render();\n overlayElement.classList.add(\"pw-relative\");\n // Container for title and description - positioned above the overlay\n const headerContainer = document.createElement(\"div\");\n headerContainer.classList.add(\"pw-absolute\");\n headerContainer.style.zIndex = \"1\";\n headerContainer.style.left = \"50%\";\n headerContainer.style.bottom = \"100%\";\n headerContainer.style.transform = \"translate(-50%, -20px)\";\n headerContainer.style.width = \"calc(100vw - 48px)\";\n headerContainer.style.maxWidth = \"1400px\";\n // Set Title\n const titleDiv = document.createElement(\"div\");\n titleDiv.id = \"pw-camera-title\";\n titleDiv.classList.add(\"pw-text-2xl\", \"pw-text-center\", \"pw-text-white\", \"pw-mb-4\");\n titleDiv.innerHTML = camera_instruction_1.CAMERA_INSTRUCTION[this.cardType].title;\n this.titleElement = titleDiv;\n headerContainer.appendChild(titleDiv);\n // Set Description\n const descriptionDiv = document.createElement(\"div\");\n descriptionDiv.classList.add(\"pw-text-center\", \"pw-px-5\", \"pw-text-xl\", \"pw-text-white\");\n descriptionDiv.innerHTML = camera_instruction_1.CAMERA_INSTRUCTION[this.cardType].description;\n headerContainer.appendChild(descriptionDiv);\n overlayElement.appendChild(headerContainer);\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 // Mirror canvas to match video when not on mobile device (desktop/laptop)\n if (!(0, deviceDetection_1.isMobileDevice)()) {\n canvasElement.style.transform = \"scaleX(-1)\";\n }\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 // Mirror horizontally when not on mobile device (desktop/laptop front camera UX)\n if (!(0, deviceDetection_1.isMobileDevice)()) {\n videoElement.style.transform = \"scaleX(-1)\";\n }\n // 2.1\n this.videoElement = videoElement;\n this.overlayElement = overlayElement;\n this.canvasElement = canvasElement;\n overlayContainer.appendChild(overlayElement);\n videoContainer.appendChild(overlayContainer);\n videoContainer.appendChild(canvasElement);\n videoContainer.appendChild(videoElement);\n body.appendChild(videoContainer);\n // 3. append child\n const mainBody = document.getElementById(this.cameraId);\n if (!mainBody)\n return;\n mainBody.classList.add(\"camera-bg\", \"pw-overflow-y-hidden\");\n mainBody.appendChild(header);\n mainBody.appendChild(body);\n }\n listenToCardCapture(cb) {\n const cameraBody = document.getElementById(this.cameraId);\n cameraBody === null || cameraBody === void 0 ? void 0 : cameraBody.addEventListener(\"captureImage\", (e) => {\n cb(e);\n });\n }\n // here also is where the take picture button resides\n renderCameraBottomUi() {\n const footer = document.createElement(\"div\");\n footer.id = camera_1.ELEMENT_IDS.FOOTER;\n footer.classList.add(\"pw-absolute\", \"pw-bottom-[10%]\", \"pw-z-[2]\", \"pw-w-full\", \"pw-text-white\", \"pw-relative\");\n const hint = document.createElement(\"div\");\n hint.id = \"pw-doc-hint\";\n hint.classList.add(\"pw-absolute\", \"pw-bottom-[9.5rem]\", \"pw-w-full\", \"pw-text-center\", \"pw-text-lg\", \"pw-text-white\");\n hint.innerHTML = (0, translations_1.$t)(\"doc_align_card\");\n footer.appendChild(hint);\n const takePicButton = document.createElement(\"div\");\n takePicButton.id = \"takePicButton\";\n takePicButton.classList.add(\"pw-box-border\", \"pw-mx-auto\", \"pw-absolute\", \"pw-bottom-16\", \"pw-relative\", \"pw-rounded-full\", \"pw-h-16\", \"pw-w-16\", \"pw-border-solid\", \"pw-border-white\", \"pw-border-4\", \"pw-pointer-events-none\");\n // SVG progress ring sitting on top of the white border circle. \n // The progress arc is animated from empty -> full over AUTO_CAPTURE_HOLD_MS.\n const svgNS = \"http://www.w3.org/2000/svg\";\n const ringSvg = document.createElementNS(svgNS, \"svg\");\n ringSvg.setAttribute(\"viewBox\", \"0 0 64 64\");\n ringSvg.classList.add(\"pw-absolute\", \"pw-inset-0\", \"pw-w-full\", \"pw-h-full\");\n ringSvg.style.transform = \"rotate(-90deg)\";\n const progressCircle = document.createElementNS(svgNS, \"circle\");\n progressCircle.id = \"takePicProgress\";\n progressCircle.setAttribute(\"cx\", \"32\");\n progressCircle.setAttribute(\"cy\", \"32\");\n progressCircle.setAttribute(\"r\", `${CardCamera.RING_RADIUS}`);\n progressCircle.setAttribute(\"fill\", \"none\");\n progressCircle.setAttribute(\"stroke\", \"#ffffff\");\n progressCircle.setAttribute(\"stroke-width\", \"6\");\n progressCircle.setAttribute(\"stroke-linecap\", \"round\");\n progressCircle.setAttribute(\"stroke-dasharray\", `${CardCamera.RING_CIRCUMFERENCE}`);\n progressCircle.setAttribute(\"stroke-dashoffset\", `${CardCamera.RING_CIRCUMFERENCE}`);\n progressCircle.style.transition = \"stroke-dashoffset 80ms linear, stroke 150ms linear\";\n ringSvg.appendChild(progressCircle);\n takePicButton.appendChild(ringSvg);\n const smallCircleContainer = document.createElement(\"div\");\n smallCircleContainer.classList.add(\"pw-absolute\", \"pw-top-1/2\", \"-pw-translate-y-1/2\", \"pw-left-1/2\", \"-pw-translate-x-1/2\");\n takePicButton.appendChild(smallCircleContainer);\n const smallCircle = document.createElement(\"div\");\n smallCircle.id = \"takePicInner\";\n smallCircle.classList.add(\"pw-w-11\", \"pw-h-11\", \"pw-bg-white\", \"pw-rounded-full\", \"pw-opacity-60\");\n smallCircleContainer.appendChild(smallCircle);\n footer.appendChild(takePicButton);\n const mainBody = document.getElementById(this.cameraId);\n this.footerElement = footer;\n mainBody === null || mainBody === void 0 ? void 0 : mainBody.appendChild(footer);\n // Powered by pipwave at bottom of viewport\n const poweredByWrapper = document.createElement(\"div\");\n poweredByWrapper.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 poweredByFooter = new footer_1.default().render(true);\n poweredByWrapper.appendChild(poweredByFooter);\n mainBody === null || mainBody === void 0 ? void 0 : mainBody.appendChild(poweredByWrapper);\n }\n pauseVideoStream() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.stopDetectionLoop();\n this.cancelHold();\n // stop the video\n (_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.pause();\n // stop idle timeout\n if (this.timer) {\n clearTimeout(this.timer);\n }\n });\n }\n resumeVideoStream(cardType) {\n var _a;\n return __awaiter(this, void 0, void 0, function* () {\n // resume the video first so detection grabs live frames\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.play();\n // updateType re-arms auto-capture and (re)starts the detection loop for this side\n this.updateType(cardType);\n // start idle timeout\n this.timer = setTimeout(() => {\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 (0, components_1.hideFullLoading)(this.cameraId);\n });\n }\n stopVideoStream() {\n this._cleanup();\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 _cleanup() {\n var _a;\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.dispose();\n if (this.videoElement && this.videoElement.srcObject) {\n const stream = this.videoElement.srcObject;\n stream.getTracks().forEach(t => t.stop());\n this.videoElement.srcObject = null;\n }\n if (this.timer)\n clearTimeout(this.timer);\n window.removeEventListener(\"resize\", this._onWindowResize);\n window.removeEventListener(\"beforeunload\", this._onBeforeUnload);\n }\n updateType(cardType) {\n this.cardType = cardType;\n // New side (card flipped to back) or resumed session — re-arm auto-capture.\n this.hasAutoCaptured = false;\n this.cancelHold();\n // update title\n const title = document.getElementById(\"pw-camera-title\");\n if (title)\n title.innerHTML = camera_instruction_1.CAMERA_INSTRUCTION[this.cardType].title;\n // triggerAutoCapture stops the loop on every capture, so (re)start it for this\n // side. The flip flow (VerifyId) only calls updateType — not resumeVideoStream —\n // so this is what brings detection back for the back of the card.\n this.startDetection();\n }\n startDetection() {\n var _a;\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.startDetectionLoop((framed) => this.onFramedChange(framed));\n }\n loadVideoStream(videoElement) {\n const initialConstraints = {\n facingMode: \"environment\",\n width: {\n ideal: 1280, // 1280, 1024, 640\n },\n height: {\n ideal: 960, // 960, 768, 480\n },\n video: true,\n };\n // Mirror video when not on mobile device (desktop/laptop front camera UX)\n if (!(0, deviceDetection_1.isMobileDevice)()) {\n videoElement.style.transform = \"scaleX(-1)\";\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 })\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 // Fire model load concurrently — typically ready by the time video starts.\n this.cardDetectionSdk.loadModel();\n // video event listeners\n this.videoElement.addEventListener(\"loadedmetadata\", () => {\n var _a;\n this.cardDetectionSdk.init(this.cardType);\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.classList.remove(\"pw-hidden\");\n // Set initial video dimensions based on orientation\n this.resizeElement();\n this.startDetection();\n });\n this.videoElement.addEventListener(\"resize\", () => {\n const isFooterExist = document.getElementById(camera_1.ELEMENT_IDS.FOOTER);\n if (!isFooterExist) {\n this.renderCameraBottomUi();\n }\n this.resizeElement();\n });\n window.addEventListener(\"resize\", this._onWindowResize);\n window.addEventListener(\"beforeunload\", this._onBeforeUnload);\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 // Driven by the card-detection loop. \n onFramedChange(framed) {\n this.isFramed = framed;\n if (this.hasAutoCaptured)\n return;\n if (framed) {\n this.setHint((0, translations_1.$t)(\"doc_hold_steady\"));\n this.ensureHoldLoop();\n }\n else {\n // Lost framing: empty the ring instantly and restart — no slow drain.\n this.cancelHold();\n }\n }\n // Single rAF loop: progress climbs while framed. Secures the frame at the midpoint\n // and captures once it hits 1. Draining is handled instantly by onFramedChange.\n ensureHoldLoop() {\n if (this.holdRafId !== null || this.hasAutoCaptured)\n return;\n this.lastHoldTick = performance.now();\n const tick = (now) => {\n const dt = now - this.lastHoldTick;\n this.lastHoldTick = now;\n if (!this.isFramed) {\n this.holdRafId = null;\n return;\n }\n this.holdProgress = Math.min(1, this.holdProgress + dt / CardCamera.AUTO_CAPTURE_HOLD_MS);\n this.setRingProgress(this.holdProgress);\n if (this.holdProgress >= CardCamera.AUTO_CAPTURE_GRAB_AT && this.pendingCapture === null) {\n this.pendingCapture = this.captureImage();\n if (MainPage_1.enableImageDebug)\n console.log(`[DEBUG] ${this.cardType} frame secured at midpoint`);\n }\n if (this.holdProgress >= 1) {\n this.holdRafId = null;\n this.triggerAutoCapture();\n return;\n }\n this.holdRafId = requestAnimationFrame(tick);\n };\n this.holdRafId = requestAnimationFrame(tick);\n }\n cancelHold() {\n if (this.holdRafId !== null) {\n cancelAnimationFrame(this.holdRafId);\n this.holdRafId = null;\n }\n this.isFramed = false;\n this.holdProgress = 0;\n this.pendingCapture = null;\n this.setRingProgress(0);\n this.hideCaptureTick();\n if (!this.hasAutoCaptured)\n this.setHint((0, translations_1.$t)(\"doc_align_card\"));\n }\n triggerAutoCapture() {\n var _a;\n return __awaiter(this, void 0, void 0, function* () {\n if (this.hasAutoCaptured || this.pendingCapture === null)\n return;\n this.hasAutoCaptured = true;\n // Commit only the frame secured at the midpoint — never a fresh (possibly blurry) grab.\n const capture = this.pendingCapture;\n this.pendingCapture = null;\n // Stop inference immediately — no point running the model (and flickering the\n // border over a frozen frame) in the window before pause/flip kicks in.\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.stopDetectionLoop();\n this.setRingProgress(1);\n const mainBody = document.getElementById(this.cameraId);\n const image = yield capture;\n if (MainPage_1.enableImageDebug)\n console.log(`[DEBUG] ${this.cardType} image captured (committed at success)`);\n // Frame is secured — now confirm success to the user.\n this.showCaptureTick();\n const event = new CustomEvent(\"captureImage\", {\n detail: {\n image,\n type: this.cardType,\n },\n });\n yield new Promise((resolve) => setTimeout(resolve, CardCamera.CAPTURE_TICK_MS));\n mainBody === null || mainBody === void 0 ? void 0 : mainBody.dispatchEvent(event);\n });\n }\n // progress 0 -> empty ring, 1 -> full ring\n setRingProgress(progress) {\n const ring = document.getElementById(\"takePicProgress\");\n if (!ring)\n return;\n ring.setAttribute(\"stroke\", CardCamera.RING_FILL_COLOR);\n const offset = CardCamera.RING_CIRCUMFERENCE * (1 - progress);\n ring.setAttribute(\"stroke-dashoffset\", `${offset}`);\n }\n // Briefly flash a green checkmark inside the capture button to confirm a\n // successful capture, then clear it so the button is clean for the next side.\n showCaptureTick() {\n var _a, _b;\n if (document.getElementById(\"takePicTick\"))\n return;\n const container = (_a = document.getElementById(\"takePicInner\")) === null || _a === void 0 ? void 0 : _a.parentElement;\n if (!container)\n return;\n // White backing pops once the tick lands.\n (_b = document\n .getElementById(\"takePicInner\")) === null || _b === void 0 ? void 0 : _b.classList.replace(\"pw-opacity-60\", \"pw-opacity-100\");\n const svgNS = \"http://www.w3.org/2000/svg\";\n const tick = document.createElementNS(svgNS, \"svg\");\n tick.id = \"takePicTick\";\n tick.setAttribute(\"viewBox\", \"0 0 24 24\");\n tick.setAttribute(\"width\", \"34\");\n tick.setAttribute(\"height\", \"34\");\n tick.classList.add(\"pw-absolute\", \"pw-top-1/2\", \"-pw-translate-y-1/2\", \"pw-left-1/2\", \"-pw-translate-x-1/2\");\n const path = document.createElementNS(svgNS, \"path\");\n path.setAttribute(\"d\", \"M5 13l4 4L19 7\");\n path.setAttribute(\"fill\", \"none\");\n path.setAttribute(\"stroke\", CardCamera.RING_FILL_COLOR);\n path.setAttribute(\"stroke-width\", \"3\");\n path.setAttribute(\"stroke-linecap\", \"round\");\n path.setAttribute(\"stroke-linejoin\", \"round\");\n tick.appendChild(path);\n container.appendChild(tick);\n if (this.tickTimer)\n clearTimeout(this.tickTimer);\n this.tickTimer = setTimeout(() => this.hideCaptureTick(), CardCamera.CAPTURE_TICK_MS);\n }\n hideCaptureTick() {\n var _a, _b;\n if (this.tickTimer) {\n clearTimeout(this.tickTimer);\n this.tickTimer = null;\n }\n (_a = document.getElementById(\"takePicTick\")) === null || _a === void 0 ? void 0 : _a.remove();\n (_b = document\n .getElementById(\"takePicInner\")) === null || _b === void 0 ? void 0 : _b.classList.replace(\"pw-opacity-100\", \"pw-opacity-60\");\n }\n setHint(text) {\n const hintEl = document.getElementById(\"pw-doc-hint\");\n if (hintEl)\n hintEl.innerHTML = text;\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 containerH = vpH; // viewport wider than video — constrain by height\n containerW = vpH * videoAspect;\n }\n else {\n containerW = vpW; // viewport taller than video — constrain by width\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.cardDetectionSdk.resizeElement();\n }\n}\nCardCamera.AUTO_CAPTURE_HOLD_MS = 1500; // framed time to fill the ring\nCardCamera.AUTO_CAPTURE_GRAB_AT = 0.5; // fill ratio at which we secure the frame\nCardCamera.RING_RADIUS = 29;\nCardCamera.RING_CIRCUMFERENCE = 2 * Math.PI * CardCamera.RING_RADIUS;\nCardCamera.RING_FILL_COLOR = \"#16A34A\";\nCardCamera.CAPTURE_TICK_MS = 800;\nexports[\"default\"] = CardCamera;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/card-camera.ts?");
4003
+ 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 card_detection_1 = __importDefault(__webpack_require__(/*! ./card-detection */ \"./src/plugins/card-detection.ts\"));\nconst translations_1 = __webpack_require__(/*! ../translations */ \"./src/translations/index.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst camera_instruction_1 = __webpack_require__(/*! ../constant/camera-instruction */ \"./src/constant/camera-instruction.ts\");\nconst baseDialog_1 = __importDefault(__webpack_require__(/*! ../components/baseDialog */ \"./src/components/baseDialog.ts\"));\nconst components_1 = __webpack_require__(/*! ../components/components */ \"./src/components/components.ts\");\nconst navigationBar_1 = __importDefault(__webpack_require__(/*! ../components/navigationBar */ \"./src/components/navigationBar.ts\"));\nconst cameraViewport_1 = __importDefault(__webpack_require__(/*! ../components/cameraViewport */ \"./src/components/cameraViewport.ts\"));\nconst footer_1 = __importDefault(__webpack_require__(/*! ../components/footer */ \"./src/components/footer.ts\"));\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst imagesHelper_1 = __webpack_require__(/*! ../helper/imagesHelper */ \"./src/helper/imagesHelper.ts\");\nclass CardCamera {\n constructor(cameraId, cardType, mainScreenId = \"\") {\n this.cameraTimeLimit = 90000;\n this.holdRafId = null;\n this.hasAutoCaptured = false;\n this.isFramed = false;\n this.holdProgress = 0;\n this.lastHoldTick = 0;\n this.pendingCapture = null;\n this.tickTimer = null;\n this._onWindowResize = () => { this.resizeElement(); };\n this._onBeforeUnload = () => { this._cleanup(); };\n this._manualClickHandler = null;\n this.scannerUnavailableNotified = false;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n this.footerElement = null;\n this.cameraId = cameraId;\n this.cardType = cardType;\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, _g;\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 + manual scale\n const { width, height } = this.cardDetectionSdk.calculateHeightNWidth(bitmap.videoWidth, bitmap.videoHeight);\n cropW = Math.round(width * scaleX);\n cropH = Math.round(height * scaleY);\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 // crop the selected part image (viewport frame region)\n const imgData = cropContext.getImageData(cropX, cropY, cropW, cropH);\n cropCanvas.width = cropW;\n cropCanvas.height = cropH;\n cropContext.putImageData(imgData, 0, 0);\n // Determine card type prefix for file naming\n const cardTypePrefix = this.cardType === camera_1.CARD_TYPES.ID ? 'id-front' :\n this.cardType === camera_1.CARD_TYPES.ID_BACK ? 'id-back' :\n this.cardType === camera_1.CARD_TYPES.LICENSE ? 'license-front' :\n this.cardType === camera_1.CARD_TYPES.LICENSE_BACK ? 'license-back' :\n this.cardType === camera_1.CARD_TYPES.PASSPORT ? 'passport' :\n this.cardType === camera_1.CARD_TYPES.CC ? 'cc-front' : 'cc-back';\n // TEMP: Download BEFORE resize for comparison (only if debug enabled)\n if (MainPage_1.enableImageDebug) {\n const beforeResizeFullCanvas = canvas.cloneNode();\n const beforeResizeFullCtx = beforeResizeFullCanvas.getContext(\"2d\");\n beforeResizeFullCanvas.width = canvas.width;\n beforeResizeFullCanvas.height = canvas.height;\n beforeResizeFullCtx.drawImage(canvas, 0, 0);\n const beforeResizeCropCanvas = cropCanvas.cloneNode();\n const beforeResizeCropCtx = beforeResizeCropCanvas.getContext(\"2d\");\n beforeResizeCropCanvas.width = cropCanvas.width;\n beforeResizeCropCanvas.height = cropCanvas.height;\n beforeResizeCropCtx.drawImage(cropCanvas, 0, 0);\n console.log(`[DEBUG] ${cardTypePrefix} BEFORE resize - Full: ${beforeResizeFullCanvas.width}x${beforeResizeFullCanvas.height}, Cropped: ${beforeResizeCropCanvas.width}x${beforeResizeCropCanvas.height}`);\n this.downloadImage(beforeResizeFullCanvas.toDataURL(\"image/jpeg\"), `${cardTypePrefix}-full-BEFORE-${beforeResizeFullCanvas.width}x${beforeResizeFullCanvas.height}.jpg`);\n this.downloadImage(beforeResizeCropCanvas.toDataURL(\"image/jpeg\"), `${cardTypePrefix}-cropped-BEFORE-${beforeResizeCropCanvas.width}x${beforeResizeCropCanvas.height}.jpg`);\n }\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(\"doc resize image error\", err);\n }\n // 4. construct payload - convert to desired file format\n const { width: displayWidth = \"\", height: displayHeight = \"\" } = ((_g = this.overlayElement) === null || _g === void 0 ? void 0 : _g.style) || {};\n const payload = {\n fullImage: canvas.toDataURL(\"image/jpeg\"),\n croppedImage: cropCanvas.toDataURL(\"image/jpeg\"),\n displaySize: {\n width: Number(displayWidth.replace(\"px\", \"\")),\n height: Number(displayHeight.replace(\"px\", \"\")),\n },\n };\n // TEMP: Download AFTER resize for comparison (only if debug enabled)\n if (MainPage_1.enableImageDebug) {\n console.log(`[DEBUG] ${cardTypePrefix} AFTER resize - Full: ${canvas.width}x${canvas.height}, Cropped: ${cropCanvas.width}x${cropCanvas.height}`);\n if (payload.fullImage)\n this.downloadImage(payload.fullImage, `${cardTypePrefix}-full-AFTER-${canvas.width}x${canvas.height}.jpg`);\n if (payload.croppedImage)\n this.downloadImage(payload.croppedImage, `${cardTypePrefix}-cropped-AFTER-${cropCanvas.width}x${cropCanvas.height}.jpg`);\n }\n return payload;\n });\n }\n // TEMP: Helper method to download image\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 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.cardDetectionSdk = new card_detection_1.default({\n videoElement,\n canvasElement,\n overlayElement,\n titleElement,\n });\n // 3. Get the camera permission and trigger the card detection\n this.cameraInit();\n // 4. Start timer to close camera\n this.timer = setTimeout(() => {\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 // hide loading spinner in full pg\n (0, components_1.hideFullLoading)(this.cameraId);\n }\n }\n drawCameraDisplay() {\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 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 // Use CameraViewport component for consistent viewport design\n const cameraViewport = new cameraViewport_1.default(this.cardType);\n const overlayElement = cameraViewport.render();\n overlayElement.classList.add(\"pw-relative\");\n // Container for title and description - positioned above the overlay\n const headerContainer = document.createElement(\"div\");\n headerContainer.classList.add(\"pw-absolute\");\n headerContainer.style.zIndex = \"1\";\n headerContainer.style.left = \"50%\";\n headerContainer.style.bottom = \"100%\";\n headerContainer.style.transform = \"translate(-50%, -20px)\";\n headerContainer.style.width = \"calc(100vw - 48px)\";\n headerContainer.style.maxWidth = \"1400px\";\n // Set Title\n const titleDiv = document.createElement(\"div\");\n titleDiv.id = \"pw-camera-title\";\n titleDiv.classList.add(\"pw-text-2xl\", \"pw-text-center\", \"pw-text-white\", \"pw-mb-4\");\n titleDiv.innerHTML = camera_instruction_1.CAMERA_INSTRUCTION[this.cardType].title;\n this.titleElement = titleDiv;\n headerContainer.appendChild(titleDiv);\n // Set Description\n const descriptionDiv = document.createElement(\"div\");\n descriptionDiv.classList.add(\"pw-text-center\", \"pw-px-5\", \"pw-text-xl\", \"pw-text-white\");\n descriptionDiv.innerHTML = camera_instruction_1.CAMERA_INSTRUCTION[this.cardType].description;\n headerContainer.appendChild(descriptionDiv);\n overlayElement.appendChild(headerContainer);\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 // Mirror canvas to match video when not on mobile device (desktop/laptop)\n if (!(0, deviceDetection_1.isMobileDevice)()) {\n canvasElement.style.transform = \"scaleX(-1)\";\n }\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 // Mirror horizontally when not on mobile device (desktop/laptop front camera UX)\n if (!(0, deviceDetection_1.isMobileDevice)()) {\n videoElement.style.transform = \"scaleX(-1)\";\n }\n // 2.1\n this.videoElement = videoElement;\n this.overlayElement = overlayElement;\n this.canvasElement = canvasElement;\n overlayContainer.appendChild(overlayElement);\n videoContainer.appendChild(overlayContainer);\n videoContainer.appendChild(canvasElement);\n videoContainer.appendChild(videoElement);\n body.appendChild(videoContainer);\n // 3. append child\n const mainBody = document.getElementById(this.cameraId);\n if (!mainBody)\n return;\n mainBody.classList.add(\"camera-bg\", \"pw-overflow-y-hidden\");\n mainBody.appendChild(header);\n mainBody.appendChild(body);\n }\n listenToCardCapture(cb) {\n const cameraBody = document.getElementById(this.cameraId);\n cameraBody === null || cameraBody === void 0 ? void 0 : cameraBody.addEventListener(\"captureImage\", (e) => {\n cb(e);\n });\n }\n // here also is where the take picture button resides\n renderCameraBottomUi() {\n const footer = document.createElement(\"div\");\n footer.id = camera_1.ELEMENT_IDS.FOOTER;\n footer.classList.add(\"pw-absolute\", \"pw-bottom-[10%]\", \"pw-z-[2]\", \"pw-w-full\", \"pw-text-white\", \"pw-relative\");\n const hint = document.createElement(\"div\");\n hint.id = \"pw-doc-hint\";\n hint.classList.add(\"pw-absolute\", \"pw-bottom-[9.5rem]\", \"pw-w-full\", \"pw-text-center\", \"pw-text-lg\", \"pw-text-white\");\n hint.innerHTML = (0, translations_1.$t)(\"doc_align_card\");\n footer.appendChild(hint);\n const takePicButton = document.createElement(\"div\");\n takePicButton.id = \"takePicButton\";\n takePicButton.classList.add(\"pw-box-border\", \"pw-mx-auto\", \"pw-absolute\", \"pw-bottom-16\", \"pw-relative\", \"pw-rounded-full\", \"pw-h-16\", \"pw-w-16\", \"pw-border-solid\", \"pw-border-white\", \"pw-border-4\", \"pw-pointer-events-none\");\n // SVG progress ring sitting on top of the white border circle. \n // The progress arc is animated from empty -> full over AUTO_CAPTURE_HOLD_MS.\n const svgNS = \"http://www.w3.org/2000/svg\";\n const ringSvg = document.createElementNS(svgNS, \"svg\");\n ringSvg.setAttribute(\"viewBox\", \"0 0 64 64\");\n ringSvg.classList.add(\"pw-absolute\", \"pw-inset-0\", \"pw-w-full\", \"pw-h-full\");\n ringSvg.style.transform = \"rotate(-90deg)\";\n const progressCircle = document.createElementNS(svgNS, \"circle\");\n progressCircle.id = \"takePicProgress\";\n progressCircle.setAttribute(\"cx\", \"32\");\n progressCircle.setAttribute(\"cy\", \"32\");\n progressCircle.setAttribute(\"r\", `${CardCamera.RING_RADIUS}`);\n progressCircle.setAttribute(\"fill\", \"none\");\n progressCircle.setAttribute(\"stroke\", \"#ffffff\");\n progressCircle.setAttribute(\"stroke-width\", \"6\");\n progressCircle.setAttribute(\"stroke-linecap\", \"round\");\n progressCircle.setAttribute(\"stroke-dasharray\", `${CardCamera.RING_CIRCUMFERENCE}`);\n progressCircle.setAttribute(\"stroke-dashoffset\", `${CardCamera.RING_CIRCUMFERENCE}`);\n progressCircle.style.transition = \"stroke-dashoffset 80ms linear, stroke 150ms linear\";\n ringSvg.appendChild(progressCircle);\n takePicButton.appendChild(ringSvg);\n const smallCircleContainer = document.createElement(\"div\");\n smallCircleContainer.classList.add(\"pw-absolute\", \"pw-top-1/2\", \"-pw-translate-y-1/2\", \"pw-left-1/2\", \"-pw-translate-x-1/2\");\n takePicButton.appendChild(smallCircleContainer);\n const smallCircle = document.createElement(\"div\");\n smallCircle.id = \"takePicInner\";\n smallCircle.classList.add(\"pw-w-11\", \"pw-h-11\", \"pw-bg-white\", \"pw-rounded-full\", \"pw-opacity-60\");\n smallCircleContainer.appendChild(smallCircle);\n footer.appendChild(takePicButton);\n const mainBody = document.getElementById(this.cameraId);\n this.footerElement = footer;\n mainBody === null || mainBody === void 0 ? void 0 : mainBody.appendChild(footer);\n // Powered by pipwave at bottom of viewport\n const poweredByWrapper = document.createElement(\"div\");\n poweredByWrapper.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 poweredByFooter = new footer_1.default().render(true);\n poweredByWrapper.appendChild(poweredByFooter);\n mainBody === null || mainBody === void 0 ? void 0 : mainBody.appendChild(poweredByWrapper);\n }\n pauseVideoStream() {\n var _a, _b;\n return __awaiter(this, void 0, void 0, function* () {\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.stopDetectionLoop();\n this.cancelHold();\n // stop the video\n (_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.pause();\n // stop idle timeout\n if (this.timer) {\n clearTimeout(this.timer);\n }\n });\n }\n resumeVideoStream(cardType) {\n var _a;\n return __awaiter(this, void 0, void 0, function* () {\n // resume the video first so detection grabs live frames\n (_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.play();\n // updateType re-arms auto-capture and (re)starts the detection loop for this side\n this.updateType(cardType);\n // start idle timeout\n this.timer = setTimeout(() => {\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 (0, components_1.hideFullLoading)(this.cameraId);\n });\n }\n stopVideoStream() {\n this._cleanup();\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 _cleanup() {\n var _a;\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.dispose();\n if (this.videoElement && this.videoElement.srcObject) {\n const stream = this.videoElement.srcObject;\n stream.getTracks().forEach(t => t.stop());\n this.videoElement.srcObject = null;\n }\n if (this.timer)\n clearTimeout(this.timer);\n window.removeEventListener(\"resize\", this._onWindowResize);\n window.removeEventListener(\"beforeunload\", this._onBeforeUnload);\n }\n updateType(cardType) {\n this.cardType = cardType;\n // New side (card flipped to back) or resumed session — re-arm auto-capture.\n this.hasAutoCaptured = false;\n this.cancelHold();\n this.disableManualCapture();\n // update title\n const title = document.getElementById(\"pw-camera-title\");\n if (title)\n title.innerHTML = camera_instruction_1.CAMERA_INSTRUCTION[this.cardType].title;\n // triggerAutoCapture stops the loop on every capture, so (re)start it for this\n // side. The flip flow (VerifyId) only calls updateType — not resumeVideoStream —\n // so this is what brings detection back for the back of the card.\n this.startDetection();\n }\n startDetection() {\n var _a;\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.startDetectionLoop((framed) => this.onFramedChange(framed), () => this.enableManualCapture());\n }\n // Detector unavailable -> enable the manual shutter once, and offer Retry.\n enableManualCapture() {\n const btn = document.getElementById(\"takePicButton\");\n if (!btn || btn.dataset.manual === \"1\")\n return;\n btn.dataset.manual = \"1\";\n btn.classList.remove(\"pw-pointer-events-none\");\n btn.classList.add(\"pw-cursor-pointer\");\n this.setHint((0, translations_1.$t)(\"doc_align_card\"));\n this._manualClickHandler = () => this.triggerManualCapture();\n btn.addEventListener(\"click\", this._manualClickHandler);\n if (!this.scannerUnavailableNotified) {\n this.scannerUnavailableNotified = true;\n this._showScannerUnavailableDialog();\n }\n }\n // Re-lock the shutter and detach the manual handler (detection resumed, or side flipped).\n disableManualCapture() {\n const btn = document.getElementById(\"takePicButton\");\n if (btn) {\n if (this._manualClickHandler)\n btn.removeEventListener(\"click\", this._manualClickHandler);\n btn.classList.add(\"pw-pointer-events-none\");\n btn.classList.remove(\"pw-cursor-pointer\");\n delete btn.dataset.manual;\n }\n this._manualClickHandler = null;\n }\n _showScannerUnavailableDialog() {\n new baseDialog_1.default().render(this.mainScreenId, (0, translations_1.$t)(\"doc_scanner_unavailable\"), (0, translations_1.$t)(\"btn_retry\"), () => __awaiter(this, void 0, void 0, function* () {\n var _a, _b;\n yield ((_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.reloadModel());\n if ((_b = this.cardDetectionSdk) === null || _b === void 0 ? void 0 : _b.detectionAvailable) {\n this.disableManualCapture();\n this.startDetection();\n }\n else {\n this._showScannerUnavailableDialog(); // still down -> keep offering Retry\n }\n }), true);\n }\n triggerManualCapture() {\n var _a;\n return __awaiter(this, void 0, void 0, function* () {\n if (this.hasAutoCaptured)\n return;\n this.hasAutoCaptured = true; // latch: one capture per side\n const btn = document.getElementById(\"takePicButton\");\n btn === null || btn === void 0 ? void 0 : btn.classList.add(\"pw-pointer-events-none\");\n const image = yield this.captureImage();\n const event = new CustomEvent(\"captureImage\", {\n detail: { image, type: this.cardType },\n });\n (_a = document.getElementById(this.cameraId)) === null || _a === void 0 ? void 0 : _a.dispatchEvent(event);\n });\n }\n loadVideoStream(videoElement) {\n const initialConstraints = {\n facingMode: \"environment\",\n width: {\n ideal: 1280, // 1280, 1024, 640\n },\n height: {\n ideal: 960, // 960, 768, 480\n },\n video: true,\n };\n // Mirror video when not on mobile device (desktop/laptop front camera UX)\n if (!(0, deviceDetection_1.isMobileDevice)()) {\n videoElement.style.transform = \"scaleX(-1)\";\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 })\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 // Fire model load concurrently — typically ready by the time video starts.\n this.cardDetectionSdk.loadModel();\n // video event listeners\n this.videoElement.addEventListener(\"loadedmetadata\", () => {\n var _a;\n this.cardDetectionSdk.init(this.cardType);\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.classList.remove(\"pw-hidden\");\n // Set initial video dimensions based on orientation\n this.resizeElement();\n this.startDetection();\n });\n this.videoElement.addEventListener(\"resize\", () => {\n const isFooterExist = document.getElementById(camera_1.ELEMENT_IDS.FOOTER);\n if (!isFooterExist) {\n this.renderCameraBottomUi();\n }\n this.resizeElement();\n });\n window.addEventListener(\"resize\", this._onWindowResize);\n window.addEventListener(\"beforeunload\", this._onBeforeUnload);\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 // Driven by the card-detection loop. \n onFramedChange(framed) {\n this.isFramed = framed;\n if (this.hasAutoCaptured)\n return;\n if (framed) {\n this.setHint((0, translations_1.$t)(\"doc_hold_steady\"));\n this.ensureHoldLoop();\n }\n else {\n // Lost framing: empty the ring instantly and restart — no slow drain.\n this.cancelHold();\n }\n }\n // Single rAF loop: progress climbs while framed. Secures the frame at the midpoint\n // and captures once it hits 1. Draining is handled instantly by onFramedChange.\n ensureHoldLoop() {\n if (this.holdRafId !== null || this.hasAutoCaptured)\n return;\n this.lastHoldTick = performance.now();\n const tick = (now) => {\n const dt = now - this.lastHoldTick;\n this.lastHoldTick = now;\n if (!this.isFramed) {\n this.holdRafId = null;\n return;\n }\n // Detector saw no valid card recently (card pulled out, but the border debounce hasn't\n // caught up on slow single-thread devices) — drain instead of completing a stale capture.\n const sdk = this.cardDetectionSdk;\n if ((sdk === null || sdk === void 0 ? void 0 : sdk.detectionAvailable) && sdk.msSinceValidDetection > CardCamera.AUTO_CAPTURE_STALE_MS) {\n this.holdRafId = null;\n this.cancelHold();\n return;\n }\n this.holdProgress = Math.min(1, this.holdProgress + dt / CardCamera.AUTO_CAPTURE_HOLD_MS);\n this.setRingProgress(this.holdProgress);\n if (this.holdProgress >= CardCamera.AUTO_CAPTURE_GRAB_AT && this.pendingCapture === null) {\n this.pendingCapture = this.captureImage();\n if (MainPage_1.enableImageDebug)\n console.log(`[DEBUG] ${this.cardType} frame secured at midpoint`);\n }\n if (this.holdProgress >= 1) {\n this.holdRafId = null;\n this.triggerAutoCapture();\n return;\n }\n this.holdRafId = requestAnimationFrame(tick);\n };\n this.holdRafId = requestAnimationFrame(tick);\n }\n cancelHold() {\n if (this.holdRafId !== null) {\n cancelAnimationFrame(this.holdRafId);\n this.holdRafId = null;\n }\n this.isFramed = false;\n this.holdProgress = 0;\n this.pendingCapture = null;\n this.setRingProgress(0);\n this.hideCaptureTick();\n if (!this.hasAutoCaptured)\n this.setHint((0, translations_1.$t)(\"doc_align_card\"));\n }\n triggerAutoCapture() {\n var _a;\n return __awaiter(this, void 0, void 0, function* () {\n if (this.hasAutoCaptured || this.pendingCapture === null)\n return;\n this.hasAutoCaptured = true;\n // Commit only the frame secured at the midpoint — never a fresh (possibly blurry) grab.\n const capture = this.pendingCapture;\n this.pendingCapture = null;\n // Stop inference immediately — no point running the model (and flickering the\n // border over a frozen frame) in the window before pause/flip kicks in.\n (_a = this.cardDetectionSdk) === null || _a === void 0 ? void 0 : _a.stopDetectionLoop();\n this.setRingProgress(1);\n const mainBody = document.getElementById(this.cameraId);\n const image = yield capture;\n if (MainPage_1.enableImageDebug)\n console.log(`[DEBUG] ${this.cardType} image captured (committed at success)`);\n // Frame is secured — now confirm success to the user.\n this.showCaptureTick();\n const event = new CustomEvent(\"captureImage\", {\n detail: {\n image,\n type: this.cardType,\n },\n });\n yield new Promise((resolve) => setTimeout(resolve, CardCamera.CAPTURE_TICK_MS));\n mainBody === null || mainBody === void 0 ? void 0 : mainBody.dispatchEvent(event);\n });\n }\n // progress 0 -> empty ring, 1 -> full ring\n setRingProgress(progress) {\n const ring = document.getElementById(\"takePicProgress\");\n if (!ring)\n return;\n ring.setAttribute(\"stroke\", CardCamera.RING_FILL_COLOR);\n const offset = CardCamera.RING_CIRCUMFERENCE * (1 - progress);\n ring.setAttribute(\"stroke-dashoffset\", `${offset}`);\n }\n // Briefly flash a green checkmark inside the capture button to confirm a\n // successful capture, then clear it so the button is clean for the next side.\n showCaptureTick() {\n var _a, _b;\n if (document.getElementById(\"takePicTick\"))\n return;\n const container = (_a = document.getElementById(\"takePicInner\")) === null || _a === void 0 ? void 0 : _a.parentElement;\n if (!container)\n return;\n // White backing pops once the tick lands.\n (_b = document\n .getElementById(\"takePicInner\")) === null || _b === void 0 ? void 0 : _b.classList.replace(\"pw-opacity-60\", \"pw-opacity-100\");\n const svgNS = \"http://www.w3.org/2000/svg\";\n const tick = document.createElementNS(svgNS, \"svg\");\n tick.id = \"takePicTick\";\n tick.setAttribute(\"viewBox\", \"0 0 24 24\");\n tick.setAttribute(\"width\", \"34\");\n tick.setAttribute(\"height\", \"34\");\n tick.classList.add(\"pw-absolute\", \"pw-top-1/2\", \"-pw-translate-y-1/2\", \"pw-left-1/2\", \"-pw-translate-x-1/2\");\n const path = document.createElementNS(svgNS, \"path\");\n path.setAttribute(\"d\", \"M5 13l4 4L19 7\");\n path.setAttribute(\"fill\", \"none\");\n path.setAttribute(\"stroke\", CardCamera.RING_FILL_COLOR);\n path.setAttribute(\"stroke-width\", \"3\");\n path.setAttribute(\"stroke-linecap\", \"round\");\n path.setAttribute(\"stroke-linejoin\", \"round\");\n tick.appendChild(path);\n container.appendChild(tick);\n if (this.tickTimer)\n clearTimeout(this.tickTimer);\n this.tickTimer = setTimeout(() => this.hideCaptureTick(), CardCamera.CAPTURE_TICK_MS);\n }\n hideCaptureTick() {\n var _a, _b;\n if (this.tickTimer) {\n clearTimeout(this.tickTimer);\n this.tickTimer = null;\n }\n (_a = document.getElementById(\"takePicTick\")) === null || _a === void 0 ? void 0 : _a.remove();\n (_b = document\n .getElementById(\"takePicInner\")) === null || _b === void 0 ? void 0 : _b.classList.replace(\"pw-opacity-100\", \"pw-opacity-60\");\n }\n setHint(text) {\n const hintEl = document.getElementById(\"pw-doc-hint\");\n if (hintEl)\n hintEl.innerHTML = text;\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 containerH = vpH; // viewport wider than video — constrain by height\n containerW = vpH * videoAspect;\n }\n else {\n containerW = vpW; // viewport taller than video — constrain by width\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.cardDetectionSdk.resizeElement();\n }\n}\nCardCamera.AUTO_CAPTURE_HOLD_MS = 1500; // framed time to fill the ring\nCardCamera.AUTO_CAPTURE_STALE_MS = 600; // drain the hold if no valid detection within this window (single-thread infer ≈240–350ms)\nCardCamera.AUTO_CAPTURE_GRAB_AT = 0.5; // fill ratio at which we secure the frame\nCardCamera.RING_RADIUS = 29;\nCardCamera.RING_CIRCUMFERENCE = 2 * Math.PI * CardCamera.RING_RADIUS;\nCardCamera.RING_FILL_COLOR = \"#16A34A\";\nCardCamera.CAPTURE_TICK_MS = 800;\nexports[\"default\"] = CardCamera;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/card-camera.ts?");
4004
4004
 
4005
4005
  /***/ }),
4006
4006
 
@@ -4011,7 +4011,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4011
4011
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
4012
4012
 
4013
4013
  "use strict";
4014
- 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 id_1 = __webpack_require__(/*! ../constant/id */ \"./src/constant/id.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst CARD_DETECTION_CONFIG = {\n // Model / inference\n // Fine-tuned YOLO11n, single \"document\" class, INT8. Output is [1, 5, N]:\n // cx, cy, w, h, score per anchor (N comes from the model, we don't hardcode it).\n MODEL_URL: `${\"https://assets.pipwave.com/ekyc/sdk/card-models\" || 0}/document-detection-int8-480.onnx`,\n // onnxruntime-web. The matching .mjs/.wasm live next to it (public/ort/ in dev, the\n // CDN dir in prod). In prod this is cross-origin, so importScripts/fetch require the\n // assets host to serve permissive CORS (+ CORP under COEP for the threaded wasm).\n ORT_URL: `${\"https://assets.pipwave.com/ekyc/sdk/ort\" || 0}/ort.wasm.min.js`,\n INPUT_SIZE: 480,\n MAX_THREADS: 4,\n // so cranking this up doesn't help and just hogs the CPU.\n // Detection rule\n // Two thresholds, not one, so a score wobbling around a single cutoff doesn't flip\n // valid/no-card every frame: needs to get above CONF_ENTER to count as a card, then\n // stays a card until it drops under CONF_EXIT.\n CONF_ENTER: 0.55,\n CONF_EXIT: 0.40,\n // Smooth the score so one bad frame doesn't drop us (the score tanks for a frame or two\n // on a held card when it blurs). ema = alpha*raw + (1-alpha)*ema; smaller alpha = smoother.\n CONF_EMA_ALPHA: 0.5,\n // Framing geometry (the real gate is containment: is the card inside the guide) ──\n MIN_FILL_RATIO: 0.45,\n // How far the card may poke past the guide (x min(guideW,guideH)) before it's \"out of box\".\n // POSITIVE = lenient (card can spill outside the guide). NEGATIVE = strict: the card must stay\n // this far INSIDE the guide, which shrinks the effective detection box so an angled/edging card\n // is flagged \"out-of-box\" early, before it actually crosses the visible guide line.\n CONTAINMENT_SLACK_RATIO: -0.04,\n // Loop / debounce\n // Asymmetric on purpose: slow to go green (don't want a false capture), quick to drop it.\n THROTTLE_FPS: 8,\n STABLE_FRAMES_TO_ENABLE: 2,\n STABLE_FRAMES_TO_DISABLE: 1,\n BORDER_STABLE_FRAMES: 2,\n BORDER_CLEAR_FRAMES: 1,\n INFERENCE_STUCK_MS: 3000, // if a frame's been out this long the worker lost it,\n // so clear the busy flag and carry on instead of hanging\n};\nconst ID_CARD_TYPES = [\n camera_1.CARD_TYPES.ID,\n camera_1.CARD_TYPES.ID_BACK,\n camera_1.CARD_TYPES.LICENSE,\n camera_1.CARD_TYPES.LICENSE_BACK,\n camera_1.CARD_TYPES.CC,\n camera_1.CARD_TYPES.CC_BACK,\n];\n// Inference worker\n// Runs onnxruntime-web off the main thread so WASM inference never blocks UI\n// paint. Receives a transferred ImageBitmap per frame, letterboxes + preprocesses\n// on an OffscreenCanvas, runs the model, and posts back the best box in video px.\n// Kept as a Blob string so it needs no webpack worker-loader config.\nconst WORKER_SOURCE = `\nlet session = null, ortLib = null, ready = false;\nlet INPUT = 320, ctx = null;\n// Input buffer + tensor, allocated once and reused every frame to avoid per-frame GC.\nlet inputBuf = null, inputTensor = null;\n\nself.onmessage = async (e) => {\n const msg = e.data;\n\n if (msg.type === 'init') {\n INPUT = msg.input;\n let stage = 'importScripts';\n try {\n importScripts(msg.ortUrl);\n stage = 'ort-global';\n ortLib = self.ort;\n if (!ortLib) throw new Error('self.ort is undefined after importScripts(' + msg.ortUrl + ')');\n stage = 'config';\n ortLib.env.wasm.wasmPaths = msg.wasmDir;\n ortLib.env.wasm.simd = true;\n // Threads need SharedArrayBuffer, which only exists if the page is cross-origin\n // isolated (COOP/COEP headers). No isolation, no threads - drop to one.\n ortLib.env.wasm.numThreads = self.crossOriginIsolated\n ? Math.min(msg.maxThreads || 4, navigator.hardwareConcurrency || 4)\n : 1;\n stage = 'offscreen-canvas';\n const canvas = new OffscreenCanvas(INPUT, INPUT);\n ctx = canvas.getContext('2d', { willReadFrequently: true });\n stage = 'session-create (' + msg.modelUrl + ')';\n session = await ortLib.InferenceSession.create(msg.modelUrl, {\n executionProviders: ['wasm'],\n graphOptimizationLevel: 'all',\n });\n ready = !!session;\n if (!ready) throw new Error('session create returned no session');\n // Allocate the input buffer/tensor once, then do one throwaway run. That first run\n // eats the graph-optimization + allocation cost so the first real frame isn't slow.\n stage = 'warmup';\n inputBuf = new Float32Array(3 * INPUT * INPUT);\n inputTensor = new ortLib.Tensor('float32', inputBuf, [1, 3, INPUT, INPUT]);\n try {\n await session.run({ [session.inputNames[0]]: inputTensor });\n } catch (_) { /* fine, the first real frame just won't have the warm-up */ }\n self.postMessage({\n type: 'ready', ok: true, ep: 'wasm',\n coi: !!self.crossOriginIsolated,\n threads: ortLib.env.wasm.numThreads,\n });\n return;\n } catch (err) {\n const detail = err && err.message ? err.message : String(err);\n self.postMessage({ type: 'ready', ok: false, error: '[' + stage + '] ' + detail });\n return;\n }\n }\n\n if (msg.type === 'frame') {\n const bitmap = msg.bitmap;\n if (!ready) { if (bitmap && bitmap.close) bitmap.close(); self.postMessage({ type: 'result', ok: false }); return; }\n\n const W = bitmap.width, H = bitmap.height, N = INPUT;\n const scale = Math.min(N / W, N / H);\n const newW = Math.round(W * scale), newH = Math.round(H * scale);\n const padX = (N - newW) / 2, padY = (N - newH) / 2;\n ctx.fillStyle = '#727272';\n ctx.fillRect(0, 0, N, N);\n ctx.drawImage(bitmap, 0, 0, W, H, padX, padY, newW, newH);\n if (bitmap.close) bitmap.close();\n\n const d = ctx.getImageData(0, 0, N, N).data;\n const area = N * N, f = inputBuf; // reused buffer; tensor wraps it by reference\n for (let i = 0; i < area; i++) {\n f[i] = d[i * 4] / 255;\n f[area + i] = d[i * 4 + 1] / 255;\n f[2 * area + i] = d[i * 4 + 2] / 255;\n }\n\n let out, inferMs = 0;\n try {\n const t0 = performance.now();\n out = await session.run({ [session.inputNames[0]]: inputTensor });\n inferMs = performance.now() - t0;\n } catch (err) { self.postMessage({ type: 'result', ok: false }); return; }\n\n const o = out[session.outputNames[0]], od = o.data, nA = o.dims[2];\n let bestScore = 0, bestIdx = -1; const sBase = 4 * nA;\n for (let a = 0; a < nA; a++) { const s = od[sBase + a]; if (s > bestScore) { bestScore = s; bestIdx = a; } }\n\n let box = null;\n if (bestIdx >= 0) {\n const cx = od[bestIdx], cy = od[nA + bestIdx], bw = od[2 * nA + bestIdx], bh = od[3 * nA + bestIdx];\n box = {\n x1: (cx - bw / 2 - padX) / scale, y1: (cy - bh / 2 - padY) / scale,\n x2: (cx + bw / 2 - padX) / scale, y2: (cy + bh / 2 - padY) / scale,\n };\n }\n self.postMessage({ type: 'result', ok: true, score: bestScore, box: box, W: W, H: H, inferMs: inferMs });\n }\n};\n`;\nclass CARDDETECTIONSDK {\n constructor(val) {\n this.type = camera_1.CARD_TYPES.ID;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n // Worker / model state\n this.worker = null;\n this.workerUrl = null;\n this.modelReady = false;\n this.modelLoadPromise = null;\n // Loop state\n this.rafId = null;\n this.inferenceBusy = false;\n this.lastFrameTime = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.stateChangeCallback = null;\n this.lastReason = '';\n this.lastInferMs = 0;\n this.cardPresent = false; // confidence-hysteresis latch (see _evaluateBox)\n this.scoreEma = 0; // smoothed confidence (rejects single-frame dropouts)\n this.inferenceStartedAt = 0; // timestamp of the in-flight frame (watchdog)\n Object.assign(this, val);\n }\n // Returns overlay dimensions in DISPLAY PIXELS — callers cropping from the\n // video stream must scale by videoWidth/clientWidth.\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || width;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || height;\n const idCardRatio = id_1.ID_CARD_RATIO[this.type] || id_1.ID_CARD_RATIO[\"others\"];\n const isDesktop = window.innerWidth >= id_1.VIEWPORT_SIZE.DESKTOP_BREAKPOINT;\n let newWidth;\n let newHeight;\n if (isDesktop) {\n const isIdCard = ID_CARD_TYPES.includes(this.type);\n const maxWidthRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_WIDTH_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_WIDTH_RATIO;\n const maxHeightRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_HEIGHT_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_HEIGHT_RATIO;\n const maxWidth = videoWidth * maxWidthRatio;\n const maxHeight = videoHeight * maxHeightRatio;\n const widthBasedHeight = maxWidth / idCardRatio;\n const heightBasedWidth = maxHeight * idCardRatio;\n if (widthBasedHeight <= maxHeight) {\n newWidth = maxWidth;\n newHeight = widthBasedHeight;\n }\n else {\n newWidth = heightBasedWidth;\n newHeight = maxHeight;\n }\n // prevent overlay from overlapping host page header\n if (newHeight > id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT) {\n newHeight = id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT;\n newWidth = newHeight * idCardRatio;\n }\n }\n else {\n const availableWidth = Math.min(videoWidth, window.innerWidth) -\n id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n newWidth = Math.max(availableWidth, 200); // min 200px for very small screens\n newHeight = newWidth / idCardRatio;\n const maxHeight = videoHeight - id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n if (maxHeight > 0 && newHeight > maxHeight) {\n newHeight = Math.max(maxHeight, 100);\n newWidth = newHeight * idCardRatio;\n }\n }\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d, _e, _f;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoWidth = this.videoElement.clientWidth;\n const videoHeight = this.videoElement.clientHeight;\n this.canvasElement.width = videoWidth;\n this.canvasElement.height = videoHeight;\n const { width, height } = this.calculateHeightNWidth(videoWidth, videoHeight);\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"15px\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"0px\");\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 const topBorder = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"inherit\");\n const bottomBorder = (_f = this.overlayElement) === null || _f === void 0 ? void 0 : _f.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"inherit\");\n }\n disableDetection() {\n var _a, _b;\n const topBorder = (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"none\");\n const bottomBorder = (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"none\");\n }\n init(type) {\n if (type)\n this.type = type;\n this.resizeElement();\n }\n // Spin up the inference worker and load the model inside it. Resolves even on\n // failure — the caller checks `modelReady` and degrades gracefully.\n loadModel() {\n if (this.modelLoadPromise)\n return this.modelLoadPromise;\n this.modelLoadPromise = new Promise((resolve) => {\n const supported = typeof Worker !== 'undefined' &&\n typeof OffscreenCanvas !== 'undefined' &&\n typeof createImageBitmap !== 'undefined';\n if (!supported) {\n console.warn('[card-detection] Worker/OffscreenCanvas unsupported — detection disabled');\n resolve();\n return;\n }\n try {\n const blob = new Blob([WORKER_SOURCE], { type: 'application/javascript' });\n this.workerUrl = URL.createObjectURL(blob);\n this.worker = new Worker(this.workerUrl);\n }\n catch (e) {\n console.error('[card-detection] failed to start worker', e);\n resolve();\n return;\n }\n this.worker.onmessage = (e) => {\n const m = e.data;\n if ((m === null || m === void 0 ? void 0 : m.type) === 'ready') {\n this.modelReady = !!m.ok;\n if (m.ok) {\n if (MainPage_1.enableImageDebug)\n console.log(`[card-detection] model ready — backend=${m.ep} crossOriginIsolated=${m.coi} threads=${m.threads}`);\n }\n else {\n console.error('[card-detection] model failed to load in worker:', m.error || '(no detail)');\n }\n resolve();\n }\n else if ((m === null || m === void 0 ? void 0 : m.type) === 'result') {\n this._onResult(m);\n }\n };\n this.worker.onerror = (e) => {\n if (MainPage_1.enableImageDebug)\n console.error('[card-detection] worker error', e === null || e === void 0 ? void 0 : e.message);\n if (!this.modelReady)\n resolve();\n };\n // A Blob worker's base is blob:, so resolve every URL to absolute up front.\n const modelUrl = new URL(CARD_DETECTION_CONFIG.MODEL_URL, location.href).href;\n const ortUrl = new URL(CARD_DETECTION_CONFIG.ORT_URL, location.href).href;\n const wasmDir = ortUrl.replace(/[^/]*$/, ''); // dir holding the .mjs + .wasm\n this.worker.postMessage({\n type: 'init',\n input: CARD_DETECTION_CONFIG.INPUT_SIZE,\n maxThreads: CARD_DETECTION_CONFIG.MAX_THREADS,\n modelUrl,\n ortUrl,\n wasmDir,\n });\n });\n return this.modelLoadPromise;\n }\n startDetectionLoop(onStateChange) {\n return __awaiter(this, void 0, void 0, function* () {\n this.stateChangeCallback = onStateChange; // save first so a re-call updates the callback\n if (this.rafId)\n return; // then bail if the loop is already running\n yield this.modelLoadPromise;\n if (!this.modelReady) {\n onStateChange(true); // model/worker failed — let the user capture anyway\n return;\n }\n this.lastFrameTime = 0;\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.lastReason = '';\n this.lastInferMs = 0;\n this.rafId = requestAnimationFrame((now) => this._loopTick(now));\n });\n }\n stopDetectionLoop() {\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.setBorderState('none');\n }\n // Terminal teardown — stops the loop and releases the worker. Call when the\n // camera is closed for good (card-camera `_cleanup`), not on pause.\n dispose() {\n this.stopDetectionLoop();\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n if (this.workerUrl) {\n URL.revokeObjectURL(this.workerUrl);\n this.workerUrl = null;\n }\n this.modelReady = false;\n this.modelLoadPromise = null;\n }\n setBorderState(state) {\n if (!this.overlayElement)\n return;\n const borders = [\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`),\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`),\n ];\n borders.forEach(el => {\n if (!el)\n return;\n el.classList.remove('border-element--valid', 'border-element--invalid');\n if (state !== 'none')\n el.classList.add(`border-element--${state}`);\n });\n }\n _loopTick(now) {\n var _a;\n this.rafId = requestAnimationFrame((n) => this._loopTick(n));\n if (now - this.lastFrameTime < 1000 / CARD_DETECTION_CONFIG.THROTTLE_FPS)\n return;\n // Watchdog: a frame out longer than INFERENCE_STUCK_MS means the worker died or\n // dropped it. Clear the busy flag so we recover - otherwise it stays true forever\n // and detection just quietly stops.\n if (this.inferenceBusy) {\n if (now - this.inferenceStartedAt < CARD_DETECTION_CONFIG.INFERENCE_STUCK_MS)\n return;\n if (MainPage_1.enableImageDebug)\n console.warn('[card-detection] inference watchdog fired, resetting stuck frame');\n this.inferenceBusy = false;\n }\n if (!this.worker || !this.modelReady)\n return;\n if (!((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.videoWidth))\n return;\n this.lastFrameTime = now;\n this.inferenceBusy = true;\n this.inferenceStartedAt = now;\n // Grab the frame as a transferable ImageBitmap, shrunk during decode to about\n // INPUT_SIZE on the long side. Sending the full 1280x960 every time and resizing\n // in the worker was wasteful. Resolution doesn't matter to detection - the guide is\n // normalised and the worker letterboxes whatever it gets.\n const N = CARD_DETECTION_CONFIG.INPUT_SIZE;\n const vw = this.videoElement.videoWidth, vh = this.videoElement.videoHeight;\n const s = N / Math.max(vw, vh);\n const rw = Math.max(1, Math.round(vw * s)), rh = Math.max(1, Math.round(vh * s));\n createImageBitmap(this.videoElement, { resizeWidth: rw, resizeHeight: rh, resizeQuality: 'low' })\n .then((bitmap) => {\n if (!this.worker) {\n bitmap.close();\n this.inferenceBusy = false;\n return;\n }\n this.worker.postMessage({ type: 'frame', bitmap }, [bitmap]);\n })\n .catch(() => { this.inferenceBusy = false; });\n }\n // Worker posted a detection result — do the (cheap) framing geometry against\n // the current guide on the main thread, then debounce.\n _onResult(m) {\n var _a, _b, _c, _d, _e;\n this.inferenceBusy = false;\n if (!m.ok)\n return;\n this.lastInferMs = (_a = m.inferMs) !== null && _a !== void 0 ? _a : 0;\n const state = this._evaluateBox((_b = m.score) !== null && _b !== void 0 ? _b : 0, (_c = m.box) !== null && _c !== void 0 ? _c : null, (_d = m.W) !== null && _d !== void 0 ? _d : 0, (_e = m.H) !== null && _e !== void 0 ? _e : 0);\n this._updateBorderDebounced(state);\n this._updateDebounce(state);\n }\n // Turns a raw detection into a state: below the confidence gate is 'no-card', a card\n // spilling out of the guide or too small for it is 'invalid', otherwise 'valid'.\n _evaluateBox(score, box, W, H) {\n // Smooth the score first so one dropout frame can't kick us out of the locked\n // state. A frame with no box counts as 0 and pulls the average down.\n const a = CARD_DETECTION_CONFIG.CONF_EMA_ALPHA;\n this.scoreEma = a * (box ? score : 0) + (1 - a) * this.scoreEma;\n const conf = this.scoreEma;\n // Hysteresis: once we've locked onto a card we let the score sag to CONF_EXIT before\n // giving up; until then it has to clear the higher CONF_ENTER. Stops the flicker.\n const gate = this.cardPresent ? CARD_DETECTION_CONFIG.CONF_EXIT : CARD_DETECTION_CONFIG.CONF_ENTER;\n if (!box || conf <= gate) {\n this.cardPresent = false;\n this._trace('no-card', conf, null);\n return 'no-card';\n }\n this.cardPresent = true;\n const guide = this._computeNormalizedGuide();\n if (!guide || !W || !H) {\n this._trace('VALID', conf, null);\n return 'valid';\n } // no guide → conf-only\n const gx1 = guide.left * W, gy1 = guide.top * H;\n const gx2 = (guide.left + guide.width) * W, gy2 = (guide.top + guide.height) * H;\n const gArea = Math.max(1, (gx2 - gx1) * (gy2 - gy1));\n const gMin = Math.min(gx2 - gx1, gy2 - gy1);\n // fill = how much of the guide the card covers (card too far → low fill).\n const inter = Math.max(0, Math.min(box.x2, gx2) - Math.max(box.x1, gx1))\n * Math.max(0, Math.min(box.y2, gy2) - Math.max(box.y1, gy1));\n const fill = inter / gArea;\n // containment = card stays inside the guide (+slack); spilling out → \"out of box\".\n const eps = CARD_DETECTION_CONFIG.CONTAINMENT_SLACK_RATIO * gMin;\n const contained = box.x1 >= gx1 - eps && box.y1 >= gy1 - eps\n && box.x2 <= gx2 + eps && box.y2 <= gy2 + eps;\n let result;\n let reason;\n if (!contained) {\n result = 'invalid';\n reason = 'out-of-box';\n }\n else if (fill < CARD_DETECTION_CONFIG.MIN_FILL_RATIO) {\n result = 'invalid';\n reason = 'too-small';\n }\n else {\n result = 'valid';\n reason = 'VALID';\n }\n this._trace(reason, conf, fill);\n return result;\n }\n // Guide rect as fractions of the raw (un-mirrored) frame - the space the worker's boxes\n // are in. Two gotchas to undo:\n // 1. the <video> is object-fit:cover, so it scales the frame to fill the element and\n // crops the rest. That means a fraction of what you see on screen isn't the same\n // fraction of the real frame. Skip this and a card poking into the cropped-off strip\n // looks \"inside the guide\" to the model. So we map display fractions back through\n // the cover crop.\n // 2. on desktop the video is mirrored (scaleX(-1)), so flip X back.\n _computeNormalizedGuide() {\n if (!this.overlayElement || !this.videoElement)\n return null;\n const oRect = this.overlayElement.getBoundingClientRect();\n const vRect = this.videoElement.getBoundingClientRect();\n if (!vRect.width || !vRect.height)\n return null;\n const VW = this.videoElement.videoWidth, VH = this.videoElement.videoHeight;\n if (!VW || !VH)\n return null;\n // Guide as fractions of the DISPLAYED video box.\n let left = (oRect.left - vRect.left) / vRect.width;\n let top = (oRect.top - vRect.top) / vRect.height;\n let width = oRect.width / vRect.width;\n let height = oRect.height / vRect.height;\n // Walk those display fractions back through the (centred) cover crop.\n const videoAspect = VW / VH, displayAspect = vRect.width / vRect.height;\n if (videoAspect >= displayAspect) {\n // frame is wider than the element, so the sides get cropped, full height shows\n const visW = displayAspect / videoAspect;\n left = (1 - visW) / 2 + left * visW;\n width = width * visW;\n }\n else {\n // frame is taller than the element, so top/bottom get cropped, full width shows\n const visH = videoAspect / displayAspect;\n top = (1 - visH) / 2 + top * visH;\n height = height * visH;\n }\n if (!(0, deviceDetection_1.isMobileDevice)())\n left = 1 - left - width;\n return { left, top, width, height };\n }\n // Diagnostic console trace. Only logs when the state actually changes, so no spam.\n _trace(reason, conf, fill) {\n if (reason === this.lastReason)\n return;\n this.lastReason = reason;\n if (MainPage_1.enableImageDebug)\n console.log(`[detect] ${reason} | conf=${conf.toFixed(3)} fill=${fill == null ? '-' : fill.toFixed(2)} (min ${CARD_DETECTION_CONFIG.MIN_FILL_RATIO}) gate=${CARD_DETECTION_CONFIG.CONF_ENTER}/${CARD_DETECTION_CONFIG.CONF_EXIT} | infer=${this.lastInferMs.toFixed(0)}ms`);\n }\n // Border colour debounce, deliberately lopsided: going green needs BORDER_STABLE_FRAMES\n // good frames in a row so we don't flash green by mistake, but dropping green only needs\n // BORDER_CLEAR_FRAMES, so pulling the card out clears it almost immediately.\n _updateBorderDebounced(rawState) {\n const target = rawState === 'valid' ? 'valid' :\n rawState === 'no-card' ? 'none' :\n 'invalid';\n if (target === this.candidateBorder) {\n this.candidateBorderCount++;\n }\n else {\n this.candidateBorder = target;\n this.candidateBorderCount = 1;\n }\n const needed = target === 'valid'\n ? CARD_DETECTION_CONFIG.BORDER_STABLE_FRAMES\n : CARD_DETECTION_CONFIG.BORDER_CLEAR_FRAMES;\n if (this.candidateBorderCount >= needed && target !== this.activeBorder) {\n this.activeBorder = target;\n this.setBorderState(target);\n }\n }\n _updateDebounce(rawState) {\n var _a;\n const wasFramed = this.isDocumentProperlyFramed;\n if (rawState === 'valid') {\n this.consecutiveInvalidFrames = 0;\n this.consecutiveValidFrames++;\n if (!this.isDocumentProperlyFramed &&\n this.consecutiveValidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_ENABLE) {\n this.isDocumentProperlyFramed = true;\n }\n }\n else {\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames++;\n if (this.isDocumentProperlyFramed &&\n this.consecutiveInvalidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_DISABLE) {\n this.isDocumentProperlyFramed = false;\n }\n }\n if (this.isDocumentProperlyFramed !== wasFramed) {\n (_a = this.stateChangeCallback) === null || _a === void 0 ? void 0 : _a.call(this, this.isDocumentProperlyFramed);\n }\n }\n}\nexports[\"default\"] = CARDDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/card-detection.ts?");
4014
+ 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 id_1 = __webpack_require__(/*! ../constant/id */ \"./src/constant/id.ts\");\nconst camera_1 = __webpack_require__(/*! ../models/camera */ \"./src/models/camera.ts\");\nconst deviceDetection_1 = __webpack_require__(/*! ../helper/deviceDetection */ \"./src/helper/deviceDetection.ts\");\nconst MainPage_1 = __webpack_require__(/*! ../classes/MainPage */ \"./src/classes/MainPage.ts\");\nconst detection_failure_1 = __webpack_require__(/*! ./detection-failure */ \"./src/plugins/detection-failure.ts\");\nconst CARD_DETECTION_CONFIG = {\n // Model / inference\n // Fine-tuned YOLO11n, single \"document\" class, INT8. Output is [1, 5, N]:\n // cx, cy, w, h, score per anchor (N comes from the model, we don't hardcode it).\n MODEL_URL: `${\"https://assets.pipwave.com/ekyc/sdk/card-models\" || 0}/document-detection-int8-480.onnx`,\n // Directory holding the ORT .mjs/.wasm and the worker (public/ort/ in dev, CDN dir in prod).\n ORT_DIR: `${\"https://staging-assets.pipwave.com/ekyc/sdk/ort\" || 0}/`,\n // Static ESM module worker — served from https:, no blob: needed, CSP-safe.\n WORKER_URL: `${\"https://staging-assets.pipwave.com/ekyc/sdk/ort\" || 0}/card-detection.worker.mjs`,\n INPUT_SIZE: 480,\n // Detection rule\n // Two thresholds, not one, so a score wobbling around a single cutoff doesn't flip\n // valid/no-card every frame: needs to get above CONF_ENTER to count as a card, then\n // stays a card until it drops under CONF_EXIT.\n CONF_ENTER: 0.55,\n CONF_EXIT: 0.40,\n // Smooth the score so one bad frame doesn't drop us (the score tanks for a frame or two\n // on a held card when it blurs). ema = alpha*raw + (1-alpha)*ema; smaller alpha = smoother.\n CONF_EMA_ALPHA: 0.5,\n // Framing geometry (the real gate is containment: is the card inside the guide) ──\n MIN_FILL_RATIO: 0.45,\n // How far the card may poke past the guide (x min(guideW,guideH)) before it's \"out of box\".\n // POSITIVE = lenient (card can spill outside the guide). NEGATIVE = strict: the card must stay\n // this far INSIDE the guide, which shrinks the effective detection box so an angled/edging card\n // is flagged \"out-of-box\" early, before it actually crosses the visible guide line.\n CONTAINMENT_SLACK_RATIO: -0.04,\n // Loop / debounce\n // Asymmetric on purpose: slow to go green (don't want a false capture), quick to drop it.\n THROTTLE_FPS: 8,\n STABLE_FRAMES_TO_ENABLE: 2,\n STABLE_FRAMES_TO_DISABLE: 1,\n BORDER_STABLE_FRAMES: 2,\n BORDER_CLEAR_FRAMES: 1,\n INFERENCE_STUCK_MS: 3000,\n // so clear the busy flag and carry on instead of hanging\n LOAD_TIMEOUT_MS: 9000,\n MAX_LOAD_RETRIES: 2, // max automatic retries for transient load failures\n};\nconst ID_CARD_TYPES = [\n camera_1.CARD_TYPES.ID,\n camera_1.CARD_TYPES.ID_BACK,\n camera_1.CARD_TYPES.LICENSE,\n camera_1.CARD_TYPES.LICENSE_BACK,\n camera_1.CARD_TYPES.CC,\n camera_1.CARD_TYPES.CC_BACK,\n];\n// Inference worker\n// Runs onnxruntime-web off the main thread so WASM inference never blocks UI\n// paint. Receives a transferred ImageBitmap per frame, letterboxes + preprocesses\n// on an OffscreenCanvas, runs the model, and posts back the best box in video px.\n// Loaded as a static ESM module worker from public/ort/ so no blob: URL is needed\n// and the merchant CSP (script-src … https:) allows it.\nclass CARDDETECTIONSDK {\n // True once the worker + model are loaded and inference can run.\n get detectionAvailable() {\n return this.modelReady;\n }\n // ms since the last frame the detector judged a card VALID — used to drain a stale auto-capture hold.\n get msSinceValidDetection() {\n return performance.now() - this.lastValidAt;\n }\n constructor(val) {\n this.type = camera_1.CARD_TYPES.ID;\n this.videoElement = null;\n this.canvasElement = null;\n this.overlayElement = null;\n this.titleElement = null;\n // Worker / model state\n this.worker = null;\n this.modelReady = false;\n this.modelLoadPromise = null;\n this._loadAttempt = 0;\n // Loop state\n this.rafId = null;\n this.loopStarting = false;\n this.inferenceBusy = false;\n this.lastFrameTime = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.stateChangeCallback = null;\n this.lastReason = '';\n this.lastInferMs = 0;\n this.cardPresent = false; // confidence-hysteresis latch (see _evaluateBox)\n this.scoreEma = 0; // smoothed confidence (rejects single-frame dropouts)\n this.inferenceStartedAt = 0; // timestamp of the in-flight frame (watchdog)\n this.lastValidAt = 0;\n Object.assign(this, val);\n }\n // Returns overlay dimensions in DISPLAY PIXELS — callers cropping from the\n // video stream must scale by videoWidth/clientWidth.\n calculateHeightNWidth(width, height) {\n var _a, _b;\n const videoWidth = ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.clientWidth) || width;\n const videoHeight = ((_b = this.videoElement) === null || _b === void 0 ? void 0 : _b.clientHeight) || height;\n const idCardRatio = id_1.ID_CARD_RATIO[this.type] || id_1.ID_CARD_RATIO[\"others\"];\n const isDesktop = window.innerWidth >= id_1.VIEWPORT_SIZE.DESKTOP_BREAKPOINT;\n let newWidth;\n let newHeight;\n if (isDesktop) {\n const isIdCard = ID_CARD_TYPES.includes(this.type);\n const maxWidthRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_WIDTH_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_WIDTH_RATIO;\n const maxHeightRatio = isIdCard\n ? id_1.VIEWPORT_SIZE.DESKTOP_ID_MAX_HEIGHT_RATIO\n : id_1.VIEWPORT_SIZE.DESKTOP_PASSPORT_MAX_HEIGHT_RATIO;\n const maxWidth = videoWidth * maxWidthRatio;\n const maxHeight = videoHeight * maxHeightRatio;\n const widthBasedHeight = maxWidth / idCardRatio;\n const heightBasedWidth = maxHeight * idCardRatio;\n if (widthBasedHeight <= maxHeight) {\n newWidth = maxWidth;\n newHeight = widthBasedHeight;\n }\n else {\n newWidth = heightBasedWidth;\n newHeight = maxHeight;\n }\n // prevent overlay from overlapping host page header\n if (newHeight > id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT) {\n newHeight = id_1.VIEWPORT_SIZE.MAX_OVERLAY_HEIGHT;\n newWidth = newHeight * idCardRatio;\n }\n }\n else {\n const availableWidth = Math.min(videoWidth, window.innerWidth) -\n id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n newWidth = Math.max(availableWidth, 200); // min 200px for very small screens\n newHeight = newWidth / idCardRatio;\n const maxHeight = videoHeight - id_1.VIEWPORT_SIZE.MOBILE_HORIZONTAL_MARGIN * 2;\n if (maxHeight > 0 && newHeight > maxHeight) {\n newHeight = Math.max(maxHeight, 100);\n newWidth = newHeight * idCardRatio;\n }\n }\n return { width: newWidth, height: newHeight };\n }\n resizeElement() {\n var _a, _b, _c, _d, _e, _f;\n if (!this.videoElement || !this.canvasElement)\n return;\n const videoWidth = this.videoElement.clientWidth;\n const videoHeight = this.videoElement.clientHeight;\n this.canvasElement.width = videoWidth;\n this.canvasElement.height = videoHeight;\n const { width, height } = this.calculateHeightNWidth(videoWidth, videoHeight);\n (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.style.setProperty(\"border-radius\", \"15px\");\n (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.style.setProperty(\"border\", \"0px\");\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 const topBorder = (_e = this.overlayElement) === null || _e === void 0 ? void 0 : _e.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"inherit\");\n const bottomBorder = (_f = this.overlayElement) === null || _f === void 0 ? void 0 : _f.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"inherit\");\n }\n disableDetection() {\n var _a, _b;\n const topBorder = (_a = this.overlayElement) === null || _a === void 0 ? void 0 : _a.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`);\n topBorder === null || topBorder === void 0 ? void 0 : topBorder.style.setProperty(\"display\", \"none\");\n const bottomBorder = (_b = this.overlayElement) === null || _b === void 0 ? void 0 : _b.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`);\n bottomBorder === null || bottomBorder === void 0 ? void 0 : bottomBorder.style.setProperty(\"display\", \"none\");\n }\n init(type) {\n if (type)\n this.type = type;\n this.resizeElement();\n }\n // Spin up the inference worker and load the model inside it. Resolves even on\n // failure — the caller checks `modelReady` and degrades gracefully.\n loadModel() {\n if (this.modelLoadPromise)\n return this.modelLoadPromise;\n this._loadAttempt = 0;\n this.modelLoadPromise = new Promise((resolve) => {\n const supported = typeof Worker !== 'undefined' &&\n typeof OffscreenCanvas !== 'undefined' &&\n typeof createImageBitmap !== 'undefined';\n if (!supported) {\n console.warn('[card-detection] Worker/OffscreenCanvas unsupported — detection disabled');\n resolve();\n return;\n }\n try {\n const workerUrl = new URL(CARD_DETECTION_CONFIG.WORKER_URL, location.href).href;\n this.worker = new Worker(workerUrl, { type: 'module' });\n }\n catch (e) {\n console.error('[card-detection] failed to start worker', e);\n resolve();\n return;\n }\n this.worker.onmessage = (e) => {\n const m = e.data;\n if ((m === null || m === void 0 ? void 0 : m.type) === 'ready') {\n if (m.ok) {\n this.modelReady = true;\n if (MainPage_1.enableImageDebug)\n console.log(`[card-detection] model ready — backend=${m.ep} crossOriginIsolated=${m.coi} threads=${m.threads}`);\n resolve();\n return;\n }\n // Failure — retry transient (network/session) stages a few times, else give up.\n const stage = (m.error || '').replace(/^\\[(.*?)\\][\\s\\S]*$/, '$1');\n if ((0, detection_failure_1.classifyFailure)(stage) === 'retryable' && this._loadAttempt < CARD_DETECTION_CONFIG.MAX_LOAD_RETRIES) {\n const wait = (0, detection_failure_1.computeBackoffMs)(this._loadAttempt);\n this._loadAttempt++;\n if (MainPage_1.enableImageDebug)\n console.warn(`[card-detection] load failed at [${stage}] — retry ${this._loadAttempt}/${CARD_DETECTION_CONFIG.MAX_LOAD_RETRIES} in ${wait}ms`);\n setTimeout(() => this._reinitWorker(), wait);\n return; // keep promise pending; the retry's 'ready' resolves it\n }\n this.modelReady = false;\n console.error('[card-detection] model failed to load (giving up):', m.error || '(no detail)');\n resolve();\n return;\n }\n else if ((m === null || m === void 0 ? void 0 : m.type) === 'result') {\n this._onResult(m);\n }\n };\n this.worker.onerror = (e) => {\n // A worker-level error (script/import failed to load, e.g. cross-origin CORS block in prod)\n // is treated as fatal: resolve so modelReady stays false -> manual-capture fallback. We do NOT\n // route this through the load retry, since these failures (CORS, unsupported) won't recover on retry.\n if (MainPage_1.enableImageDebug)\n console.error('[card-detection] worker error', e === null || e === void 0 ? void 0 : e.message);\n if (!this.modelReady)\n resolve();\n };\n this.worker.postMessage(this._initPayload());\n });\n return this.modelLoadPromise;\n }\n startDetectionLoop(onStateChange, onUnavailable) {\n return __awaiter(this, void 0, void 0, function* () {\n this.stateChangeCallback = onStateChange; // save first so a re-call updates the callback\n if (this.rafId || this.loopStarting)\n return;\n this.loopStarting = true;\n try {\n if (this.modelLoadPromise) {\n yield (0, detection_failure_1.withTimeout)(this.modelLoadPromise, CARD_DETECTION_CONFIG.LOAD_TIMEOUT_MS, 'model load');\n }\n }\n catch (e) {\n if (MainPage_1.enableImageDebug)\n console.warn('[card-detection]', e.message);\n // timed out → fall through; modelReady stays false → onUnavailable below\n }\n if (!this.modelReady) {\n this.loopStarting = false;\n // Detector unavailable (e.g. CSP-blocked worker). Do NOT force framed=true —\n // that drove a blind auto-capture and the getImageData crash (sc-341281).\n // Hand control to the camera so it can enable manual capture.\n onUnavailable === null || onUnavailable === void 0 ? void 0 : onUnavailable();\n return;\n }\n this.lastFrameTime = 0;\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.lastReason = '';\n this.lastInferMs = 0;\n this.lastValidAt = 0;\n this.rafId = requestAnimationFrame((now) => this._loopTick(now));\n this.loopStarting = false;\n });\n }\n stopDetectionLoop() {\n if (this.rafId) {\n cancelAnimationFrame(this.rafId);\n this.rafId = null;\n }\n this.loopStarting = false;\n this.inferenceBusy = false;\n this.cardPresent = false;\n this.scoreEma = 0;\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames = 0;\n this.isDocumentProperlyFramed = false;\n this.candidateBorder = 'none';\n this.candidateBorderCount = 0;\n this.activeBorder = 'none';\n this.lastValidAt = 0;\n this.setBorderState('none');\n }\n // Terminal teardown — stops the loop and releases the worker. Call when the\n // camera is closed for good (card-camera `_cleanup`), not on pause.\n dispose() {\n this.stopDetectionLoop();\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n this.modelReady = false;\n this.modelLoadPromise = null;\n }\n _reinitWorker() {\n if (!this.worker)\n return;\n this.worker.postMessage(this._initPayload());\n }\n _initPayload() {\n return {\n type: 'init',\n input: CARD_DETECTION_CONFIG.INPUT_SIZE,\n modelUrl: new URL(CARD_DETECTION_CONFIG.MODEL_URL, location.href).href,\n wasmDir: new URL(CARD_DETECTION_CONFIG.ORT_DIR, location.href).href,\n };\n }\n // Tear down + reload the worker/model from scratch (user Retry).\n reloadModel() {\n return __awaiter(this, void 0, void 0, function* () {\n if (this.worker) {\n this.worker.terminate();\n this.worker = null;\n }\n this.modelReady = false;\n this.modelLoadPromise = null;\n this._loadAttempt = 0;\n yield this.loadModel();\n });\n }\n setBorderState(state) {\n if (!this.overlayElement)\n return;\n const borders = [\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.TOP_BORDER}`),\n this.overlayElement.querySelector(`#${camera_1.ELEMENT_IDS.BOTTOM_BORDER}`),\n ];\n borders.forEach(el => {\n if (!el)\n return;\n el.classList.remove('border-element--valid', 'border-element--invalid');\n if (state !== 'none')\n el.classList.add(`border-element--${state}`);\n });\n }\n _loopTick(now) {\n var _a;\n this.rafId = requestAnimationFrame((n) => this._loopTick(n));\n if (now - this.lastFrameTime < 1000 / CARD_DETECTION_CONFIG.THROTTLE_FPS)\n return;\n // Watchdog: a frame out longer than INFERENCE_STUCK_MS means the worker died or\n // dropped it. Clear the busy flag so we recover - otherwise it stays true forever\n // and detection just quietly stops.\n if (this.inferenceBusy) {\n if (now - this.inferenceStartedAt < CARD_DETECTION_CONFIG.INFERENCE_STUCK_MS)\n return;\n if (MainPage_1.enableImageDebug)\n console.warn('[card-detection] inference watchdog fired, resetting stuck frame');\n this.inferenceBusy = false;\n }\n if (!this.worker || !this.modelReady)\n return;\n if (!((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.videoWidth))\n return;\n this.lastFrameTime = now;\n this.inferenceBusy = true;\n this.inferenceStartedAt = now;\n // Grab the frame as a transferable ImageBitmap, shrunk during decode to about\n // INPUT_SIZE on the long side. Sending the full 1280x960 every time and resizing\n // in the worker was wasteful. Resolution doesn't matter to detection - the guide is\n // normalised and the worker letterboxes whatever it gets.\n const N = CARD_DETECTION_CONFIG.INPUT_SIZE;\n const vw = this.videoElement.videoWidth, vh = this.videoElement.videoHeight;\n const s = N / Math.max(vw, vh);\n const rw = Math.max(1, Math.round(vw * s)), rh = Math.max(1, Math.round(vh * s));\n createImageBitmap(this.videoElement, { resizeWidth: rw, resizeHeight: rh, resizeQuality: 'low' })\n .then((bitmap) => {\n if (!this.worker) {\n bitmap.close();\n this.inferenceBusy = false;\n return;\n }\n this.worker.postMessage({ type: 'frame', bitmap }, [bitmap]);\n })\n .catch(() => { this.inferenceBusy = false; });\n }\n // Worker posted a detection result — do the (cheap) framing geometry against\n // the current guide on the main thread, then debounce.\n _onResult(m) {\n var _a, _b, _c, _d, _e;\n this.inferenceBusy = false;\n if (!m.ok)\n return;\n this.lastInferMs = (_a = m.inferMs) !== null && _a !== void 0 ? _a : 0;\n const state = this._evaluateBox((_b = m.score) !== null && _b !== void 0 ? _b : 0, (_c = m.box) !== null && _c !== void 0 ? _c : null, (_d = m.W) !== null && _d !== void 0 ? _d : 0, (_e = m.H) !== null && _e !== void 0 ? _e : 0);\n if (state === 'valid')\n this.lastValidAt = performance.now();\n this._updateBorderDebounced(state);\n this._updateDebounce(state);\n }\n // Turns a raw detection into a state: below the confidence gate is 'no-card', a card\n // spilling out of the guide or too small for it is 'invalid', otherwise 'valid'.\n _evaluateBox(score, box, W, H) {\n // Smooth the score first so one dropout frame can't kick us out of the locked\n // state. A frame with no box counts as 0 and pulls the average down.\n const a = CARD_DETECTION_CONFIG.CONF_EMA_ALPHA;\n this.scoreEma = a * (box ? score : 0) + (1 - a) * this.scoreEma;\n const conf = this.scoreEma;\n // Hysteresis: once we've locked onto a card we let the score sag to CONF_EXIT before\n // giving up; until then it has to clear the higher CONF_ENTER. Stops the flicker.\n const gate = this.cardPresent ? CARD_DETECTION_CONFIG.CONF_EXIT : CARD_DETECTION_CONFIG.CONF_ENTER;\n if (!box || conf <= gate) {\n this.cardPresent = false;\n this._trace('no-card', conf, null);\n return 'no-card';\n }\n this.cardPresent = true;\n const guide = this._computeNormalizedGuide();\n if (!guide || !W || !H) {\n this._trace('VALID', conf, null);\n return 'valid';\n } // no guide → conf-only\n const gx1 = guide.left * W, gy1 = guide.top * H;\n const gx2 = (guide.left + guide.width) * W, gy2 = (guide.top + guide.height) * H;\n const gArea = Math.max(1, (gx2 - gx1) * (gy2 - gy1));\n const gMin = Math.min(gx2 - gx1, gy2 - gy1);\n // fill = how much of the guide the card covers (card too far → low fill).\n const inter = Math.max(0, Math.min(box.x2, gx2) - Math.max(box.x1, gx1))\n * Math.max(0, Math.min(box.y2, gy2) - Math.max(box.y1, gy1));\n const fill = inter / gArea;\n // containment = card stays inside the guide (+slack); spilling out → \"out of box\".\n const eps = CARD_DETECTION_CONFIG.CONTAINMENT_SLACK_RATIO * gMin;\n const contained = box.x1 >= gx1 - eps && box.y1 >= gy1 - eps\n && box.x2 <= gx2 + eps && box.y2 <= gy2 + eps;\n let result;\n let reason;\n if (!contained) {\n result = 'invalid';\n reason = 'out-of-box';\n }\n else if (fill < CARD_DETECTION_CONFIG.MIN_FILL_RATIO) {\n result = 'invalid';\n reason = 'too-small';\n }\n else {\n result = 'valid';\n reason = 'VALID';\n }\n this._trace(reason, conf, fill);\n return result;\n }\n // Guide rect as fractions of the raw (un-mirrored) frame - the space the worker's boxes\n // are in. Two gotchas to undo:\n // 1. the <video> is object-fit:cover, so it scales the frame to fill the element and\n // crops the rest. That means a fraction of what you see on screen isn't the same\n // fraction of the real frame. Skip this and a card poking into the cropped-off strip\n // looks \"inside the guide\" to the model. So we map display fractions back through\n // the cover crop.\n // 2. on desktop the video is mirrored (scaleX(-1)), so flip X back.\n _computeNormalizedGuide() {\n if (!this.overlayElement || !this.videoElement)\n return null;\n const oRect = this.overlayElement.getBoundingClientRect();\n const vRect = this.videoElement.getBoundingClientRect();\n if (!vRect.width || !vRect.height)\n return null;\n const VW = this.videoElement.videoWidth, VH = this.videoElement.videoHeight;\n if (!VW || !VH)\n return null;\n // Guide as fractions of the DISPLAYED video box.\n let left = (oRect.left - vRect.left) / vRect.width;\n let top = (oRect.top - vRect.top) / vRect.height;\n let width = oRect.width / vRect.width;\n let height = oRect.height / vRect.height;\n // Walk those display fractions back through the (centred) cover crop.\n const videoAspect = VW / VH, displayAspect = vRect.width / vRect.height;\n if (videoAspect >= displayAspect) {\n // frame is wider than the element, so the sides get cropped, full height shows\n const visW = displayAspect / videoAspect;\n left = (1 - visW) / 2 + left * visW;\n width = width * visW;\n }\n else {\n // frame is taller than the element, so top/bottom get cropped, full width shows\n const visH = videoAspect / displayAspect;\n top = (1 - visH) / 2 + top * visH;\n height = height * visH;\n }\n if (!(0, deviceDetection_1.isMobileDevice)())\n left = 1 - left - width;\n return { left, top, width, height };\n }\n // Diagnostic console trace. Only logs when the state actually changes, so no spam.\n _trace(reason, conf, fill) {\n if (reason === this.lastReason)\n return;\n this.lastReason = reason;\n if (MainPage_1.enableImageDebug)\n console.log(`[detect] ${reason} | conf=${conf.toFixed(3)} fill=${fill == null ? '-' : fill.toFixed(2)} (min ${CARD_DETECTION_CONFIG.MIN_FILL_RATIO}) gate=${CARD_DETECTION_CONFIG.CONF_ENTER}/${CARD_DETECTION_CONFIG.CONF_EXIT} | infer=${this.lastInferMs.toFixed(0)}ms`);\n }\n // Border colour debounce, deliberately lopsided: going green needs BORDER_STABLE_FRAMES\n // good frames in a row so we don't flash green by mistake, but dropping green only needs\n // BORDER_CLEAR_FRAMES, so pulling the card out clears it almost immediately.\n _updateBorderDebounced(rawState) {\n const target = rawState === 'valid' ? 'valid' :\n rawState === 'no-card' ? 'none' :\n 'invalid';\n if (target === this.candidateBorder) {\n this.candidateBorderCount++;\n }\n else {\n this.candidateBorder = target;\n this.candidateBorderCount = 1;\n }\n const needed = target === 'valid'\n ? CARD_DETECTION_CONFIG.BORDER_STABLE_FRAMES\n : CARD_DETECTION_CONFIG.BORDER_CLEAR_FRAMES;\n if (this.candidateBorderCount >= needed && target !== this.activeBorder) {\n this.activeBorder = target;\n this.setBorderState(target);\n }\n }\n _updateDebounce(rawState) {\n var _a;\n const wasFramed = this.isDocumentProperlyFramed;\n if (rawState === 'valid') {\n this.consecutiveInvalidFrames = 0;\n this.consecutiveValidFrames++;\n if (!this.isDocumentProperlyFramed &&\n this.consecutiveValidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_ENABLE) {\n this.isDocumentProperlyFramed = true;\n }\n }\n else {\n this.consecutiveValidFrames = 0;\n this.consecutiveInvalidFrames++;\n if (this.isDocumentProperlyFramed &&\n this.consecutiveInvalidFrames >= CARD_DETECTION_CONFIG.STABLE_FRAMES_TO_DISABLE) {\n this.isDocumentProperlyFramed = false;\n }\n }\n if (this.isDocumentProperlyFramed !== wasFramed) {\n (_a = this.stateChangeCallback) === null || _a === void 0 ? void 0 : _a.call(this, this.isDocumentProperlyFramed);\n }\n }\n}\nexports[\"default\"] = CARDDETECTIONSDK;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/card-detection.ts?");
4015
4015
 
4016
4016
  /***/ }),
4017
4017
 
@@ -4026,6 +4026,17 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4026
4026
 
4027
4027
  /***/ }),
4028
4028
 
4029
+ /***/ "./src/plugins/detection-failure.ts":
4030
+ /*!******************************************!*\
4031
+ !*** ./src/plugins/detection-failure.ts ***!
4032
+ \******************************************/
4033
+ /***/ ((__unused_webpack_module, exports) => {
4034
+
4035
+ "use strict";
4036
+ eval("\nObject.defineProperty(exports, \"__esModule\", ({ value: true }));\nexports.withTimeout = exports.computeBackoffMs = exports.classifyFailure = void 0;\n// Decide whether a worker/model load failure is worth retrying.\n// Network/fetch stages can recover; environment stages (CSP, unsupported, bad runtime) cannot.\nfunction classifyFailure(stage) {\n const s = stage.toLowerCase();\n if (s.includes(\"importscripts\") || s.includes(\"session-create\") || s.includes(\"fetch\") || s.includes(\"warmup\")) {\n return \"retryable\";\n }\n return \"fatal\"; // ort-global, config, offscreen-canvas, unknown → don't loop\n}\nexports.classifyFailure = classifyFailure;\n// Exponential backoff: 500ms, 1s, 2s, 4s (capped at 4s).\nfunction computeBackoffMs(attempt) {\n return Math.min(4000, 500 * 2 ** attempt);\n}\nexports.computeBackoffMs = computeBackoffMs;\n// Reject if `p` doesn't settle within `ms`. Used so a stalled fetch can't hang the loader forever.\nfunction withTimeout(p, ms, label) {\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);\n p.then((v) => { clearTimeout(t); resolve(v); }, (e) => { clearTimeout(t); reject(e); });\n });\n}\nexports.withTimeout = withTimeout;\n\n\n//# sourceURL=webpack://PWUISDK/./src/plugins/detection-failure.ts?");
4037
+
4038
+ /***/ }),
4039
+
4029
4040
  /***/ "./src/plugins/face-camera.ts":
4030
4041
  /*!************************************!*\
4031
4042
  !*** ./src/plugins/face-camera.ts ***!
@@ -4055,7 +4066,7 @@ eval("\nvar __awaiter = (this && this.__awaiter) || function (thisArg, _argument
4055
4066
  /***/ ((__unused_webpack_module, exports) => {
4056
4067
 
4057
4068
  "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 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?");
4069
+ 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 doc_scanner_unavailable: \"Couldn't start the document scanner. Capture manually, or retry.\",\n btn_retry: \"Retry\",\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
4070
 
4060
4071
  /***/ }),
4061
4072
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "pipwave-ekyc-uikit",
3
- "version": "3.1.1",
3
+ "version": "3.1.2-beta.1",
4
4
  "description": "",
5
5
  "main": "dist/pw-bundle.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
7
+ "test": "vitest run",
8
8
  "dev": "cross-env build_env=development webpack serve --mode=development --open",
9
9
  "build": "cross-env build_env=production webpack"
10
10
  },
@@ -17,6 +17,7 @@
17
17
  "cross-env": "^10.1.0",
18
18
  "css-loader": "^6.8.1",
19
19
  "html-webpack-plugin": "^5.5.3",
20
+ "onnxruntime-web": "^1.20.1",
20
21
  "postcss": "^8.4.26",
21
22
  "postcss-import": "^15.1.0",
22
23
  "postcss-loader": "^7.3.3",
@@ -26,6 +27,7 @@
26
27
  "style-loader": "^3.3.3",
27
28
  "tailwindcss": "^3.3.3",
28
29
  "ts-loader": "^9.4.4",
30
+ "vitest": "^4.1.9",
29
31
  "webpack": "^5.88.1",
30
32
  "webpack-cli": "^5.1.4",
31
33
  "webpack-dev-server": "^4.15.1"
@@ -0,0 +1,2 @@
1
+ import { defineConfig } from "vitest/config";
2
+ export default defineConfig({ test: { environment: "node", include: ["src/**/*.test.ts"] } });