/** * Copyright (c) Microblink Ltd. All rights reserved. */ import { Component, Element, Event, EventEmitter, Host, h, Method, Prop, State, Watch } from '@stencil/core'; import { CameraEntry, CameraExperience, CameraExperienceState, CameraExperienceStateDuration, CameraExperienceTimeoutDurations } from '../../../utils/data-structures'; import { setWebComponentParts, classNames, getWebComponentParts } from '../../../utils/generic.helpers'; import { TranslationService } from '../../../utils/translation.service'; import * as Utils from './mb-camera-experience.utils'; @Component({ tag: 'mb-camera-experience', styleUrl: 'mb-camera-experience.scss', shadow: true, }) export class MbCameraExperience { @State() cameraCursorBarcodeClassName: string = 'rectangle'; @State() cameraCursorIdentityCardClassName: string = 'reticle'; @State() cameraCursorPaymentCardClassName: string = 'rectangle'; @State() scanningLineBarcodeClassName: string; @State() scanningLinePaymentCardClassName: string; @State() cameraMessageIdentityCardContent: any; @State() cameraMessageIdentityCardClassName: string = 'message'; private cameraMessageIdentityCard!: HTMLParagraphElement; private cameraMessagePaymentCard!: HTMLParagraphElement; private cameraMessageBarcode!: HTMLParagraphElement; private cameraToolbar!: HTMLMbCameraToolbarElement; private cardIdentityElement!: HTMLDivElement; private cameraStateChangeId: number = 0; private cameraStateInProgress: boolean = false; private flipCameraStateInProgress: boolean = false; /** * Choose desired camera experience. * * Each experience type must be implemented in this component. */ @Prop() type: CameraExperience; /** * Configure camera experience state timeout durations */ @Prop() cameraExperienceStateDurations: CameraExperienceTimeoutDurations = null; /** * Unless specifically granted by your license key, you are not allowed to * modify or remove the Microblink logo displayed on the bottom of the camera * overlay. */ @Prop() showOverlay: boolean = true; /** * Instance of TranslationService passed from root component. */ @Prop() translationService: TranslationService; /** * Api state passed from root component. */ @Prop() apiState: string; /** * Camera horizontal state passed from root component. * * Horizontal camera image can be mirrored */ @Prop({ mutable: true }) cameraFlipped: boolean = false; /** * Show scanning line on camera */ @Prop() showScanningLine: boolean = false; /** * Show camera feedback message on camera for Barcode scanning */ @Prop() showCameraFeedbackBarcodeMessage: boolean = false; @Prop() clearIsCameraActive: boolean = false; @Watch('apiState') apiStateHandler(apiState: string, _oldValue: string) { if (apiState === '' && (this.type === CameraExperience.CardSingleSide || this.type === CameraExperience.CardMultiSide)) this.cardIdentityElement.classList.add('visible'); else this.cardIdentityElement.classList.remove('visible'); } /** * Emitted when user clicks on 'X' button. */ @Event() close: EventEmitter; /** * Emitted when camera stream becomes active. */ @Event() setIsCameraActive: EventEmitter; /** * Emitted when user selects a different camera device. */ @Event() changeCameraDevice: EventEmitter; /** * Host element as variable for manipulation */ @Element() hostEl: HTMLElement; /** * Change active camera. */ @Method() async setActiveCamera(cameraId: string) { this.cameraToolbar.setActiveCamera(cameraId); } /** * Populate list of camera devices. */ @Method() async populateCameraDevices() { await this.cameraToolbar.populateCameraDevices(); } /** * Emitted when user clicks on Flip button. */ @Event() flipCameraAction: EventEmitter; /** * Method is exposed outside which allow us to control Camera Flip state from parent component. */ @Method() async setCameraFlipState(isFlipped: boolean) { this.cameraFlipped = isFlipped; } /** * Set camera state which includes animation and message. */ @Method() setState(state: CameraExperienceState, isBackSide: boolean = false, force: boolean = false): Promise { return new Promise((resolve) => { if (!force && (!state || this.cameraStateInProgress || this.flipCameraStateInProgress)) { resolve(); return; } this.cameraStateInProgress = true; let cameraStateChangeId = this.cameraStateChangeId + 1; this.cameraStateChangeId = cameraStateChangeId; if (state === CameraExperienceState.Flip) { this.flipCameraStateInProgress = true; } const stateClass = Utils.getStateClass(state); switch (this.type) { case CameraExperience.CardSingleSide: case CameraExperience.CardMultiSide: this.cameraCursorIdentityCardClassName = `reticle ${stateClass}`; break; case CameraExperience.Barcode: stateClass === 'is-detection' && this.showScanningLine ? this.scanningLineBarcodeClassName = 'is-active' : this.scanningLineBarcodeClassName = ''; this.cameraCursorBarcodeClassName = `rectangle ${stateClass}`; break; case CameraExperience.PaymentCard: stateClass === 'is-default' && this.showScanningLine ? this.scanningLinePaymentCardClassName = 'is-active' : this.scanningLinePaymentCardClassName = ''; this.cameraCursorPaymentCardClassName = `rectangle ${stateClass}`; break; } this.setMessage(state, isBackSide, this.type); window.setTimeout(() => { if (this.flipCameraStateInProgress && state === CameraExperienceState.Flip) { this.flipCameraStateInProgress = false; } if (this.cameraStateChangeId === cameraStateChangeId) { this.cameraStateInProgress = false; } resolve(); }, this.getCameraExperienceStateDuration(state)); }); } private getCameraExperienceStateDuration(state: CameraExperienceState): number { return this.cameraExperienceStateDurations ? this.getStateDurationFromUserInput(state) : CameraExperienceStateDuration.get(state); } private getStateDurationFromUserInput(state: CameraExperienceState): number { const cameraExperienceStateDurationMap = new Map(Object.entries(this.cameraExperienceStateDurations)); const stateAdjusted = state[0].toLocaleLowerCase() + state.slice(1); const duration = cameraExperienceStateDurationMap.get(stateAdjusted); return duration || CameraExperienceStateDuration.get(state); } /** * Set camera state to initial method. */ @Method() resetState(): Promise { return new Promise((resolve) => { // Reset flags this.cameraStateChangeId = 0; this.cameraStateInProgress = false; this.flipCameraStateInProgress = false; // Reset DOM while (this.cameraMessageIdentityCard.firstChild) { this.cameraMessageIdentityCard.removeChild(this.cameraMessageIdentityCard.firstChild); } while (this.cameraMessagePaymentCard.firstChild) { this.cameraMessagePaymentCard.removeChild(this.cameraMessagePaymentCard.firstChild); } while (this.cameraMessageBarcode.firstChild) { this.cameraMessageBarcode.removeChild(this.cameraMessageBarcode.firstChild); } resolve(); }); } private flipCamera(): void { this.flipCameraAction.emit(); } private handleStop() { this.close.emit(); } private setMessage(state: CameraExperienceState, isBackSide: boolean, type: CameraExperience): void { const message = this.getStateMessage(state, isBackSide, type); switch (type) { case CameraExperience.CardSingleSide: case CameraExperience.CardMultiSide: while (this.cameraMessageIdentityCard.firstChild) { this.cameraMessageIdentityCard.removeChild(this.cameraMessageIdentityCard.firstChild); } if (message) { this.cameraMessageIdentityCard.appendChild(message); } this.cameraMessageIdentityCardClassName = message && message !== null ? 'message is-active' : 'message'; break; case CameraExperience.PaymentCard: while (this.cameraMessagePaymentCard.firstChild) { this.cameraMessagePaymentCard.removeChild(this.cameraMessagePaymentCard.firstChild); } if (message) { this.cameraMessagePaymentCard.appendChild(message); } this.cameraMessagePaymentCard.setAttribute('class', message && message !== null ? 'message is-active' : 'message'); break; case CameraExperience.Barcode: while (this.cameraMessageBarcode.firstChild) { this.cameraMessageBarcode.removeChild(this.cameraMessageBarcode.firstChild); } if (this.showCameraFeedbackBarcodeMessage) { if (message) { this.cameraMessageBarcode.appendChild(message); } this.cameraMessageBarcode.setAttribute('class', message && message !== null ? 'message is-active' : 'message'); } break; default: // Do nothing } } private getStateMessage(state: CameraExperienceState, isBackSide: boolean = false, type: CameraExperience): HTMLSpanElement|null { const getStateMessageAsHTML = (message: string|Array): HTMLSpanElement => { if (message) { const messageArray = typeof message === 'string' ? [ message ] : message; const children = []; while (messageArray.length) { const sentence = messageArray.shift(); children.push(document.createTextNode(sentence)); if (messageArray.length) { children.push(document.createElement('br')); } } const spanElement = document.createElement('span'); while (children.length) { spanElement.appendChild(children.shift()); } return spanElement; } } switch (state) { case CameraExperienceState.Default: if (type === CameraExperience.Barcode && this.showCameraFeedbackBarcodeMessage) { return getStateMessageAsHTML(this.translationService.i('camera-feedback-barcode-message')); } const key = isBackSide ? 'camera-feedback-scan-back' : 'camera-feedback-scan-front'; return getStateMessageAsHTML(this.translationService.i(key)); case CameraExperienceState.MoveFarther: return getStateMessageAsHTML(this.translationService.i('camera-feedback-move-farther')); case CameraExperienceState.MoveCloser: return getStateMessageAsHTML(this.translationService.i('camera-feedback-move-closer')); case CameraExperienceState.AdjustAngle: return getStateMessageAsHTML(this.translationService.i('camera-feedback-adjust-angle')); case CameraExperienceState.Flip: return getStateMessageAsHTML(this.translationService.i('camera-feedback-flip')); case CameraExperienceState.Classification: case CameraExperienceState.Detection: return type === CameraExperience.Barcode ? getStateMessageAsHTML(this.translationService.i('camera-feedback-barcode-message')) : null; case CameraExperienceState.Done: case CameraExperienceState.DoneAll: default: return null; } } private handleChangeCameraDevice(camera: CameraEntry) { this.changeCameraDevice.emit(camera); } componentDidLoad() { setWebComponentParts(this.hostEl); const parts = getWebComponentParts(this.hostEl.shadowRoot); this.hostEl.setAttribute('exportparts', parts.join(', ')); } render() { return ( {/* Barcode camera experience */}

this.cameraMessageBarcode = el as HTMLParagraphElement }>

{/* Identity card camera experience */}
this.cardIdentityElement = el as HTMLDivElement} class={ classNames({ visible: this.type === CameraExperience.CardSingleSide || this.type === CameraExperience.CardMultiSide }) }>

this.cameraMessageIdentityCard = el as HTMLParagraphElement }>

{/* PaymentCard camera experience */}

this.cameraMessagePaymentCard = el as HTMLParagraphElement }>

this.handleStop()} onFlipEvent={() => this.flipCamera()} onChangeCameraDevice={(ev: CustomEvent) => this.handleChangeCameraDevice(ev.detail)} ref={ el => this.cameraToolbar = el as HTMLMbCameraToolbarElement } >
); } }