diff --git a/.gitignore b/.gitignore index 27fe55e..ac18fa8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ dist/ es/ lib/ -resources/ types/ # Generic @@ -31,4 +30,4 @@ UserInterfaceState.xcuserstate # UI components demo/ ui/loader/ -ui/resources/ \ No newline at end of file +ui/resources/ diff --git a/.nvmrc b/.nvmrc index a66526b..f4e6f1b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -12.18 \ No newline at end of file +12.18.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 55148f0..2747d81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Release notes +## 7.7.4 + +### Breaking changes + +* We've changed the way how recognizer options are set up when using the UI component + * You can now specify how a recognizer should behave by using the new `recognizerOptions` property. + * To see the full list of available recognizer options, as well as examples on how to use them, check out the [relevant source code](ui/src/components/blinkcard-in-browser/blinkcard-in-browser.tsx). + +### Performance improvements + +* We've added three different flavors of WebAssembly builds to the SDK, to provide better performance across all browsers + * Unless defined otherwise, the SDK will load the best possible bundle during initialization: + * `Basic` Same as the existing WebAssembly build, most compatible, but least performant. + * `Advanced` WebAssembly build that provides better performance but requires a browser with advanced features. + * `AdvancedWithThreads` Most performant WebAssembly build which requires a proper setup of COOP and COEP headers on the server-side. + * For more information about different WebAssembly builds and how to use them properly, check out the [relevant section](README.md/#deploymentGuidelines) in our official documentation + +### SDK changes + +* Constructor of `VideoRecognizer` class is now public + +### Camera management updates + +* We've enabled camera image flipping + * Method `flipCamera` has been added to [`VideoRecognizer`](src/MicroblinkSDK/VideoRecognizer.ts). + * You can now let your users mirror the camera image vertically in case they find it easier to scan that way. + * By default, the UI component will display a flip icon in the top left corner once the camera is live. +* We've improved camera management on devices with multiple cameras + * Method `createVideoRecognizerFromCameraStream` has been extended in [`VideoRecognizer` class](src/MicroblinkSDK/VideoRecognizer.ts). + * Attribute `[camera-id]` has been added to the UI component so that your users can preselect their desired camera. + +### Bugfixes + +* We fixed the initialization problem that prevented the SDK from loading on iOS 13 and older versions + ## 7.7.3 * Fixed NPM package to include UI component. diff --git a/README.md b/README.md index 5de8276..396dd47 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ _PhotoPay_ In-browser SDK enables you to perform scans of various payment barcodes in your web app, directly within the web browser, without the need for sending the image to servers for processing. You can integrate the SDK into your web app simply by following the instructions below and your web app will be able to scan and process data from the payment barcodes of various national standards. For a list of all supported standards, check [this paragraph](#photopay_recognizers). -For more information on how to integrate the _PhotoPay_ SDK into your web app read the instructions below. Make sure you read the latest [changelog](CHANGELOG.md) for most recent changes and improvements. +For more information on how to integrate the _PhotoPay_ SDK into your web app read the instructions below. Make sure you read the latest [CHANGELOG.md](CHANGELOG.md) file for most recent changes and improvements. -Check out the [official demo app](https://demo.microblink.com/in-browser-sdk/photopay/index.html) or live examples to see the _BlinkID_ SDK in action: +Check out the [official demo app](https://demo.microblink.com/in-browser-sdk/photopay/index.html) or live examples to see the _PhotoPay_ SDK in action: 1. Example with UI component at [Codepen](https://codepen.io/microblink/pen/dyMKdxQ) 2. Example without UI at [Codepen](https://codepen.io/microblink/pen/ZEQNNZg) @@ -15,8 +15,9 @@ Finally, check out the [examples directory](examples) to see how to integrate th _PhotoPay_ In-browser SDK is meant to be used natively in a web browser. It will not work correctly within a iOS/Android WebView or NodeJS backend service. -# Table of contents +## Table of contents +* [Components of SDK](#components-of-sdk) * [Integration instructions](#integration) * [Obtaining a license key](#obtainingalicensekey) * [Installation](#installation) @@ -45,7 +46,11 @@ _PhotoPay_ In-browser SDK is meant to be used natively in a web browser. It will * [Slovakia](#photopay_recognizers_slovakia) * [Slovenia](#photopay_recognizers_slovenia) * [Switzerland](#photopay_recognizers_switzerland) -* [UI component](#uiComponent) +* [Recognizer settings](#recognizerSettings) +* [Technical requirements](#technicalRequirements) +* [Supported browsers](#webassembly-support) +* [Camera devices](#camera-devices) +* [Device support](#device-support) * [Troubleshooting](#troubleshoot) * [Integration problems](#integrationProblems) * [SDK problems](#sdkProblems) @@ -54,28 +59,51 @@ _PhotoPay_ In-browser SDK is meant to be used natively in a web browser. It will * [FAQ and known issues](#faq) * [Additional info](#info) +## Components of SDK -# Integration instructions +PhotoPay In-browser SDK consists of: -This repository contains WebAssembly file and support JS files which contains the core implementation of _PhotoPay_ functionalities. +* WASM library that recognizes a document a user is holding and extracts an image of the most suitable frame from the camera feed. +* Web component with a prebuilt and customizable UI, which acts as a wrapper for the WASM library to provide a straightforward integration. -In order to make integration of the WebAssembly easier and more developer friendly, a JavaScript/TypeScript support code is also provided, giving an easy to use integration API to the developer. +You can add it to your website or web app in two ways: -This repository also contains a sample JS/TS integration app which demonstrates how you can integrate the _PhotoPay_ into your web app. +1. For the simplest form of integration, use a web component with a prebuilt and customizable UI. + * Follow the integration instructions in the [ui/README.md](ui/README.md) file. + * You can find the source code of example applications in the [ui/examples](ui/examples) directory. +2. For an advanced form of integration where UI has to be built from scratch, use a WASM library instead. + * See the integration instructions [here](#integration). + * Find the source code of example applications in the [examples](examples) directory. -_PhotoPay_ requires a browser with a support for [WebAssembly](https://webassembly.org), but works best with latest versions of Firefox, Chrome, Safari and Microsoft Edge. It's worth noting that scan performance depends on the device processing capabilities. +## Integration instructions -## Obtaining a license key +This repository contains WebAssembly files and supporting JS files which contain the core implementation of PhotoPay functionalities. -Using _PhotoPay_ in your web app requires a valid license key. +In order to make integration of the WebAssembly easier and more developer friendly, a JavaScript/TypeScript support code is also provided, giving you an easy-to-use integration API. + +This repository also contains a sample JS/TS integration app which demonstrates how you can integrate the PhotoPay into your web app. + +PhotoPay will work in any browser that supports [WebAssembly](https://webassembly.org), but works best with the latest versions of Firefox, Chrome, Safari and Microsoft Edge. It's worth noting that scan performance depends on the device processing capabilities. + +### Obtaining a license key + +Using PhotoPay in your web app requires a valid license key. You can obtain a free trial license key by registering to [Microblink dashboard](https://microblink.com/login). After registering, you will be able to generate a license key for your web app. -The license key is bound to [fully qualified domain name](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) of your web app, so please make sure you enter the correct name when asked. Also, keep in mind that if you plan to serve your web app from different domains, you will need different license keys. +Make sure you enter a [fully qualified domain name](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) of your web app when filling out the form — the license key will be bound to it. Also, if you plan to serve your web app from different domains, you'll need a license key for each one. + +**Keep in mind:** Versions PhotoPay 7.8.0 and above require an internet connection to work under our new License Management Program. -## Installation +This means your web app has to be connected to the Internet in order for us to validate your trial license key. Scanning or data extraction of documents still happens offline, in the browser itself. -It's recommended to install the stable version via NPM or Yarn: +Once the validation is complete, you can continue using the SDK in an offline mode (or over a private network) until the next check. + +We've added error callback to Microblink SDK to inform you about the status of your license key. + +### Installation + +We recommend you install a stable version via NPM or Yarn: ```sh # NPM @@ -85,18 +113,18 @@ npm install @microblink/photopay-in-browser-sdk yarn add @microblink/photopay-in-browser-sdk ``` -Which then can be used with a module bundler in Node environment: +Which can then be used with a module bundler in Node environment: ```javascript import * as PhotoPaySDK from "@microblink/photopay-in-browser-sdk"; ``` -Source code of PhotoPaySDK is written in TypeScript and types are exposed in the public NPM package, so it's possible +Source code of `PhotoPaySDK` is written in TypeScript and types are exposed in the public NPM package, so it's possible to use the SDK in both JavaScript and TypeScript projects. --- -Alternatively, it's possible to use UMD builds, which can be loaded from [the `dist` folder on unpkg](https://unpkg.com/@microblink/photopay-in-browser-sdk/dist/). The UMD builds make _PhotoPay_ available as a `window.PhotoPaySDK` global variable: +Alternatively, it's possible to use UMD builds, which can be loaded from [the `dist` folder on unpkg](https://unpkg.com/@microblink/photopay-in-browser-sdk/dist/). The UMD builds make `PhotoPaySDK` available as a `window.PhotoPaySDK` global variable: ```html @@ -108,31 +136,33 @@ Finally, it's possible to use ES builds, which can be downloaded from [the `es` import * as PhotoPaySDK from "./es/photopay-sdk.js"; ``` -### WASM Resources +**Important:** Unpkg CDN is used here due to simplicity of usage. It's not intended to be used in production! -After adding the _PhotoPay_ SDK to your project, make sure to include all files from its `resources` folder in your distribution. Those files contain compiled WebAssembly module and support JS code. +#### WASM Resources -Do not add those files to the main app bundle, but rather place them on a publicly available location so SDK can load them at the appropriate time. For example, place the resources in `my-angular-app/src/assets/` folder if using `ng new`, or place the resources in `my-react-app/public/` folder if using `create-react-app`. +After adding PhotoPay SDK to your project, make sure to include all files from its `resources` folder in your distribution. Those files contain a compiled WebAssembly module and support JS code. + +Do not add those files to the main app bundle, but rather place them on a publicly available location so that the SDK can load them at an appropriate time. For example, place the resources in `my-angular-app/src/assets/` folder if using `ng new` or in `my-react-app/public/` folder if using `create-react-app`. For more information on how to setup aforementioned resources, check out the [Configuration of SDK](#sdkConfiguration) section. -### Versions and backward compatibility +#### Versions and backward compatibility -Even though the API is not going to change between minor versions, structure of results for various recognizers can change between minor versions. +Even though the API is not going to change between minor versions, the structure of results for various recognizers might change between minor versions. -This is due to improvements that are made on recognizers with every minor release. +This is due to the improvements we make to our recognizers with every minor release. We suggest you familiarize yourself with what [Recognizer, RecognizerRunner and VideoRecognizer](#availableRecognizers) are before moving on. -It's a good practice to always lock on minor version and to check `CHANGELOG.md` before upgrading to new minor version. +It's a good practice to always lock your minor version and check the [CHANGELOG.md](CHANGELOG.md) file before upgrading to a new minor version. -For example, in `package.json` you should have something like `"@microblink/photopay-in-browser-sdk": "~4.1.1"` instead of default value `"@microblink/photopay-in-browser-sdk": "^4.1.1"`. +For example, in `package.json` you should have something like `"@microblink/photopay-in-browser-sdk": "~4.1.1"` instead of the default `"@microblink/photopay-in-browser-sdk": "^4.1.1"`. -## Performing your first scan +### Performing your first scan -*Note: following code snippets are written in TypeScript, but it's possible to use them in plain JavaScript.* +*Note: the following code snippets are written in TypeScript, but it's possible to use them in plain JavaScript.* -1. Make sure to have a valid license key. Information on how to get a license key can be seen in the [Obtaining a license key](#obtainingalicensekey) section. +1. Make sure you have a valid license key. See [Obtaining a license key](#obtainingalicensekey). -2. Add SDK to your web app by using one of the options provided in the [Installation](#installation) section. +2. Add the SDK to your web app by using one of the options provided in the [Installation](#installation) section. 3. Initialize the SDK using the following code snippet: @@ -163,7 +193,7 @@ For example, in `package.json` you should have something like `"@microblink/phot } ``` -4. Create recognizer objects that will perform image recognition, configure them and use them to create a `RecognizerRunner` object: +4. Create recognizer objects that will perform image recognition, configure them to your needs (to scan specific types of documents, for example) and use them to create a `RecognizerRunner` object: ```typescript import * as PhotoPaySDK from "@microblink/photopay-in-browser-sdk"; @@ -190,8 +220,8 @@ For example, in `package.json` you should have something like `"@microblink/phot if ( error.name === "VideoRecognizerError" ) { // Reason is of type PhotoPaySDK.NotSupportedReason and contains information why video - // recognizer could not be used. Usually this happens when user didn't give permission - // to use the camera or when a hardware or OS error occurs. + // recognizer could not be used. Usually this happens when user didn't grant access to a + // camera or when a hardware or OS error occurs. const reason = ( error as PhotoPaySDK.VideoRecognizerError ).reason; } } @@ -219,17 +249,15 @@ For example, in `package.json` you should have something like `"@microblink/phot recognizer.delete(); ``` - Note that after releasing those objects it is not valid to call any methods on them, as they are literally destroyed. This is required to release memory resources on WebAssembly heap which are not automatically released with JavaScript's garbage collector. Also, note that results returned from `getResult` method are placed on JavaScript's heap and will be cleaned by garbage collector, just like any other normal JavaScript object. - -For more information about available recognizers and `RecognizerRunner`, see [RecognizerRunner and available recognizers](#availableRecognizers). + Note that after releasing those objects it is not valid to call any methods on them, as they are literally destroyed. This is required to release memory resources on WebAssembly heap which are not automatically released with JavaScript's garbage collector. Also, note that results returned from `getResult` method are placed on JavaScript's heap and will be cleaned by its garbage collector, just like any other normal JavaScript object. -## Recognizing still images +### Recognizing still images If you just want to perform recognition of still images and do not need live camera recognition, you can do that as well. -1. Initialize recognizers and `RecognizerRunner` just as in the [steps 1-4 above](#firstScan). +1. Initialize recognizers and `RecognizerRunner` as described in the [steps 1-4 above](#firstScan). -2. Make sure you have the image set to a `HTMLImageElement`. If you only have the URL of the image that needs recognizing, You can attach it to the image element with following code snippet: +2. Make sure you have the image set to a `HTMLImageElement`. If you only have the URL of the image that needs recognizing, you can attach it to the image element with following code snippet: ```typescript const imageElement = document.getElementById( "imageToProcess" ) as HTMLImageElement; @@ -244,13 +272,13 @@ If you just want to perform recognition of still images and do not need live cam const processResult = await recognizerRunner.processImage( imageFrame ); ``` -4. Proceed as in [steps 6-7 above](#firstScan). Note that in there is no `VideoRecognizer` here that needs freeing its resources, but `RecognizerRunner` and recognizers must be deleted using the `delete` method. +4. Proceed as in [steps 6-7 above](#firstScan). Note that you don't have to release any resources of `VideoRecognizer` here as we were only recognizing a single image, but `RecognizerRunner` and recognizers must be deleted using the `delete` method. -## Configuration of SDK +### Configuration of SDK -It's possible to modify default behaviour of the SDK before WASM module is loaded. +You can modify the default behaviour of the SDK before a WASM module is loaded. -Following code snippet shows how to configure the SDK and which non-development options are available: +Check out the following code snippet to learn how to configure the SDK and which non-development options are available: ```typescript // Create instance of WASM SDK load settings @@ -270,20 +298,26 @@ loadSettings.allowHelloMessage = true; * Absolute location of WASM and related JS/data files. Useful when resource files should be loaded over CDN, or * when web frameworks/libraries are used which store resources in specific locations, e.g. inside "assets" folder. * - * Important: if engine is hosted on another origin, CORS must be enabled between two hosts. That is, server where + * Important: if the engine is hosted on another origin, CORS must be enabled between two hosts. That is, server where * engine is hosted must have 'Access-Control-Allow-Origin' header for the location of the web app. * - * Important: SDK and WASM resources must be from the same version of package. + * Important: SDK and WASM resources must be from the same version of a package. * * Default value is empty string, i.e. "". In case of empty string, value of "window.location.origin" property is * going to be used. */ loadSettings.engineLocation = ""; +/** + * Type of the WASM that will be loaded. By default, if not set, the SDK will automatically determine the best WASM + * to load. + */ +wasmType: WasmType | null = null; + /** * Optional callback function that will report the SDK loading progress. * - * This can be useful for displaying progress bar for users on slow connections. + * This can be useful for displaying progress bar to users with slow connections. * * Default value is "null". * @@ -296,43 +330,59 @@ loadSettings.loadProgressCallback = null; PhotoPaySDK.loadWasmModule( loadSettings ).then( ... ); ``` -There are some additonal development options which can be seen in the configuration class [WasmLoadSettings](src/MicroblinkSDK/WasmLoadSettings.ts). +There are some additional options which can be seen in the configuration class [WasmLoadSettings](src/MicroblinkSDK/WasmLoadSettings.ts). + +### Deployment guidelines + +This section contains information on how to deploy a web app which uses PhotoPay In-browser SDK. + +#### HTTPS + +Make sure to serve the web app over a HTTPS connection. + +Otherwise, the browser will block access to a web camera and remote scripts due to security policies. + +#### Deployment of WASM files + +WASM wrapper contain three different builds: -## Deployment guidelines +* `Basic` -This section contains information on how to deploy a web app which uses _PhotoPay_ In-browser SDK. + * The WASM that will be loaded will be most compatible with all browsers that support the WASM, but will lack features that could be used to improve performance. + +* `Advanced` -### HTTPS + * The WASM that will be loaded will be built with advanced WASM features, such as bulk memory, non-trapping floating point and sign extension. Such WASM can only be executed in browsers that support those features. Attempting to run this WASM in a non-compatible browser will crash your app. -Make sure to serve the web app on HTTPS protocol. +* `AdvancedWithThreads` -Otherwise, web camera and loading of remote scripts will be blocked by web browser due to security policies. + * The WASM that will be loaded will be build with advanced WASM features, just like above. Additionally, it will be also built with support for multi-threaded processing. This feature requires a browser with support for both advanced WASM features and `SharedArrayBuffer`. -### Deployment of WASM files + * For multi-threaded processing there are some things that needs to be set up additionally, like COOP and COEP headers, more info about web server setup can be found [here](#wasmsetup). -_Files: resources/PhotoPayWasmSDK.{data,js,wasm}_ +_Files: resources/{basic,advanced,advanced-threads}/PhotoPayWasmSDK.{data,js,wasm}_ -#### Server Configuration +##### Server Configuration -When browser loads the `.wasm` file it needs to compile it to the native code. This is unlike JavaScript code, which is interpreted and compiled to native code only if needed ([JIT, a.k.a. Just-in-time compilation](https://en.wikipedia.org/wiki/Just-in-time_compilation)). Therefore, before _PhotoPay_ is loaded, the browser must download and compile the provided `.wasm` file. +If you know how WebAssembly works, then you'll know a browser will load the `.wasm` file it needs to compile it to the native code. This is unlike JavaScript code, which is interpreted and compiled to native code only if needed ([JIT, a.k.a. Just-in-time compilation](https://en.wikipedia.org/wiki/Just-in-time_compilation)). Therefore, before PhotoPay is loaded, the browser must download and compile the provided `.wasm` file. In order to make this faster, you should configure your web server to serve `.wasm` files with `Content-Type: application/wasm`. This will instruct the browser that this is a WebAssembly file, which most modern browsers will utilize to perform streaming compilation, i.e. they will start compiling the WebAssembly code as soon as first bytes arrive from the server, instead of waiting for the entire file to download. -For more information about streaming compilation, check [this article on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming). +For more information about streaming compilation, check [this article from MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming). If your server supports serving compressed files, you should utilize that to minimize the download size of your web app. It's easy to notice that `.wasm` file is not a small file, but it is very compressible. This is also true for all other files that you need to serve for your web app. -For more information about configuring your web server for using compression and for optimal delivery of your web app that uses _PhotoPay_ SDK, you should also check the [official Emscripten documentation](https://emscripten.org/docs/compiling/Deploying-Pages.html#optimizing-download-sizes). +For more information about configuring your web server to compress and optimally deliver PhotoPay SDK in your web app, see the [official Emscripten documentation](https://emscripten.org/docs/compiling/Deploying-Pages.html#optimizing-download-sizes). -#### Location of WASM and related support files +##### Location of WASM and related support files -It's possible to host WASM and related support files on a location different than the one where web app is located. +You can host WASM and related support files in a location different from the one where your web app is located. -For example, it's possible to host WASM and related support files on `https://cdn.example.com`, while the web app is hosted on `https://example.com`. +For example, your WASM and related support files can be located in `https://cdn.example.com`, while the web app is hosted on `https://example.com`. -In that case it's important to set [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) in response from `https://cdn.example.com`. i.e. set header `Access-Control-Allow-Origin` with proper value. +In that case it's important to set [CORS headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) in response from `https://cdn.example.com`. i.e. set header `Access-Control-Allow-Origin` with proper value so that the web page knows it’s okay to take on the request. -If WASM engine is not placed in the same folder as web app, don't forget to configure instance of `WasmSDKLoadSettings` with proper location: +If WASM engine folders are not placed in the same folder as web app, don't forget to configure instance of `WasmSDKLoadSettings` with proper location: ```typescript ... @@ -342,18 +392,45 @@ loadSettings.engineLocation = "https://cdn.example.com/wasm"; ... ``` -### Setting up multiple licenses +The location should point to folder containing folders `basic`, `advanced` and `advanced-threads` that contain the WebAssembly and its support files. -Since license key of _PhotoPay_ SDK is tied to the domain name, it's required to initialize the SDK with different license keys based on the location of the web app. +The difference between `basic`, `advanced` and `advanced-threads` folders are in the way the WebAssembly file was built: -A common scenario is to have different license keys for development on the local machine, staging environment and production environment. +* WebAssembly files in `basic` folder were built to be most compatible, but less performant. +* WebAssembly files in `advanced` folder can yield better scanning performance, but requires more modern browser +* WebAssembly files in the `advanced-threads` folder uses advanced WASM features as the WASM in the `advanced` folder but will additionally use WebWorkers for multi-threaded processing which will yield best performance. -There are two most common approaches regarding setup of license key: +Depending on what features the browser actually supports, the correct WASM file will be loaded automatically. -1. Multiple apps: build different versions of web app for different environments -2. Single app: build single version of web app which has logic to determine which license key to use +Note that in order to be able to use WASM from the `advanced-threads` folder, you need to configure website to be "cross-origin isolated" using COOP and COEP headers, as described [in this article](https://web.dev/coop-coep/). This is required for browser to allow using the `SharedArrayBuffer` feature which is required for multi-threaded processing to work. Without doing so, the browser will load only the single-threaded WASM binary from the `advanced` folder. -#### Multiple apps +``` +# NGINX web server COEP and COOP header example + +... + +server { + location / { + add_header Cross-Origin-Embedder-Policy: require-corp; + add_header Cross-Origin-Opener-Policy: same-origin; + } +} + +... +``` + +#### Setting up multiple licenses + +As mentioned, the license key of PhotoPay SDK is tied to your domain name, so it's required to initialize the SDK with different license keys based on the location of your web app. + +A common scenario is to have different license keys for development on the local machine, staging environment and production environment. Our team will be happy to issue multiple trial licenses if needs be. See [Obtaining a license key](#obtainingalicensekey). + +There are two most common approaches regarding setup of your license key(s): + +1. Multiple apps: build different versions of your web app for different environments +2. Single app: build a single version of your web app which has logic to determine which license key to use + +##### Multiple apps Common approach when working with modern frameworks/libraries. @@ -361,7 +438,7 @@ Common approach when working with modern frameworks/libraries. * [Building and serving Angular apps](https://angular.io/guide/build) * [Vue.js: Modes and Environment Variables](https://cli.vuejs.org/guide/mode-and-env.html#environment-variables) -#### Single app +##### Single app Simple approach, where handling of license key is done inside the web app. @@ -382,23 +459,31 @@ if ( window.location.hostname === "example.com" ) // Place your production domai ... ``` -# The `Recognizer` concept, `RecognizerRunner` and `VideoRecognizer` +## The `Recognizer` concept, `RecognizerRunner` and `VideoRecognizer` -This section will first describe [what is a `Recognizer`](#recognizerConcept) and how it should be used to perform recognition of the images, videos and camera stream. Next, we will describe what is a [`RecognizerRunner`](#recognizerRunner) and how it can be used to tweak the recognition procedure. Finally, a [`VideoRecognizer`](#videoRecognizer) will be described and how it builds on top of `RecognizerRunner` in order to provide support for recognizing a video or a camera stream. +This section will first describe [what a `Recognizer`](#recognizerConcept) is and how it should be used to perform recognition of images, videos and camera stream. We'll also describe what [`RecognizerRunner`](#recognizerRunner) is and how it can be used to tweak the recognition procedure. Finally, we'll describe what [`VideoRecognizer`](#videoRecognizer) is and explain how it builds on top of `RecognizerRunner` in order to provide support for recognizing a video or a camera stream. -## The `Recognizer` concept +### The `Recognizer` concept -The `Recognizer` is the basic unit of processing within the _PhotoPay_ SDK. Its main purpose is to process the image and extract meaningful information from it. As you will see [later](#recognizerList), the _PhotoPay_ SDK has lots of different `Recognizer` objects that have various purposes. +The `Recognizer` is the basic unit tasked with reading documents within the domain of PhotoPay SDK. Its main purpose is to process the image and extract meaningful information from it. As you will see later, PhotoPay SDK has lots of different `Recognizer` objects you can set up to recognize various documents. -The `Recognizer` is the object on the WebAssembly heap, which means that it will not be automatically cleaned up by the garbage collector once it's not required anymore. Once you are done with using it, you **must** call the `delete` method on it to release the memory on the WebAssembly heap. Failing to do so will result in memory leak on the WebAssembly heap which may result with crash of the browser tab running your web app. +The `Recognizer` is the object on the WebAssembly heap, which means that it will not be automatically cleaned up by the garbage collector once it's not required anymore. Once you are done using it, you must call the `delete` method on it to release the memory on the WebAssembly heap. Failing to do so will result in memory leak on the WebAssembly heap which may result in a crash of the browser tab running your web app. Each `Recognizer` has a `Result` object, which contains the data that was extracted from the image. The `Result` for each specific `Recognizer` can be obtained by calling its `getResult` method, which will return a `Result` object placed on the JS heap, i.e. managed by the garbage collector. Therefore, you don't need to call any delete-like methods on the `Result` object. Every `Recognizer` is a stateful object that can be in two possible states: _idle state_ and _working state_. -While in _idle state_, you are allowed to call method `updateSettings` which will update its properties according to given settings object. At any time, you can call its `currentSettings` method to obtain its currently applied settings object. +While in _idle state_, you are allowed to call method `updateSettings` which will update its properties according to the given settings object. At any time, you can call its `currentSettings` method to obtain its currently applied settings object. -After you create a `RecognizerRunner` with array containing your recognizer, the state of the `Recognizer` will change to _working state_, in which `Recognizer` object will be used for processing. While being in _working state_, it is not possible to call method `updateSettings` (calling it will crash your web app). If you need to change configuration of your recognizer while its being used, you need to call its `currentSettings` method to obtain its current configuration, update it as you need it, create a new `Recognizer` of the same type, call `updateSettings` on it with your modified configuration and finally replace the original `Recognizer` within the `RecognizerRunner` by calling its `reconfigureRecognizers` method. +After you create a `RecognizerRunner` with an array containing your recognizer, the state of the `Recognizer` will change to _working state_, in which `Recognizer` object will be used for processing. While being in _working state_, it is not possible to call method `updateSettings` (calling it will crash your web app). + +If you need to change configuration of your recognizer while it's being used, you need to: + +1. Call its `currentSettings` method to obtain its current configuration +2. Update it as you need it +3. Create a new `Recogizer` of the same type +4. Call `updateSettings` on it with your modified configuration +5. Replace the original `Recognizer` within the `RecognizerRunner` by calling its `reconfigureRecognizers` method When written as a pseudocode, this would look like: @@ -420,27 +505,29 @@ await recognizerRunner.reconfigureRecognizers( [ newRecognizer ], true ); // use await myRecognizerInUse.delete(); ``` -While `Recognizer` object works, it changes its internal state and its result. The `Recognizer` object's `Result` always starts in `Empty` state. When corresponding `Recognizer` object performs the recognition of given image, its `Result` can either stay in `Empty` state (in case `Recognizer` failed to perform recognition), move to `Uncertain` state (in case `Recognizer` performed the recognition, but not all mandatory information was extracted) or move to `Valid` state (in case `Recognizer` performed recognition and all mandatory information was successfully extracted from the image). +While `Recognizer` object works, it changes its internal state and its result. The `Recognizer` object's `Result` always starts in `Empty` state. When corresponding `Recognizer` object performs the recognition of a given image, its `Result` can either stay in `Empty` state (in case `Recognizer` failed to perform recognition), move to `Uncertain` state (in case `Recognizer` performed the recognition, but not all mandatory information was extracted) or move to `Valid` state (in case `Recognizer` performed recognition and all mandatory information was successfully extracted from the image). -## `RecognizerRunner` +### `RecognizerRunner` The `RecognizerRunner` is the object that manages the chain of individual `Recognizer` objects within the recognition process. -It must be created by `createRecognizerRunner` method of the `WasmModuleProxy` interface, which is a member of `WasmSDK` interface which is resolved in a promise returned by the `loadWasmModule` function you've seen [above](#firstScan). The function requires a two parameters: an array of `Recognizer` objects that will be used for processing and a `boolean` indicating whether multiple `Recognizer` objects are allowed to have their `Results` enter the `Valid` state. +It must be created by `createRecognizerRunner` method of the `WasmModuleProxy` interface, which is a member of `WasmSDK` interface which is resolved in a promise returned by the `loadWasmModule` function you've seen [above](#firstScan). The function requires two parameters: an array of `Recognizer` objects that will be used for processing and a `boolean` indicating whether multiple `Recognizer` objects are allowed to have their `Results` enter the `Valid` state. -To explain further the `boolean` parameter, we first need to understand how `RecognizerRunner` performs image processing. +To explain the `boolean` parameter further, we first need to understand how `RecognizerRunner` performs image processing. -When the `processImage` method is called, it processes the image with the first `Recognizer` in chain. If the `Recognizer's` `Result` object changes its state to `Valid`, then if the above `boolean` parameter is `false`, the recognition chain will be broken and promise returned by the method will be immediately resolved. If the above parameter is `true`, then the image will also be processed with other `Recognizer` objects in chain, regardless of the state of their `Result` objects. If, after processing the image with the first `Recognizer` in chain, its `Result` object's state is not changed to `Valid`, the `RecognizerRunner` will use the next `Recognizer` object in chain for processing the image and so on - until the end of the chain (if no results become valid or always if above parameter is `true`) or until it finds the recognizer that has successfully processed the image and changed its `Result's` state to `Valid` (if above parameter is `false`). +When the `processImage` method is called, it processes the image with the first `Recognizer` in the chain. If `Recognizer's` `Result` object changes its state to `Valid`, and if the above `boolean` parameter is `false`, the recognition chain will be stopped and `Promise` returned by the method will be immediately resolved. If the above parameter is `true`, then the image will also be processed with other `Recognizer` objects in chain, regardless of the state of their `Result` objects. -You cannot change the order of the `Recognizer` objects within the chain - no matter the order in which you give `Recognizer` objects to `RecognizerRunner` (either to its creation function `createRecognizerRunner` or to its `reconfigureRecognizers` method), they are internally ordered in a way that provides best possible performance and accuracy. +That means if after processing the image with the first `Recognizer` in the chain, its `Result` object's state is not changed to `Valid`, the `RecognizerRunner` will use the next `Recognizer` object in chain for processing the image and so on - until the end of the chain (if no results become valid or always if above parameter is `true`) or until it finds the recognizer that has successfully processed the image and changed its `Result's` state to `Valid` (if above parameter is `false`). -Also, in order for _PhotoPay_ SDK to be able to order `Recognizer` objects in recognition chain in the best way possible, it is not allowed to have multiple instances of `Recognizer` objects of the same type within the chain. Attempting to do so will crash your application. +You cannot change the order of the `Recognizer` objects within the chain - regardless of the order in which you give `Recognizer` objects to `RecognizerRunner` (either to its creation function `createRecognizerRunner` or to its `reconfigureRecognizers` method), they are internally ordered in a way that ensures the best performance and accuracy possible. -## Performing recognition of video streams using `VideoRecognizer` +Also, in order for PhotoPay SDK to be able to sort `Recognizer` objects in the recognition chain the best way, it is not allowed to have multiple instances of `Recognizer` objects of the same type within the chain. Attempting to do so will crash your application. + +### Performing recognition of video streams using `VideoRecognizer` Using `RecognizerRunner` directly could be difficult in cases when you want to perform recognition of the video or the live camera stream. Additionally, handling camera management from the web browser can be [sometimes challenging](https://stackoverflow.com/questions/59636464/how-to-select-proper-backfacing-camera-in-javascript). In order to make this much easier, we provided a `VideoRecognizer` class. -To perform live camera recognition using the `VideoRecognizer`, you will need an already set up `RecognizerRunner` object and a reference to `HTMLVideoElement` to which camera stream will be attached. +To perform live camera recognition using the `VideoRecognizer`, you will need an already configured `RecognizerRunner` object and a reference to `HTMLVideoElement` to which camera stream will be attached. To perform the recognition, you should simply write: @@ -448,7 +535,7 @@ To perform the recognition, you should simply write: const cameraFeed = document.getElementById( "cameraFeed" ); try { - const videoRecognizer = await MicroblinkSDK.VideoRecognizer.createVideoRecognizerFromCameraStream( + const videoRecognizer = await PhotoPaySDK.VideoRecognizer.createVideoRecognizerFromCameraStream( cameraFeed, recognizerRunner ); @@ -462,12 +549,12 @@ catch ( error ) The `recognize` method of the `VideoRecognizer` will start the video capture and recognition loop from the camera and will return a `Promise` that will be resolved when either `processImage` of the given `RecognizerRunner` returns `Valid` for some frame or the timeout given to `recognize` method is reached (if no timeout is given, a default one is used). -### Recognizing a video file +#### Recognizing a video file -If, instead of performing recognition of live video stream, you want to perform recognition of pre-recorded video file, you should simply construct `VideoRecognizer` using a different function, as shown below: +If, instead of performing recognition of live video stream, you want to perform recognition of a pre-recorded video, you should simply construct `VideoRecognizer` using a different function, as shown below: ```typescript -const videoRecognizer = await MicroblinkSDK.createVideoRecognizerFromVideoPath( +const videoRecognizer = await PhotoPaySDK.createVideoRecognizerFromVideoPath( videoPath, htmlVideoElement, recognizerRunner @@ -475,14 +562,16 @@ const videoRecognizer = await MicroblinkSDK.createVideoRecognizerFromVideoPath( const processResult = await videoRecognizer.recognize(); ``` -## Custom UX with `VideoRecognizer` +### Custom UX with `VideoRecognizer` + +The procedure for using `VideoRecognizer` described [above](#videoRecognizer) is quite simple, but has some limits. For example, you can only perform one shot scan with it. As soon as the promise returned by `recognize` method resolves, the camera feed is paused and you need to start new recognition. -The procedure for using `VideoRecognizer` described [above](#videoRecognizer) is quite simple, but has some limits. For example, you can only perform one shot scan with it. As soon as the promise returned by `recognize` method resolves, the camera feed is paused and you need to start new recognition. However, if you need to perform multiple recognitions in single camera session, without pausing the camera preview, you can use the `startRecognition` method, as described in the example below; +However, if you need to perform multiple recognitions in single camera session, without pausing the camera preview, you can use the `startRecognition` method, as described in the example below: ```typescript videoRecognizer.startRecognition ( - ( recognitionState: MicroblinkSDK.RecognizerResultState ) => + ( recognitionState: PhotoPaySDK.RecognizerResultState ) => { // Pause recognition before performing any async operation - this will make sure that // recognition will not continue while returning the control flow back from this function. @@ -509,327 +598,282 @@ videoRecognizer.startRecognition ); ``` -# Handling processing events with `MetadataCallbacks` +## Handling processing events with `MetadataCallbacks` -Processing events, also known as _Metadata callbacks_ are purely intended for giving processing feedback on UI or to capture some debug information during development of your web app using _PhotoPay_ SDK. +Processing events, also known as _Metadata callbacks_ are purely intended to provide users with on-screen scanning guidance or to capture some debug information during development of your web app using PhotoPay SDK. -Callbacks for all events are bundled into the [MetadataCallbacks](src/MicroblinkSDK/MetadataCallbacks.ts) object. We suggest that you check for more information about available callbacks and events to which you can handle in the [source code of the `MetadataCallbacks` interface](src/MicroblinkSDK/MetadataCallbacks.ts). +Callbacks for all events are bundled into the [MetadataCallbacks](src/MicroblinkSDK/MetadataCallbacks.ts) object. We suggest that you have a look at the available callbacks and events which you can handle in the [source code of the `MetadataCallbacks` interface](src/MicroblinkSDK/MetadataCallbacks.ts). -You can associate your implementation of `MetadataCallbacks` interface with `RecognizerRunner` either during creation or by invoking its method `setMetadataCallbacks`. Please note that both those methods need to pass information about available callbacks to the native code and for efficiency reasons this is done at the time `setMetadataCallbacks` method is called and **not every time** when change occurs within the `MetadataCallbacks` object. This means that if you, for example, set `onQuadDetection` to `MetadataCallbacks` after you already called `setMetadataCallbacks` method, the `onQuadDetection` will not be registered with the native code and therefore it will not be called. +You can link the `MetadataCallbacks` interface with `RecognizerRunner` either during creation or by invoking its method `setMetadataCallbacks`. Please note that both those methods need to pass information about available callbacks to the native code. For efficiency reasons this happens at the time `setMetadataCallbacks` is called, **not every time** a change occurs within the `MetadataCallbacks` object. -Similarly, if you, for example, remove the `onQuadDetection` from `MetadataCallbacks` object after you already called `setMetadataCallbacks` method, your app will crash in attempt to invoke non-existing function when our processing code attempts to invoke it. We **deliberately** do not perform null check here because of two reasons: +This means that if you, for example, set `onQuadDetection` to `MetadataCallbacks` after you already called `setMetadataCallbacks` method, the `onQuadDetection` will not be registered with the native code and therefore it will not be called. + +Similarly, if you remove the `onQuadDetection` from `MetadataCallbacks` object after you already called `setMetadataCallbacks` method, your app will crash in attempt to invoke a non-existing function when our processing code attempts to invoke it. We **deliberately** do not perform null check here because of two reasons: - It is inefficient - Having no callback, while still being registered to native code is illegal state of your program and it should therefore crash -**Remember**, each time you make some changes to `MetadataCallbacks` object, you need to apply those changes to to your `RecognizerRunner` by calling its `setMetadataCallbacks` method. +**Remember** that whenever you make some changes to the `MetadataCallbacks` object, you need to apply those changes to your `RecognizerRunner` by calling its `setMetadataCallbacks` method. + +## List of available recognizers -# List of available recognizers +This section will give a list of all `Recognizer` objects that are available within PhotoPay SDK, their purpose and recommendations on how they should be used to achieve best performance and user experience. -This section will give a list of all `Recognizer` objects that are available within _PhotoPay_ SDK, their purpose and recommendations how they should be used to get best performance and user experience. -## Success Frame Grabber Recognizer +### Success Frame Grabber Recognizer The [`SuccessFrameGrabberRecognizer`](src/Recognizers/SuccessFrameGrabberRecognizer.ts) is a special `Recognizer` that wraps some other `Recognizer` and impersonates it while processing the image. However, when the `Recognizer` being impersonated changes its `Result` into `Valid` state, the `SuccessFrameGrabberRecognizer` captures the image and saves it into its own `Result` object. Since `SuccessFrameGrabberRecognizer` impersonates its slave `Recognizer` object, it is not possible to have both concrete `Recognizer` object and `SuccessFrameGrabberRecognizer` that wraps it in the same `RecognizerRunner` at the same time. Doing so will have the same effect as having multiple instances of the same `Recognizer` in the same `RecognizerRunner` - it will crash your application. For more information, see [paragraph about `RecognizerRunner`](#recognizerRunner). This recognizer is best for use cases when you need to capture the exact image that was being processed by some other `Recognizer` object at the time its `Result` became `Valid`. When that happens, `SuccessFrameGrabber's` `Result` will also become `Valid` and will contain described image. That image will be available in its `successFrame` property. -## Barcode recognizer + +### Barcode recognizer The `BarcodeRecognizer` is recognizer specialized for scanning various types of barcodes. As you can see from [its source code](src/Recognizers/BlinkBarcode/BarcodeRecognizer.ts), you can enable multiple barcode symbologies within this recognizer, however keep in mind that enabling more barcode symbologies affects scanning performance - the more barcode symbologies are enabled, the slower the overall recognition performance. Also, keep in mind that some simple barcode symbologies that lack proper redundancy, such as [Code 39](https://en.wikipedia.org/wiki/Code_39), can be recognized within more complex barcodes, especially 2D barcodes, like [PDF417](https://en.wikipedia.org/wiki/PDF417). -## PhotoPay recognizers -### SEPA Payment QR code recognizer +### PhotoPay recognizers + +#### SEPA Payment QR code recognizer The [`SepaQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/SEPA/SepaQrCodePaymentRecognizer.ts) is used for scanning payment information from SEPA (Single Euro Payments Area) payment QR codes. The recognizer support scanning payment QR codes that are encoded by [standard defined by European Payments Council](https://www.europeanpaymentscouncil.eu/document-library/guidance-documents/quick-response-code-guidelines-enable-data-capture-initiation). -## Country-specific PhotoPay recognizers +### Country-specific PhotoPay recognizers -### Austria +#### Austria -#### Austrian payment QR code recognizer +##### Austrian payment QR code recognizer The [`AustriaQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Austria/AustriaQrCodePaymentRecognizer.ts) is used for scanning payment information from QR code usually found on SEPA payment slips in Austria. -### Croatia +#### Croatia -#### Croatian payment PDF417 2D barcode recognizer +##### Croatian payment PDF417 2D barcode recognizer The [`CroatiaPdf417PaymentRecognizer`](src/Recognizers/PhotoPay/Croatia/CroatiaPdf417PaymentRecognizer.ts) is used for scanning payment information from PDF417 2D barcode usually found on payment slips. It supports both HUB3 and HUB1 2D barcode standards. -#### Croatian payment QR code recognizer +##### Croatian payment QR code recognizer The [`CroatiaQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Croatia/CroatiaQrCodePaymentRecognizer.html) is used for scanning payment information from QR codes that have content encoded in same format as specified by HUB3 PDF417 2D barcode standard. -### Czechia +#### Czechia -#### Czech payment QR code recognizer +##### Czech payment QR code recognizer The [`CzechiaQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Czechia/CzechiaQrCodePaymentRecognizer.ts) is used for scanning payment information from payment QR codes that are usually found on czech payment slips. -### Germany +#### Germany -#### German payment QR code recognizer +##### German payment QR code recognizer The [`GermanyQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Germany/GermanyQrCodePaymentRecognizer.ts) is used for scanning payment information from QR code usually found on SEPA payment slips in Germany. -### Kosovo +#### Kosovo -#### Kosovo Code 128 recognizer +##### Kosovo Code 128 recognizer The [`KosovoCode128PaymentRecognizer`](src/Recognizers/PhotoPay/Kosovo/KosovoCode128PaymentRecognizer.ts) is used for scanning payment information from Code128 1D barcodes usually found on payment slips in Kosovo. -### Serbia +#### Serbia -#### Serbian payment PDF417 2D barcode recognizer +##### Serbian payment PDF417 2D barcode recognizer The [`SerbiaPdf417PaymentRecognizer`](src/Recognizers/PhotoPay/Serbia/SerbiaPdf417PaymentRecognizer.ts) is used for scanning payment information from PDF417 2D barcode found on some serbian invoices. The Republic of Serbia does not have a national standard for payment slips nor payment barcodes. This recognizer supports scanning PDF417 2D barcodes that are modelled after Croatian HUB3 standard. -#### Serbian payment QR code recognizer +##### Serbian payment QR code recognizer The [`SerbiaQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Serbia/SerbiaQrCodePaymentRecognizer.ts) is used for scanning payment information from QR code found on some serbian invoices. The Republic of Serbia does not have a national standard for payment slips nor payment barcodes. This recognizer supports scanning QR codes that are modelled after Croatian HUB3 standard. -### Slovakia +#### Slovakia -#### Slovak payment Code 128 recognizer +##### Slovak payment Code 128 recognizer The [`SlovakiaCode128PaymentRecognizer`](src/Recognizers/PhotoPay/Slovakia/SlovakiaCode128PaymentRecognizer.ts) is used for scanning payment information from Code128 1D barcode usually found on both white and green payment slips in Slovakia. -#### Slovak payment Data Matrix Code recognizer +##### Slovak payment Data Matrix Code recognizer The [`SlovakiaDataMatrixPaymentRecognizer`](src/Recognizers/PhotoPay/Slovakia/SlovakiaDataMatrixPaymentRecognizer.ts) is used for scanning payment information from Data Matrix 2D barcode usually found on some white payment slips in Slovakia. -#### Slovak payBySquare QR code recognizer +##### Slovak payBySquare QR code recognizer The [`SlovakiaQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Slovakia/SlovakiaQrCodePaymentRecognizer.ts) is used for scanning payment information from [Slovak pyBySquare](https://bysquare.com/) payment QR code. This recognizer support only scanning the blue (PAY bySquare) QR codes. The orange (INVOICE bySquare) QR codes are not supported by this recognizer. -### Slovenia +#### Slovenia -#### Slovenian payment QR code recognizer +##### Slovenian payment QR code recognizer The [`SloveniaQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Slovenia/SloveniaQrCodePaymentRecognizer.ts) is used for scanning payment information from payment QR codes usually found on [UPN payment slips](https://www.nlb.si/univerzalni-placilni-nalog) in Slovenia. -### Switzerland +#### Switzerland -#### Swiss payment QR code recognizer +##### Swiss payment QR code recognizer The [`SwitzerlandQrCodePaymentRecognizer`](src/Recognizers/PhotoPay/Switzerland/SwitzerlandQrCodePaymentRecognizer.ts) is used for scanning payment information from payment QR codes used in Switzerland. -# UI component -_PhotoPay_ In-browser UI component acts as an UI layer built on top of core SDK. UI component is customizable HTML element which provides UI for scanning of various identity documents from images and from camera feed. +## Recognizer settings + +It's possible to enable various recognizer settings before recognition process to modify default behaviour of the recognizer. -One of the main goals of UI component is to simplify integration of _PhotoPay_ in web apps for various use cases. +List of all recognizer options is available in the source code of each recognizer, while list of all recognizers is available in the [List of available recognizers](#recognizerList) section. -## Installation +Recognizer settings should be enabled right after the recognizer has been created in the following manner: -To use the UI component, JS file with custom element must be loaded and WASM engine must be available. +```typescript +// Create instance of recognizer +const CroatiaPdf417PaymentRecognizer = await PhotoPaySDK.createCroatiaPdf417PaymentRecognizer( sdk ); -### Installation via CDN +// Retrieve current settings +const settings = await CroatiaPdf417PaymentRecognizer.currentSettings(); -```html - - - +// Update desired settings +settings[ " " ] = true; + +// Apply settings +await CroatiaPdf417PaymentRecognizer.updateSettings( settings ); - - - +... ``` -### Installation via NPM + +## Technical requirements -```sh -# Install latest version of UI component via NPM or Yarn -npm install @microblink/photopay-in-browser-sdk # OR yarn add @microblink/photopay-in-browser-sdk +This document provides information about technical requirements of end-user devices to run PhotoPay. -# Copy JS file to folder where other JS assets are located -cp -r node_modules/@microblink/photopay-in-browser-sdk/ui/dist/* src/public/js/ +Requirements: -# Copy WASM resources from SDK to folder where other static assets are located -cp -r node_modules/@microblink/photopay-in-browser-sdk/resources/* src/public/assets/ -``` +1. The browser is [supported](#supported-browsers). +2. The browser [has access to camera device](#camera-devices). +3. The device has [enough computing power](#device-support) to extract data from an image. -```html - - - +**Important**: PhotoPay may not work correctly in *WebView*/*WKWebView*/*SFSafariViewController*. See [this section](#embedded). - - - - - - - - -``` +## Device support + +It's hard to pinpoint exact hardware specifications for successful data extraction, but based on our testing mid-end and high-end smartphone devices released in 2018 and later should be able to extract data from an image in a relatively short time frame. -### Examples and API documentation +**Notes & Guidelines** -Demo app with multiple UI components alongside with source code can be found in the [ui/demo.html](ui/demo.html) file. +* Browsers supported by PhotoPay can run on older devices, where extraction can take much longer to execute, e.g. around 30 or even 40 seconds. -Example apps are located in the [examples](examples) directory, where minimal JavaScript example is located in the [examples/ui](examples/ui) directory, while minimal TypeScript example is located in the [examples/ui-ts](examples/ui-ts) directory. +## SDK and *WebView*/*WKWebView*/*SFSafariViewController* -Complete API documentation of UI components is located in the [docs directory](ui/docs). -# Troubleshooting +### Android and *WebView* -## Integration problems +*WebView* is not supported for a couple of reasons: -In case of problems with the integration of the SDK, first make sure that you have tried integrating the SDK exactly as described [in integration instructions](#firstScan). +* There is no guarantee that developers of mobile apps are using *WebView* with all necessary features enabled. +* It's up to developers of mobile apps to provide support for camera access from *WebView* (which is integral part of our experience), which requires additional work compared to classic camera permission in mobile apps. -If you have followed the instructions to the letter and you still have the problems, please contact us at [help.microblink.com](https://help.microblink.com) +Also, it's possible for mobile app developers to use *WebView* alternatives like *GeckoView* and similar, which have their own constraints. -## SDK problems +### iOS, *WKWebView* and *SFSafariViewController* + +As for now, it's not possible to access the camera from *WKWebView* and *SFSafariViewController*. + +Camera access on iOS, i.e. WebRTC, is only supported in Safari browser. Other browsers like Chrome and Firefox won't work as expected. + +### Conclusion + +There is a general technical constraint when using PhotoPay from in-app browser - it's not possible to know for sure if the SDK has or hasn't got camera access. That is, it's not possible to notify the user if the camera is not available during the initialization. + +However, majority of widely used apps with in-app browsers, e.g. Facebook and Snapchat, are using standard *WebView* or embedded Safari with all the features. For example, WASM and modern JS are supported. + +But the major problem still remains, how to get an image from the camera? Currently, we can advise two approaches: + +1. Detect via UA string if in-app browser is used and prompt the user to use the native browser. +2. Detect via UA string if in-app browser is used and enable classic image upload via `` element. + * Based on the operating system and software version, users will be able to select an image from the gallery, or to capture an image from the camera. + +## Troubleshooting + +### Integration problems + +In case you're having issues integrating our SDK, the first thing you should do is revisit our [integration instructions](#firstScan) and make sure to closely follow each step. + +If you have followed the instructions to the letter and you still have problems, please contact us at [help.microblink.com](https://help.microblink.com). + +When contacting us, please make sure you include the following information: + +* Log from the web console. +* High resolution scan/photo of the document that you are trying to scan. +* Information about the device and browser that you are using — we need the exact version of the browser and operating system it runs on. Also, if it runs on a mobile device, we also need the model of the device in question (camera management is specific to browser, OS and device). +* Please stress out that you are reporting a problem related to the WebAssembly version of the PhotoPay SDK. + +### SDK problems In case of problems with using the SDK, you should do as follows: -### Licensing problems +#### Licensing problems -If you are getting "invalid license key" error or having other license-related problems (e.g. some feature is not enabled that should be), first check the browser console. All license-related problems are logged to web console so it is easy to determine what went wrong. +If you are getting an "invalid license key" error or having other license-related problems (e.g. some feature is not enabled that should be), first check the browser console. All license-related problems are logged to the web console so that it's easier to determine what went wrong. -When you have to determine what is the license-related problem or you simply do not understand the log, you should contact us [help.microblink.com](http://help.microblink.com). When contacting us, please make sure you provide following information: +When you can't determine the license-related problem or you simply do not understand the log information, you should contact us at [help.microblink.com](http://help.microblink.com). When contacting us, please make sure you provide following information: * Exact fully qualified domain name of your app, i.e. where the app is hosted. * License that is causing problems. -* Please stress out that you are reporting problem related to WebAssembly version of the _PhotoPay_ SDK. -* If unsure about the problem, you should also provide excerpt from web console containing the license error. +* Please stress out that you are reporting a problem related to the WebAssembly version of the PhotoPay SDK. +* If unsure about the problem, you should also provide an excerpt from the web console containing the license error. + +#### Other problems + +If you are having problems with scanning certain items, undesired behaviour on specific device(s), crashes inside PhotoPay SDK or anything unmentioned, please contact our support with the same information as listed at the start of this section. + +## FAQ and known issues + +* **After switching from trial to production license I get error `This entity is not allowed by currently active license!` when I create a specific `Recognizer` object.** + +Each license key contains information about which features are allowed to use and which are not. This error indicates that your production license does not allow the use of a specific `Recognizer` object. You should contact [support](http://help.microblink.com) to check if the provided license is OK and that it really contains the features you've requested. -### Other problems +* **Why am I getting No internet connection error if I'm on a private network?** -If you are having problems with scanning certain items, undesired behaviour on specific device(s), crashes inside _PhotoPay_ or anything unmentioned, please do as follows: +Versions PhotoPay 7.8.0 and above require an internet connection to work under our new License Management Program. -* Contact us at [help.microblink.com](http://help.microblink.com) describing your problem and provide following information: - * Log from the web console. - * High resolution scan/photo of the item that you are trying to scan. - * Information about device and browser that you are using - we need exact version of the browser and operating system it runs on. Also, if it runs on mobile device, we also need the model of the device in question (camera management is specific to both browser, OS and device). - * Please stress out that you are reporting problem related to WebAssembly version of the _PhotoPay_ SDK. -# FAQ and known issues +This means your web app has to be connected to the Internet in order for us to validate your trial license key. Scanning or data extraction of documents still happens offline, in the browser itself. -#### After switching from trial to production license I get error `This entity is not allowed by currently active license!` when I create a specific `Recognizer` object. +Once the validation is complete, you can continue using the SDK in an offline mode (or over a private network) until the next check. -Each license key contains information about which features are allowed to use and which are not. This error indicates that your production license does not allow using of specific `Recognizer` object. You should contact [support](http://help.microblink.com) to check if provided license is OK and that it really contains all features that you have purchased. +We've added error callback to Microblink SDK to inform you about the status of your license key. -# Additional info +## Additional info -Complete source code of the TypeScript wrapper can be found in [here](src). +Complete source code of the TypeScript wrapper can be found [here](src). For any other questions, feel free to contact us at [help.microblink.com](http://help.microblink.com). diff --git a/examples/es-module/main.js b/examples/es-module/main.js index 7f913e6..d5cb890 100644 --- a/examples/es-module/main.js +++ b/examples/es-module/main.js @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + /** * PhotoPay In-browser SDK demo app which demonstrates how to: * @@ -30,7 +34,7 @@ function main() } // 1. It's possible to obtain a free trial license key on microblink.com - const licenseKey = "sRwAAAYJbG9jYWxob3N0r/lOPk4/w35CpHlVLi84YJhfzkKNDWt5k6TOIq/BqQY4bts33tGdQc7RawrRGyvbnbj5DEV92rrMkoGMUk3QCySYI9IPMLsIG1aPiOLf1Dhq9FZbGgJvTq6f1O/4pQzxtWn5rN+fs9TqjLz+ei2k0Bv12JREFNsBroMSZUuIDW7uU2bAnW4qW2cedBUaDI9KuhBAtS1B/78M3zb7Fm3dhvMvXj2Mlhl+iwFDwqAhHb5f8vxRICbnqjrb9GO34z4jgJFVQ/mDFZzgLASEJlUO01vfBs18GWt78ups4pIgiIpJph2DhMi76GMxoqQKJfoEs6Wl+VwmIftwWJQbMg=="; + const licenseKey = "sRwAAAYJbG9jYWxob3N0r/lOPk4/w35CpHlVKjc9YGS1TbhKMOp/628Nz+3wucEKOKiY/6REBB0awpfPXXng8x6oFT8mEe+eFZwM6UTZKMO58PYWB2BUoq3KuLZWA0iIrN5l0EOTf4y0aTFs1KXROvrx2TbPyeNjYtPqtuMZq7Mo6L0GGWp5zehmxpUnuWBsW8/tR/8NLpfFQHucZnA+nnsS3Oj/qzbaf96oTjl1Ov4T4WVRbNK4yjzUre+L+NleOrZygXTQnqPLtPnhKmoHjJ9dtyTRp1C89NxNHUqVeacwp0Q8v+plPxr+fS8zSCMVeEWgumsmmLhFiaFLxHQ14VPYB+ycRpMi6FAZVPNXPbXtfjWi0g=="; // 2. Create instance of SDK load settings with your license key const loadSettings = new PhotoPaySDK.WasmSDKLoadSettings( licenseKey ); diff --git a/examples/es-module/style.css b/examples/es-module/style.css index 1590300..602b5fb 100644 --- a/examples/es-module/style.css +++ b/examples/es-module/style.css @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + * { box-sizing: border-box; diff --git a/examples/typescript/package.json b/examples/typescript/package.json index 1d1385b..6650250 100644 --- a/examples/typescript/package.json +++ b/examples/typescript/package.json @@ -19,6 +19,6 @@ "typescript": "^3.9.5" }, "dependencies": { - "@microblink/photopay-in-browser-sdk": "~7.7.3" + "@microblink/photopay-in-browser-sdk": "~7.7.4" } } \ No newline at end of file diff --git a/examples/typescript/public/style.css b/examples/typescript/public/style.css index 1590300..602b5fb 100644 --- a/examples/typescript/public/style.css +++ b/examples/typescript/public/style.css @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + * { box-sizing: border-box; diff --git a/examples/typescript/src/app.ts b/examples/typescript/src/app.ts index df281f3..8c97fe1 100644 --- a/examples/typescript/src/app.ts +++ b/examples/typescript/src/app.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + /** * PhotoPay In-browser SDK demo app which demonstrates how to: * @@ -29,7 +33,7 @@ function main() } // 1. It's possible to obtain a free trial license key on microblink.com - const licenseKey = "sRwAAAYJbG9jYWxob3N0r/lOPk4/w35CpHlVLi84YJhfzkKNDWt5k6TOIq/BqQY4bts33tGdQc7RawrRGyvbnbj5DEV92rrMkoGMUk3QCySYI9IPMLsIG1aPiOLf1Dhq9FZbGgJvTq6f1O/4pQzxtWn5rN+fs9TqjLz+ei2k0Bv12JREFNsBroMSZUuIDW7uU2bAnW4qW2cedBUaDI9KuhBAtS1B/78M3zb7Fm3dhvMvXj2Mlhl+iwFDwqAhHb5f8vxRICbnqjrb9GO34z4jgJFVQ/mDFZzgLASEJlUO01vfBs18GWt78ups4pIgiIpJph2DhMi76GMxoqQKJfoEs6Wl+VwmIftwWJQbMg=="; + const licenseKey = "sRwAAAYJbG9jYWxob3N0r/lOPk4/w35CpHlVKjc9YGS1TbhKMOp/628Nz+3wucEKOKiY/6REBB0awpfPXXng8x6oFT8mEe+eFZwM6UTZKMO58PYWB2BUoq3KuLZWA0iIrN5l0EOTf4y0aTFs1KXROvrx2TbPyeNjYtPqtuMZq7Mo6L0GGWp5zehmxpUnuWBsW8/tR/8NLpfFQHucZnA+nnsS3Oj/qzbaf96oTjl1Ov4T4WVRbNK4yjzUre+L+NleOrZygXTQnqPLtPnhKmoHjJ9dtyTRp1C89NxNHUqVeacwp0Q8v+plPxr+fS8zSCMVeEWgumsmmLhFiaFLxHQ14VPYB+ycRpMi6FAZVPNXPbXtfjWi0g=="; // 2. Create instance of SDK load settings with your license key const loadSettings = new PhotoPaySDK.WasmSDKLoadSettings( licenseKey ); diff --git a/examples/ui/basic/index.html b/examples/ui/basic/index.html deleted file mode 100644 index 6ccdd77..0000000 --- a/examples/ui/basic/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Example: PhotoPay UI - - - - - - - - - - - diff --git a/examples/umd/index.html b/examples/umd/index.html index cf21451..5bf3b35 100644 --- a/examples/umd/index.html +++ b/examples/umd/index.html @@ -23,7 +23,7 @@

Loading...

- + + + + + + +``` + +*Keep in mind that Unpkg CDN is used for demonstration, it's not intended to be used in production!* + +### Installation via NPM + +```sh +# Install latest version of UI component via NPM or Yarn +npm install @microblink/photopay-in-browser-sdk # OR yarn add @microblink/photopay-in-browser-sdk + +# Copy JS file to folder where other JS assets are located +cp -r node_modules/@microblink/photopay-in-browser-sdk/ui/dist/* src/public/js/ + +# Copy WASM resources from SDK to folder where other static assets are located +cp -r node_modules/@microblink/photopay-in-browser-sdk/resources/* src/public/assets/ +``` + +```html + + + + + + + + + +``` + +### Examples and API documentation + +A demo app with multiple UI components alongside with source code can be found in the [demo.html](demo.html) file. + +Example apps are located in the [examples](examples) directory, where minimal JavaScript example is located in the [examples/javascript](examples/javascript) directory, while the minimal TypeScript example is located in the [examples/typescript](examples/typescript) directory. + +Auto-generated API documentation of UI component is located in the [docs](docs) directory. + +## Customization + +All attributes, properties and events of UI component can be seen in [`` API documentation](docs/components/photopay-in-browser/readme.md). + +### UI customization + +UI component relies on CSS variables which can be used to override the default styles. + +All CSS variables are defined in [\_globals.scss](src/components/shared/styles/_globals.scss) file. + +```css +/** + * Example code which modifies default values of CSS variables used by an + * instance of UI component. + */ +photopay-in-browser { + --mb-font-family: inherit; + --mb-component-background: #FFF; + --mb-component-font-color: #000; + --mb-component-font-size: 14px; +} +``` + +### Custom icons + +It's possible to change the default icons used by the UI component during configuration. + +```javascript +const el = document.querySelector('photopay-in-browser'); + +// Value provided to this property will be used for setting the `src` attribute +// of element. +el.iconSpinner = '/images/icon-spinner.gif'; +``` + +For a full list of customizable icons, see [`` API documentation](docs/components/photopay-in-browser/readme.md). + +### Localization + +It's possible to override the default messages defined in the [translation.service.ts](src/utils/translation.service.ts) file. + +```javascript +const el = document.querySelector('photopay-in-browser'); + +el.translations = { + 'action-message': 'Alternative CTA', + + // During the camera scan action, messages can be split in multiple lines by + // providing array of strings instead of a plain string. + 'camera-feedback-scan-front': ['Place the front side', 'of a document'] +} +``` + +#### RTL support + +To use UI component in RTL interfaces, explicitly set `dir="rtl"` attribute on HTML element. + +```html + +``` diff --git a/ui/demo.html b/ui/demo.html index bc58246..89853a3 100644 --- a/ui/demo.html +++ b/ui/demo.html @@ -4,7 +4,8 @@ PhotoPay UI: examples - - - + + @@ -168,16 +163,64 @@

Minimal example

+ + + +
+

Example with feedback message

+

UI component with default behaviour where it's required to set valid license key and recognizer.

+

Recognizer: CroatiaPdf417PaymentRecognizer

+ +
<photopay-in-browser
+engine-location="http://localhost/resources/"
+license-key="LICENSE-KEY-GOES-HERE"
+recognizers="CroatiaPdf417PaymentRecognizer"
+></photopay-in-browser>
+
+ + +

Custom messages

It's possible to change default text messages which are displayed to the user.

@@ -190,26 +233,39 @@

Custom messages

recognizers="CroatiaPdf417PaymentRecognizer" ></photopay-in-browser> <script> - const photopay = document.querySelector('photopay-in-browser'); + const el = document.querySelector('photopay-in-browser'); // See src/utils/translations.ts for possible translation keys - photopay.translations = { + el.translations = { 'action-message': 'Alternative CTA' }; </script>
@@ -225,33 +281,45 @@

RTL support

dir="rtl" ></photopay-in-browser> <script> - const photopay = document.querySelector('photopay-in-browser'); + const el = document.querySelector('photopay-in-browser'); // See src/utils/translations.ts for possible translation keys - photopay.translations = { + el.translations = { 'action-message': 'Alternative CTA' }; </script>

UI customization

-

It's possible to customize UI of PhotoPay component by using preset CSS variables.

+

It's possible to ize UI of CroatiaPdf417PaymentRecognizer component by using preset CSS variables.

Recognizer: CroatiaPdf417PaymentRecognizer

@@ -318,11 +386,23 @@

CSS variables

@@ -388,7 +468,7 @@

CSS variables

customizatorForm.addEventListener('submit', ev => { ev.preventDefault(); - + document.querySelector( '#example-customization .box' ).style.background = document.getElementById('customization-background').value; @@ -407,4 +487,4 @@

CSS variables

- + \ No newline at end of file diff --git a/ui/docs/components/photopay-in-browser/readme.md b/ui/docs/components/photopay-in-browser/readme.md index ac70b4c..c9ce0bd 100644 --- a/ui/docs/components/photopay-in-browser/readme.md +++ b/ui/docs/components/photopay-in-browser/readme.md @@ -7,40 +7,78 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ----------- | -| `allowHelloMessage` | `allow-hello-message` | Write a hello message to the browser console when license check is successfully performed. Hello message will contain the name and version of the SDK, which are required information for all support tickets. Default value is true. | `boolean` | `true` | -| `enableDrag` | `enable-drag` | Set to 'false' if component should not enable drag and drop functionality. Default value is 'true'. | `boolean` | `true` | -| `engineLocation` | `engine-location` | Absolute location of WASM and related JS/data files. Useful when resource files should be loaded over CDN, or when web frameworks/libraries are used which store resources in specific locations, e.g. inside "assets" folder. Important: if engine is hosted on another origin, CORS must be enabled between two hosts. That is, server where engine is hosted must have 'Access-Control-Allow-Origin' header for the location of the web app. Important: SDK and WASM resources must be from the same version of package. Default value is empty string, i.e. "". In case of empty string, value of "window.location.origin" property is going to be used. | `string` | `''` | -| `hideFeedback` | `hide-feedback` | If set to 'true', UI component will not display feedback, i.e. information and error messages. Setting this attribute to 'false' won't disable 'scanError' and 'scanInfo' events. Default value is 'false'. | `boolean` | `false` | -| `hideLoadingAndErrorUi` | `hide-loading-and-error-ui` | If set to 'true', UI component will become visible after successful SDK initialization. Also, error screen is not going to be displayed in case of initialization error. If set to 'false', loading and error screens of the UI component will be visible during initialization and in case of an error. Default value is 'false'. | `boolean` | `false` | -| `iconCameraActive` | `icon-camera-active` | Hover state of iconCameraDefault. | `string` | `undefined` | -| `iconCameraDefault` | `icon-camera-default` | Provide alternative camera icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative camera icon. Image is scaled to 20x20 pixels. | `string` | `undefined` | -| `iconGalleryActive` | `icon-gallery-active` | Hover state of iconGalleryDefault. | `string` | `undefined` | -| `iconGalleryDefault` | `icon-gallery-default` | Provide alternative gallery icon. This icon is also used during drag and drop action. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 20x20 pixels. In drag and drop dialog image is scaled to 24x24 pixels. | `string` | `undefined` | -| `iconInvalidFormat` | `icon-invalid-format` | Provide alternative invalid format icon which is used during drag and drop action. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. | `string` | `undefined` | -| `iconSpinner` | `icon-spinner` | Provide alternative loading icon. CSS rotation is applied to this icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. | `string` | `undefined` | -| `includeSuccessFrame` | `include-success-frame` | Set to 'true' if success frame should be included in final scanning results. Default value is 'false'. | `boolean` | `false` | -| `licenseKey` | `license-key` | License key which is going to be used to unlock WASM library. Keep in mind that UI component will reinitialize every time license key is changed. | `string` | `undefined` | -| `rawRecognizerOptions` | `recognizer-options` | Specify additional recognizer options. Example: @TODO | `string` | `undefined` | -| `rawRecognizers` | `recognizers` | List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting HTML attribute "recognizers", for example: `` | `string` | `undefined` | -| `rawTranslations` | `translations` | Set custom translations for UI component. List of available translation keys can be found in `src/utils/translation.service.ts` file. | `string` | `undefined` | -| `recognizerOptions` | -- | Specify additional recognizer options. Example: @TODO | `string[]` | `undefined` | -| `recognizers` | -- | List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting JS property "recognizers", for example: ``` const photopay = document.querySelector('photopay-in-browser'); photopay.recognizers = ['CroatiaPdf417PaymentRecognizer', 'CroatiaQrCodePaymentRecognizer']; ``` | `string[]` | `undefined` | -| `scanFromCamera` | `scan-from-camera` | Set to 'true' if scan from camera should be enabled. If set to 'true' and camera is not available or disabled, related button will be visible but disabled. Default value is 'true'. | `boolean` | `true` | -| `scanFromImage` | `scan-from-image` | Set to 'true' if scan from image should be enabled. Default value is 'true'. | `boolean` | `true` | -| `translations` | -- | Set custom translations for UI component. List of available translation keys can be found in `src/utils/translation.service.ts` file. | `{ [key: string]: string; }` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ---------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ----------- | +| `allowHelloMessage` | `allow-hello-message` | Write a hello message to the browser console when license check is successfully performed. Hello message will contain the name and version of the SDK, which are required information for all support tickets. Default value is true. | `boolean` | `true` | +| `cameraId` | `camera-id` | Camera device ID passed from root component. Client can choose which camera to turn on if array of cameras exists. | `string` | `null` | +| `enableDrag` | `enable-drag` | Set to 'false' if component should not enable drag and drop functionality. Default value is 'true'. | `boolean` | `true` | +| `engineLocation` | `engine-location` | Absolute location of WASM and related JS/data files. Useful when resource files should be loaded over CDN, or when web frameworks/libraries are used which store resources in specific locations, e.g. inside "assets" folder. Important: if engine is hosted on another origin, CORS must be enabled between two hosts. That is, server where engine is hosted must have 'Access-Control-Allow-Origin' header for the location of the web app. Important: SDK and WASM resources must be from the same version of package. Default value is empty string, i.e. "". In case of empty string, value of "window.location.origin" property is going to be used. | `string` | `''` | +| `hideFeedback` | `hide-feedback` | If set to 'true', UI component will not display feedback, i.e. information and error messages. Setting this attribute to 'false' won't disable 'scanError' and 'scanInfo' events. Default value is 'false'. | `boolean` | `false` | +| `hideLoadingAndErrorUi` | `hide-loading-and-error-ui` | If set to 'true', UI component will become visible after successful SDK initialization. Also, error screen is not going to be displayed in case of initialization error. If set to 'false', loading and error screens of the UI component will be visible during initialization and in case of an error. Default value is 'false'. | `boolean` | `false` | +| `iconCameraActive` | `icon-camera-active` | Hover state of iconCameraDefault. | `string` | `undefined` | +| `iconCameraDefault` | `icon-camera-default` | Provide alternative camera icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative camera icon. Image is scaled to 20x20 pixels. | `string` | `undefined` | +| `iconGalleryActive` | `icon-gallery-active` | Hover state of iconGalleryDefault. | `string` | `undefined` | +| `iconGalleryDefault` | `icon-gallery-default` | Provide alternative gallery icon. This icon is also used during drag and drop action. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 20x20 pixels. In drag and drop dialog image is scaled to 24x24 pixels. | `string` | `undefined` | +| `iconInvalidFormat` | `icon-invalid-format` | Provide alternative invalid format icon which is used during drag and drop action. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. | `string` | `undefined` | +| `iconSpinnerFromGalleryExperience` | `icon-spinner-from-gallery-experience` | Provide alternative loading icon. CSS rotation is applied to this icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. | `string` | `undefined` | +| `iconSpinnerScreenLoading` | `icon-spinner-screen-loading` | Provide alternative loading icon. CSS rotation is applied to this icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. | `string` | `undefined` | +| `includeSuccessFrame` | `include-success-frame` | Set to 'true' if success frame should be included in final scanning results. Default value is 'false'. | `boolean` | `false` | +| `licenseKey` | `license-key` | License key which is going to be used to unlock WASM library. Keep in mind that UI component will reinitialize every time license key is changed. | `string` | `undefined` | +| `rawRecognizers` | `recognizers` | List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting HTML attribute "recognizers", for example: `` | `string` | `undefined` | +| `rawTranslations` | `translations` | Set custom translations for UI component. List of available translation keys can be found in `src/utils/translation.service.ts` file. | `string` | `undefined` | +| `recognizerOptions` | -- | Specify recognizer options. This option can only bet set as a JavaScript property. Pass an object to `recognizerOptions` property where each key represents a recognizer, while the value represents desired recognizer options. ``` photopay.recognizerOptions = { 'CroatiaBaseBarcodePaymentRecognizer': { 'shouldSanitize': true } } ``` For a full list of available recognizer options see source code of a recognizer. For example, list of available recognizer options for CroatiaBaseBarcodePaymentRecognizer can be seen in the `src/Recognizers/PhotoPay/Croatia/CroatiaBaseBarcodePaymentRecognizer.ts` file. | `{ [key: string]: any; }` | `undefined` | +| `recognizers` | -- | List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting JS property "recognizers", for example: ``` const photopay = document.querySelector('photopay-in-browser'); photopay.recognizers = ['CroatiaPdf417PaymentRecognizer', 'CroatiaQrCodePaymentRecognizer']; ``` | `string[]` | `undefined` | +| `scanFromCamera` | `scan-from-camera` | Set to 'true' if scan from camera should be enabled. If set to 'true' and camera is not available or disabled, related button will be visible but disabled. Default value is 'true'. | `boolean` | `true` | +| `scanFromImage` | `scan-from-image` | Set to 'true' if scan from image should be enabled. Default value is 'true'. | `boolean` | `true` | +| `showActionLabels` | `show-action-labels` | Set to 'true' if text labels should be displayed below action buttons. Default value is 'false'. | `boolean` | `false` | +| `showCameraFeedbackBarcodeMessage` | `show-camera-feedback-barcode-message` | Set to 'true' if for Barcode scanning camera feedback message should be displayed on camera screen. Default value is 'false'. | `boolean` | `false` | +| `showModalWindows` | `show-modal-windows` | Set to 'true' if modal window should be displayed in case of an error. Default value is 'false'. | `boolean` | `false` | +| `showScanningLine` | `show-scanning-line` | Scan line animation option passed from root component. Client can choose if scan line animation will be present in UI. Default value is 'false' | `boolean` | `false` | +| `translations` | -- | Set custom translations for UI component. List of available translation keys can be found in `src/utils/translation.service.ts` file. | `{ [key: string]: string; }` | `undefined` | +| `wasmType` | `wasm-type` | Defines the type of the WebAssembly build that will be loaded. If omitted, SDK will determine the best possible WebAssembly build which should be loaded based on the browser support. Available WebAssembly builds: - 'BASIC' - 'ADVANCED' - 'ADVANCED_WITH_THREADS' For more information about different WebAssembly builds, check out the `src/MicroblinkSDK/WasmType.ts` file. | `string` | `''` | ## Events -| Event | Description | Type | -| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -| `fatalError` | Event which is emitted during initialization of UI component. Each event contains `code` property which has deatils about fatal errror. | `CustomEvent` | -| `feedback` | Event which is emitted during positive or negative user feedback. If attribute/property `hideFeedback` is set to `false`, UI component will display the feedback. | `CustomEvent` | -| `ready` | Event which is emitted when UI component is successfully initialized and ready for use. | `CustomEvent` | -| `scanError` | Event which is emitted during or immediately after scan error. | `CustomEvent` | -| `scanSuccess` | Event which is emitted after successful scan. This event contains recognition results. | `CustomEvent` | +| Event | Description | Type | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | +| `cameraScanStarted` | Event which is emitted when camera scan is started, i.e. when user clicks on _scan from camera_ button. | `CustomEvent` | +| `fatalError` | Event which is emitted during initialization of UI component. Each event contains `code` property which has deatils about fatal errror. | `CustomEvent` | +| `feedback` | Event which is emitted during positive or negative user feedback. If attribute/property `hideFeedback` is set to `false`, UI component will display the feedback. | `CustomEvent` | +| `imageScanStarted` | Event which is emitted when image scan is started, i.e. when user clicks on _scan from gallery button. | `CustomEvent` | +| `ready` | Event which is emitted when UI component is successfully initialized and ready for use. | `CustomEvent` | +| `scanError` | Event which is emitted during or immediately after scan error. | `CustomEvent` | +| `scanSuccess` | Event which is emitted after successful scan. This event contains recognition results. | `CustomEvent` | + + +## Methods + +### `setUiMessage(state: 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK', message: string) => Promise` + +Show message alongside UI component. + +Possible values for `state` are 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK'. + +#### Returns + +Type: `Promise` + + + +### `setUiState(state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS') => Promise` + +Control UI state of camera overlay. + +Possible values are 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS'. + +In case of state `ERROR` and if `showModalWindows` is set to `true`, modal window +with error message will be displayed. Otherwise, UI will close. + +#### Returns + +Type: `Promise` + + ## Dependencies @@ -61,8 +99,10 @@ graph TD; mb-component --> mb-spinner mb-component --> mb-button mb-component --> mb-overlay - mb-component --> mb-camera-experience mb-component --> mb-modal + mb-component --> mb-camera-experience + mb-component --> mb-api-process-status + mb-api-process-status --> mb-modal style photopay-in-browser fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/ui/docs/components/shared/mb-api-process-status/readme.md b/ui/docs/components/shared/mb-api-process-status/readme.md new file mode 100644 index 0000000..d1dd528 --- /dev/null +++ b/ui/docs/components/shared/mb-api-process-status/readme.md @@ -0,0 +1,45 @@ +# mb-api-process-status + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------------- | --------- | ------------------------------------------------------------------------------------ | --------------------------------------------- | ----------- | +| `state` | `state` | State value of API processing received from parent element ('loading' or 'success'). | `"ERROR" \| "LOADING" \| "NONE" \| "SUCCESS"` | `undefined` | +| `translationService` | -- | Instance of TranslationService passed from parent component. | `TranslationService` | `undefined` | +| `visible` | `visible` | Element visibility, default is 'false'. | `boolean` | `false` | + + +## Events + +| Event | Description | Type | +| ---------------- | ------------------------------------------- | ------------------- | +| `closeFromStart` | Emitted when user clicks on 'x' button. | `CustomEvent` | +| `closeTryAgain` | Emitted when user clicks on 'Retry' button. | `CustomEvent` | + + +## Dependencies + +### Used by + + - [mb-component](../mb-component) + +### Depends on + +- [mb-modal](../mb-modal) + +### Graph +```mermaid +graph TD; + mb-api-process-status --> mb-modal + mb-component --> mb-api-process-status + style mb-api-process-status fill:#f9f,stroke:#333,stroke-width:4px +``` + +---------------------------------------------- + +*Built with [StencilJS](https://stenciljs.com/)* diff --git a/ui/docs/components/shared/mb-button/readme.md b/ui/docs/components/shared/mb-button/readme.md index 3cf0679..40ee5c5 100644 --- a/ui/docs/components/shared/mb-button/readme.md +++ b/ui/docs/components/shared/mb-button/readme.md @@ -14,6 +14,7 @@ | `imageAlt` | `image-alt` | Passed description text for image element from parent component. | `string` | `''` | | `imageSrcActive` | `image-src-active` | Passed image from parent component. | `string` | `''` | | `imageSrcDefault` | `image-src-default` | Passed image from parent component. | `string` | `''` | +| `label` | `label` | Set to string which should be displayed below the icon. If omitted, nothing will show. | `string` | `''` | | `preventDefault` | `prevent-default` | Set to 'true' if default event should be prevented. | `boolean` | `false` | | `translationService` | -- | Instance of TranslationService passed from root component. | `TranslationService` | `undefined` | | `visible` | `visible` | Set to 'true' if button should be visible. | `boolean` | `false` | diff --git a/ui/docs/components/shared/mb-camera-experience/readme.md b/ui/docs/components/shared/mb-camera-experience/readme.md index 4080730..8451b13 100644 --- a/ui/docs/components/shared/mb-camera-experience/readme.md +++ b/ui/docs/components/shared/mb-camera-experience/readme.md @@ -7,22 +7,37 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------- | -| `showOverlay` | `show-overlay` | 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. | `boolean` | `true` | -| `translationService` | -- | Instance of TranslationService passed from root component. | `TranslationService` | `undefined` | -| `type` | `type` | Choose desired camera experience. Each experience type must be implemented in this component. | `CameraExperience.Barcode \| CameraExperience.CardCombined \| CameraExperience.CardSingleSide` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ---------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------- | +| `apiState` | `api-state` | Api state passed from root component. | `string` | `undefined` | +| `cameraFlipped` | `camera-flipped` | Camera horizontal state passed from root component. Horizontal camera image can be mirrored | `boolean` | `false` | +| `showCameraFeedbackBarcodeMessage` | `show-camera-feedback-barcode-message` | Show camera feedback message on camera for Barcode scanning | `boolean` | `false` | +| `showOverlay` | `show-overlay` | 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. | `boolean` | `true` | +| `showScanningLine` | `show-scanning-line` | Show scanning line on camera | `boolean` | `false` | +| `translationService` | -- | Instance of TranslationService passed from root component. | `TranslationService` | `undefined` | +| `type` | `type` | Choose desired camera experience. Each experience type must be implemented in this component. | `CameraExperience.Barcode \| CameraExperience.BlinkCard \| CameraExperience.CardCombined \| CameraExperience.CardSingleSide` | `undefined` | ## Events -| Event | Description | Type | -| ------- | --------------------------------------- | ------------------- | -| `close` | Emitted when user clicks on 'X' button. | `CustomEvent` | +| Event | Description | Type | +| ------------------ | ---------------------------------------- | ------------------- | +| `close` | Emitted when user clicks on 'X' button. | `CustomEvent` | +| `flipCameraAction` | Emitted when user clicks on Flip button. | `CustomEvent` | ## Methods +### `setCameraFlipState(isFlipped: boolean) => Promise` + +Method is exposed outside which allow us to control Camera Flip state from parent component. + +#### Returns + +Type: `Promise` + + + ### `setState(state: CameraExperienceState, isBackSide?: boolean, force?: boolean) => Promise` Set camera scanning state. diff --git a/ui/docs/components/shared/mb-component/readme.md b/ui/docs/components/shared/mb-component/readme.md index 724def1..2f5d143 100644 --- a/ui/docs/components/shared/mb-component/readme.md +++ b/ui/docs/components/shared/mb-component/readme.md @@ -7,37 +7,64 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ----------------------- | --------------------------- | ---------------------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allowHelloMessage` | `allow-hello-message` | See description in public component. | `boolean` | `true` | -| `enableDrag` | `enable-drag` | See description in public component. | `boolean` | `true` | -| `engineLocation` | `engine-location` | See description in public component. | `string` | `''` | -| `hideLoadingAndErrorUi` | `hide-loading-and-error-ui` | See description in public component. | `boolean` | `false` | -| `iconCameraActive` | `icon-camera-active` | | `string` | `'data:image/svg+xml;utf8,'` | -| `iconCameraDefault` | `icon-camera-default` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | -| `iconGalleryActive` | `icon-gallery-active` | | `string` | `'data:image/svg+xml;utf8,'` | -| `iconGalleryDefault` | `icon-gallery-default` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | -| `iconInvalidFormat` | `icon-invalid-format` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | -| `iconSpinner` | `icon-spinner` | See description in public component. | `string` | `undefined` | -| `includeSuccessFrame` | `include-success-frame` | See description in public component. | `boolean` | `false` | -| `licenseKey` | `license-key` | See description in public component. | `string` | `undefined` | -| `recognizerOptions` | -- | See description in public component. | `string[]` | `undefined` | -| `recognizers` | -- | See description in public component. | `string[]` | `undefined` | -| `rtl` | `rtl` | See description in public component. | `boolean` | `false` | -| `scanFromCamera` | `scan-from-camera` | See description in public component. | `boolean` | `true` | -| `scanFromImage` | `scan-from-image` | See description in public component. | `boolean` | `true` | -| `translationService` | -- | Instance of TranslationService passed from root component. | `TranslationService` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ---------------------------------- | -------------------------------------- | ---------------------------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowHelloMessage` | `allow-hello-message` | See description in public component. | `boolean` | `true` | +| `cameraId` | `camera-id` | Camera device ID passed from root component. | `string` | `null` | +| `enableDrag` | `enable-drag` | See description in public component. | `boolean` | `true` | +| `engineLocation` | `engine-location` | See description in public component. | `string` | `''` | +| `hideLoadingAndErrorUi` | `hide-loading-and-error-ui` | See description in public component. | `boolean` | `false` | +| `iconCameraActive` | `icon-camera-active` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | +| `iconCameraDefault` | `icon-camera-default` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | +| `iconGalleryActive` | `icon-gallery-active` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | +| `iconGalleryDefault` | `icon-gallery-default` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | +| `iconInvalidFormat` | `icon-invalid-format` | See description in public component. | `string` | `'data:image/svg+xml;utf8,'` | +| `iconSpinnerFromGalleryExperience` | `icon-spinner-from-gallery-experience` | See description in public component. | `string` | `undefined` | +| `iconSpinnerScreenLoading` | `icon-spinner-screen-loading` | See description in public component. | `string` | `undefined` | +| `includeSuccessFrame` | `include-success-frame` | See description in public component. | `boolean` | `false` | +| `licenseKey` | `license-key` | See description in public component. | `string` | `undefined` | +| `recognizerOptions` | -- | See description in public component. | `{ [key: string]: any; }` | `undefined` | +| `recognizers` | -- | See description in public component. | `string[]` | `undefined` | +| `rtl` | `rtl` | See description in public component. | `boolean` | `false` | +| `scanFromCamera` | `scan-from-camera` | See description in public component. | `boolean` | `true` | +| `scanFromImage` | `scan-from-image` | See description in public component. | `boolean` | `true` | +| `sdkService` | -- | Instance of SdkService passed from root component. | `SdkService` | `undefined` | +| `showActionLabels` | `show-action-labels` | See description in public component. | `boolean` | `false` | +| `showCameraFeedbackBarcodeMessage` | `show-camera-feedback-barcode-message` | See description in public component. | `boolean` | `false` | +| `showModalWindows` | `show-modal-windows` | See description in public component. | `boolean` | `false` | +| `showScanningLine` | `show-scanning-line` | See description in public component. | `boolean` | `false` | +| `thoroughScanFromImage` | `thorough-scan-from-image` | See description in public component. | `boolean` | `false` | +| `translationService` | -- | Instance of TranslationService passed from root component. | `TranslationService` | `undefined` | +| `wasmType` | `wasm-type` | See description in public component. | `string` | `undefined` | ## Events -| Event | Description | Type | -| ------------- | ----------------------------------------------------------------------------- | ------------------------------- | -| `fatalError` | See event 'fatalError' in public component. | `CustomEvent` | -| `feedback` | Event containing FeedbackMessage which can be passed to MbFeedback component. | `CustomEvent` | -| `ready` | See event 'ready' in public component. | `CustomEvent` | -| `scanError` | See event 'scanError' in public component. | `CustomEvent` | -| `scanSuccess` | See event 'scanSuccess' in public component. | `CustomEvent` | +| Event | Description | Type | +| ------------------- | ----------------------------------------------------------------------------- | ------------------------------- | +| `cameraScanStarted` | See event 'cameraScanStarted' in public component. | `CustomEvent` | +| `fatalError` | See event 'fatalError' in public component. | `CustomEvent` | +| `feedback` | Event containing FeedbackMessage which can be passed to MbFeedback component. | `CustomEvent` | +| `imageScanStarted` | See event 'imageScanStarted' in public component. | `CustomEvent` | +| `ready` | See event 'ready' in public component. | `CustomEvent` | +| `scanError` | See event 'scanError' in public component. | `CustomEvent` | +| `scanSuccess` | See event 'scanSuccess' in public component. | `CustomEvent` | + + +## Methods + +### `setUiState(state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS') => Promise` + +Method is exposed outside which allow us to control UI state from parent component. + +In case of state `ERROR` and if `showModalWindows` is set to `true`, modal window +with error message will be displayed. + +#### Returns + +Type: `Promise` + + ## Dependencies @@ -52,8 +79,9 @@ - [mb-spinner](../mb-spinner) - [mb-button](../mb-button) - [mb-overlay](../mb-overlay) -- [mb-camera-experience](../mb-camera-experience) - [mb-modal](../mb-modal) +- [mb-camera-experience](../mb-camera-experience) +- [mb-api-process-status](../mb-api-process-status) ### Graph ```mermaid @@ -62,8 +90,10 @@ graph TD; mb-component --> mb-spinner mb-component --> mb-button mb-component --> mb-overlay - mb-component --> mb-camera-experience mb-component --> mb-modal + mb-component --> mb-camera-experience + mb-component --> mb-api-process-status + mb-api-process-status --> mb-modal photopay-in-browser --> mb-component style mb-component fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/ui/docs/components/shared/mb-modal/readme.md b/ui/docs/components/shared/mb-modal/readme.md index d0c8ff5..aecfda5 100644 --- a/ui/docs/components/shared/mb-modal/readme.md +++ b/ui/docs/components/shared/mb-modal/readme.md @@ -7,27 +7,32 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| --------- | --------- | ------------------------------------ | -------------- | ----------- | -| `content` | -- | Passed content from parent component | `ModalContent` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ----------------- | ------------------ | ------------------------------------------ | --------- | ------- | +| `content` | `content` | Passed body content from parent component | `string` | `""` | +| `contentCentered` | `content-centered` | Center content inside modal | `boolean` | `true` | +| `modalTitle` | `modal-title` | Passed title content from parent component | `string` | `""` | +| `visible` | `visible` | Show modal content | `boolean` | `false` | ## Events -| Event | Description | Type | -| ------- | ------------------------------------------- | ------------------- | -| `close` | Emitted when user clicks on 'Close' button. | `CustomEvent` | +| Event | Description | Type | +| ------- | --------------------------------------- | ------------------- | +| `close` | Emitted when user clicks on 'X' button. | `CustomEvent` | ## Dependencies ### Used by + - [mb-api-process-status](../mb-api-process-status) - [mb-component](../mb-component) ### Graph ```mermaid graph TD; + mb-api-process-status --> mb-modal mb-component --> mb-modal style mb-modal fill:#f9f,stroke:#333,stroke-width:4px ``` diff --git a/ui/examples/README.md b/ui/examples/README.md new file mode 100644 index 0000000..ef892f8 --- /dev/null +++ b/ui/examples/README.md @@ -0,0 +1,34 @@ +# Examples + +Provided examples should help you with integration of this SDK with your app. + +Deployment: + +* When accessing examples via web browser always use `localhost` instead of `127.0.0.1`. +* Examples should be served via HTTPS. + * We recommend usage of NPM package [https-localhost](https://www.npmjs.com/package/https-localhost) for simple local deployment. + +## TypeScript Example + +To run TypeScript example: + +1. Install example dependencies and build an application: + ``` + # Make sure you're in the 'ui/examples/typescript' folder + + # Install dependencies + npm install + + # Build an application in folder 'dist/' + npm run build + ``` +2. Runtime resources are copied to `dist/` folder during build action, check `rollup.config.js` file. +3. Serve `dist/` folder, e.g. `serve dist/`. + +## JavaScript Example + +To run JavaScript examples: + +1. Serve `javascript/` folder, e.g. `serve javascript/`. + * Make sure to have internet connection since runtime resources are loaded from the CDN. + * Alternatively, change resource paths and provide JS bundles. \ No newline at end of file diff --git a/ui/examples/javascript/index.html b/ui/examples/javascript/index.html new file mode 100644 index 0000000..04f6072 --- /dev/null +++ b/ui/examples/javascript/index.html @@ -0,0 +1,83 @@ + + + + + + Example: PhotoPay UI + + + + + + + + + + diff --git a/examples/ui/typescript/.gitignore b/ui/examples/typescript/.gitignore similarity index 100% rename from examples/ui/typescript/.gitignore rename to ui/examples/typescript/.gitignore diff --git a/examples/ui/typescript/package.json b/ui/examples/typescript/package.json similarity index 90% rename from examples/ui/typescript/package.json rename to ui/examples/typescript/package.json index f53c3a0..b45495a 100644 --- a/examples/ui/typescript/package.json +++ b/ui/examples/typescript/package.json @@ -19,6 +19,6 @@ "typescript": "^3.9.5" }, "dependencies": { - "@microblink/photopay-in-browser-sdk": "~7.7.3" + "@microblink/photopay-in-browser-sdk": "~7.7.4" } } \ No newline at end of file diff --git a/examples/ui/typescript/public/index.html b/ui/examples/typescript/public/index.html similarity index 100% rename from examples/ui/typescript/public/index.html rename to ui/examples/typescript/public/index.html diff --git a/examples/ui/typescript/public/style.css b/ui/examples/typescript/public/style.css similarity index 82% rename from examples/ui/typescript/public/style.css rename to ui/examples/typescript/public/style.css index c8a0e18..eae3774 100644 --- a/examples/ui/typescript/public/style.css +++ b/ui/examples/typescript/public/style.css @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + * { box-sizing: border-box; @@ -21,4 +25,4 @@ body margin: 0 auto; padding: 3rem 1.5rem; background-color: #eee; -} \ No newline at end of file +} diff --git a/examples/ui/typescript/rollup.config.js b/ui/examples/typescript/rollup.config.js similarity index 100% rename from examples/ui/typescript/rollup.config.js rename to ui/examples/typescript/rollup.config.js diff --git a/examples/ui/typescript/src/app.ts b/ui/examples/typescript/src/app.ts similarity index 73% rename from examples/ui/typescript/src/app.ts rename to ui/examples/typescript/src/app.ts index dfc37d2..9e173ff 100644 --- a/examples/ui/typescript/src/app.ts +++ b/ui/examples/typescript/src/app.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + // Import typings for UI component import "@microblink/photopay-in-browser-sdk/ui"; @@ -18,7 +22,7 @@ function initializeUiComponent() throw "Could not find UI component!"; } - photopay.licenseKey = "sRwAAAYJbG9jYWxob3N0r/lOPk4/w35CpHlVLi84YJhfzkKNDWt5k6TOIq/BqQY4bts33tGdQc7RawrRGyvbnbj5DEV92rrMkoGMUk3QCySYI9IPMLsIG1aPiOLf1Dhq9FZbGgJvTq6f1O/4pQzxtWn5rN+fs9TqjLz+ei2k0Bv12JREFNsBroMSZUuIDW7uU2bAnW4qW2cedBUaDI9KuhBAtS1B/78M3zb7Fm3dhvMvXj2Mlhl+iwFDwqAhHb5f8vxRICbnqjrb9GO34z4jgJFVQ/mDFZzgLASEJlUO01vfBs18GWt78ups4pIgiIpJph2DhMi76GMxoqQKJfoEs6Wl+VwmIftwWJQbMg=="; + photopay.licenseKey = "sRwAAAYJbG9jYWxob3N0r/lOPk4/w35CpHlVKjc9YGS1TbhKMOp/628Nz+3wucEKOKiY/6REBB0awpfPXXng8x6oFT8mEe+eFZwM6UTZKMO58PYWB2BUoq3KuLZWA0iIrN5l0EOTf4y0aTFs1KXROvrx2TbPyeNjYtPqtuMZq7Mo6L0GGWp5zehmxpUnuWBsW8/tR/8NLpfFQHucZnA+nnsS3Oj/qzbaf96oTjl1Ov4T4WVRbNK4yjzUre+L+NleOrZygXTQnqPLtPnhKmoHjJ9dtyTRp1C89NxNHUqVeacwp0Q8v+plPxr+fS8zSCMVeEWgumsmmLhFiaFLxHQ14VPYB+ycRpMi6FAZVPNXPbXtfjWi0g=="; photopay.engineLocation = window.location.origin; photopay.recognizers = [ "CroatiaPdf417PaymentRecognizer" ]; diff --git a/examples/ui/typescript/tsconfig.json b/ui/examples/typescript/tsconfig.json similarity index 100% rename from examples/ui/typescript/tsconfig.json rename to ui/examples/typescript/tsconfig.json diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 0000000..03896db --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,237 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@stencil/core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.3.0.tgz", + "integrity": "sha512-VZ/Ox0E1kngcmHbJhHUufuLELi+xG3of3LuRI3X2AMWyE82JUVYlOEsQci/YBZWpfc9BS9I36R88prBew22oew==", + "dev": true + }, + "@stencil/postcss": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@stencil/postcss/-/postcss-2.0.0.tgz", + "integrity": "sha512-eTZVQEXb3AGw8A7tstKoOklV6ATXCKASs8VoykyVSYRZCjU0kECM76YgUZkzolwzEq6JG6LVwStT8xpTBSEZ+Q==", + "dev": true, + "requires": { + "postcss": "~8.2.1" + } + }, + "@stencil/sass": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-1.4.1.tgz", + "integrity": "sha512-aFKoqtxZ/8BRbvNFiWRycGiqvMI22Ifn5qsKfq0U23j43XD81jT6d7K0WQd55ejNpoSpdxJcbOuFgQy3mXizfA==", + "dev": true + }, + "@types/autoprefixer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@types/autoprefixer/-/autoprefixer-10.2.0.tgz", + "integrity": "sha512-ClU0uw3HhUra890K4xcf2IQxD6w0WOjPIaKb8jrRXYPHvvUW1P5dGufPlDtTo5gtWPWH+4L6tSBAoAKVf93uBQ==", + "dev": true, + "requires": { + "autoprefixer": "*" + } + }, + "autoprefixer": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.2.5.tgz", + "integrity": "sha512-7H4AJZXvSsn62SqZyJCP+1AWwOuoYpUfK6ot9vm0e87XD6mT8lDywc9D9OTJPMULyGcvmIxzTAMeG2Cc+YX+fA==", + "dev": true, + "requires": { + "browserslist": "^4.16.3", + "caniuse-lite": "^1.0.30001196", + "colorette": "^1.2.2", + "fraction.js": "^4.0.13", + "normalize-range": "^0.1.2", + "postcss-value-parser": "^4.1.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browserslist": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.4.tgz", + "integrity": "sha512-d7rCxYV8I9kj41RH8UKYnvDYCRENUlHRgyXy/Rhr/1BaeLGfiCptEdFE8MIrvGfWbBFNjVYx76SQWvNX1j+/cQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001208", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.712", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "caniuse-lite": { + "version": "1.0.30001211", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001211.tgz", + "integrity": "sha512-v3GXWKofIkN3PkSidLI5d1oqeKNsam9nQkqieoMhP87nxOY0RPDC8X2+jcv8pjV4dRozPLSoMqNii9sDViOlIg==", + "dev": true + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.717", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.717.tgz", + "integrity": "sha512-OfzVPIqD1MkJ7fX+yTl2nKyOE4FReeVfMCzzxQS+Kp43hZYwHwThlGP+EGIZRXJsxCM7dqo8Y65NOX/HP12iXQ==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "fraction.js": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.13.tgz", + "integrity": "sha512-E1fz2Xs9ltlUp+qbiyx9wmt2n9dRzPsS11Jtdb8D2o+cC7wr9xkkKsVKJuBX0ST+LVS+LhLO+SbLJNtfWcJvXA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "nanoid": { + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", + "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==", + "dev": true + }, + "node-releases": { + "version": "1.1.71", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", + "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "postcss": { + "version": "8.2.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.10.tgz", + "integrity": "sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==", + "dev": true, + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.22", + "source-map": "^0.6.1" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/ui/package.json b/ui/package.json index a099268..9cae114 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,26 @@ { - "main": "dist/index.js", - "module": "dist/index.mjs", + "main": "dist/index.cjs.js", + "module": "dist/index.js", "collection": "dist/collection/collection-manifest.json", - "types": "dist/types/components.d.ts" -} \ No newline at end of file + "types": "dist/types/components.d.ts", + "scripts": { + "build": "stencil build --docs --config=stencil.config.ts", + "check-types": "tsc --p ./tsconfig.json --noEmit", + "clean": "rimraf demo dist docs loader resources", + "prepare-assets": "cp -r ../resources/ resources", + "generate": "stencil generate", + "start": "stencil build --dev --watch --serve --config=stencil.config.ts", + "test": "stencil test --spec --e2e", + "test.watch": "stencil test --spec --e2e --watchAll" + }, + "devDependencies": { + "@stencil/core": "~2.3.0", + "@stencil/postcss": "^2.0.0", + "@stencil/sass": "^1.3.2", + "@types/autoprefixer": "^10.2.0", + "autoprefixer": "^10.2.5", + "rimraf": "^3.0.2", + "typescript": "^4.0.5" + }, + "private": true +} diff --git a/ui/src/components.d.ts b/ui/src/components.d.ts index 5a9c12d..221a0ae 100644 --- a/ui/src/components.d.ts +++ b/ui/src/components.d.ts @@ -6,8 +6,23 @@ */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { TranslationService } from "./utils/translation.service"; -import { CameraExperience, CameraExperienceState, EventFatalError, EventReady, EventScanError, EventScanSuccess, FeedbackMessage, ModalContent } from "./utils/data-structures"; +import { CameraExperience, CameraExperienceState, EventFatalError, EventReady, EventScanError, EventScanSuccess, FeedbackMessage } from "./utils/data-structures"; +import { SdkService } from "./utils/sdk.service"; export namespace Components { + interface MbApiProcessStatus { + /** + * State value of API processing received from parent element ('loading' or 'success'). + */ + "state": 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS'; + /** + * Instance of TranslationService passed from parent component. + */ + "translationService": TranslationService; + /** + * Element visibility, default is 'false'. + */ + "visible": boolean; + } interface MbButton { /** * Set to 'true' if button should be disabled, and if click events should not be triggered. @@ -29,6 +44,10 @@ export namespace Components { * Passed image from parent component. */ "imageSrcDefault": string; + /** + * Set to string which should be displayed below the icon. If omitted, nothing will show. + */ + "label": string; /** * Set to 'true' if default event should be prevented. */ @@ -43,14 +62,34 @@ export namespace Components { "visible": boolean; } interface MbCameraExperience { + /** + * Api state passed from root component. + */ + "apiState": string; + /** + * Camera horizontal state passed from root component. Horizontal camera image can be mirrored + */ + "cameraFlipped": boolean; + /** + * Method is exposed outside which allow us to control Camera Flip state from parent component. + */ + "setCameraFlipState": (isFlipped: boolean) => Promise; /** * Set camera scanning state. */ "setState": (state: CameraExperienceState, isBackSide?: boolean, force?: boolean) => Promise; + /** + * Show camera feedback message on camera for Barcode scanning + */ + "showCameraFeedbackBarcodeMessage": boolean; /** * 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. */ "showOverlay": boolean; + /** + * Show scanning line on camera + */ + "showScanningLine": boolean; /** * Instance of TranslationService passed from root component. */ @@ -65,6 +104,10 @@ export namespace Components { * See description in public component. */ "allowHelloMessage": boolean; + /** + * Camera device ID passed from root component. + */ + "cameraId": string | null; /** * See description in public component. */ @@ -77,11 +120,17 @@ export namespace Components { * See description in public component. */ "hideLoadingAndErrorUi": boolean; + /** + * See description in public component. + */ "iconCameraActive": string; /** * See description in public component. */ "iconCameraDefault": string; + /** + * See description in public component. + */ "iconGalleryActive": string; /** * See description in public component. @@ -94,7 +143,11 @@ export namespace Components { /** * See description in public component. */ - "iconSpinner": string; + "iconSpinnerFromGalleryExperience": string; + /** + * See description in public component. + */ + "iconSpinnerScreenLoading": string; /** * See description in public component. */ @@ -106,7 +159,7 @@ export namespace Components { /** * See description in public component. */ - "recognizerOptions": Array; + "recognizerOptions": { [key: string]: any }; /** * See description in public component. */ @@ -123,10 +176,42 @@ export namespace Components { * See description in public component. */ "scanFromImage": boolean; + /** + * Instance of SdkService passed from root component. + */ + "sdkService": SdkService; + /** + * Method is exposed outside which allow us to control UI state from parent component. In case of state `ERROR` and if `showModalWindows` is set to `true`, modal window with error message will be displayed. + */ + "setUiState": (state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS') => Promise; + /** + * See description in public component. + */ + "showActionLabels": boolean; + /** + * See description in public component. + */ + "showCameraFeedbackBarcodeMessage": boolean; + /** + * See description in public component. + */ + "showModalWindows": boolean; + /** + * See description in public component. + */ + "showScanningLine": boolean; + /** + * See description in public component. + */ + "thoroughScanFromImage": boolean; /** * Instance of TranslationService passed from root component. */ "translationService": TranslationService; + /** + * See description in public component. + */ + "wasmType": string | null; } interface MbContainer { } @@ -142,9 +227,21 @@ export namespace Components { } interface MbModal { /** - * Passed content from parent component + * Passed body content from parent component + */ + "content": string; + /** + * Center content inside modal + */ + "contentCentered": boolean; + /** + * Passed title content from parent component */ - "content": ModalContent; + "modalTitle": string; + /** + * Show modal content + */ + "visible": boolean; } interface MbOverlay { /** @@ -177,6 +274,10 @@ export namespace Components { * Write a hello message to the browser console when license check is successfully performed. Hello message will contain the name and version of the SDK, which are required information for all support tickets. Default value is true. */ "allowHelloMessage": boolean; + /** + * Camera device ID passed from root component. Client can choose which camera to turn on if array of cameras exists. + */ + "cameraId": string | null; /** * Set to 'false' if component should not enable drag and drop functionality. Default value is 'true'. */ @@ -216,7 +317,11 @@ export namespace Components { /** * Provide alternative loading icon. CSS rotation is applied to this icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. */ - "iconSpinner": string; + "iconSpinnerFromGalleryExperience": string; + /** + * Provide alternative loading icon. CSS rotation is applied to this icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. + */ + "iconSpinnerScreenLoading": string; /** * Set to 'true' if success frame should be included in final scanning results. Default value is 'false'. */ @@ -225,10 +330,6 @@ export namespace Components { * License key which is going to be used to unlock WASM library. Keep in mind that UI component will reinitialize every time license key is changed. */ "licenseKey": string; - /** - * Specify additional recognizer options. Example: @TODO - */ - "rawRecognizerOptions": string; /** * List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting HTML attribute "recognizers", for example: `` */ @@ -238,9 +339,9 @@ export namespace Components { */ "rawTranslations": string; /** - * Specify additional recognizer options. Example: @TODO + * Specify recognizer options. This option can only bet set as a JavaScript property. Pass an object to `recognizerOptions` property where each key represents a recognizer, while the value represents desired recognizer options. ``` photopay.recognizerOptions = { 'CroatiaBaseBarcodePaymentRecognizer': { 'shouldSanitize': true } } ``` For a full list of available recognizer options see source code of a recognizer. For example, list of available recognizer options for CroatiaBaseBarcodePaymentRecognizer can be seen in the `src/Recognizers/PhotoPay/Croatia/CroatiaBaseBarcodePaymentRecognizer.ts` file. */ - "recognizerOptions": Array; + "recognizerOptions": { [key: string]: any }; /** * List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting JS property "recognizers", for example: ``` const photopay = document.querySelector('photopay-in-browser'); photopay.recognizers = ['CroatiaPdf417PaymentRecognizer', 'CroatiaQrCodePaymentRecognizer']; ``` */ @@ -253,13 +354,47 @@ export namespace Components { * Set to 'true' if scan from image should be enabled. Default value is 'true'. */ "scanFromImage": boolean; + /** + * Show message alongside UI component. Possible values for `state` are 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK'. + */ + "setUiMessage": (state: 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK', message: string) => Promise; + /** + * Control UI state of camera overlay. Possible values are 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS'. In case of state `ERROR` and if `showModalWindows` is set to `true`, modal window with error message will be displayed. Otherwise, UI will close. + */ + "setUiState": (state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS') => Promise; + /** + * Set to 'true' if text labels should be displayed below action buttons. Default value is 'false'. + */ + "showActionLabels": boolean; + /** + * Set to 'true' if for Barcode scanning camera feedback message should be displayed on camera screen. Default value is 'false'. + */ + "showCameraFeedbackBarcodeMessage": boolean; + /** + * Set to 'true' if modal window should be displayed in case of an error. Default value is 'false'. + */ + "showModalWindows": boolean; + /** + * Scan line animation option passed from root component. Client can choose if scan line animation will be present in UI. Default value is 'false' + */ + "showScanningLine": boolean; /** * Set custom translations for UI component. List of available translation keys can be found in `src/utils/translation.service.ts` file. */ "translations": { [key: string]: string }; + /** + * Defines the type of the WebAssembly build that will be loaded. If omitted, SDK will determine the best possible WebAssembly build which should be loaded based on the browser support. Available WebAssembly builds: - 'BASIC' - 'ADVANCED' - 'ADVANCED_WITH_THREADS' For more information about different WebAssembly builds, check out the `src/MicroblinkSDK/WasmType.ts` file. + */ + "wasmType": string; } } declare global { + interface HTMLMbApiProcessStatusElement extends Components.MbApiProcessStatus, HTMLStencilElement { + } + var HTMLMbApiProcessStatusElement: { + prototype: HTMLMbApiProcessStatusElement; + new (): HTMLMbApiProcessStatusElement; + }; interface HTMLMbButtonElement extends Components.MbButton, HTMLStencilElement { } var HTMLMbButtonElement: { @@ -321,6 +456,7 @@ declare global { new (): HTMLPhotopayInBrowserElement; }; interface HTMLElementTagNameMap { + "mb-api-process-status": HTMLMbApiProcessStatusElement; "mb-button": HTMLMbButtonElement; "mb-camera-experience": HTMLMbCameraExperienceElement; "mb-component": HTMLMbComponentElement; @@ -334,6 +470,28 @@ declare global { } } declare namespace LocalJSX { + interface MbApiProcessStatus { + /** + * Emitted when user clicks on 'x' button. + */ + "onCloseFromStart"?: (event: CustomEvent) => void; + /** + * Emitted when user clicks on 'Retry' button. + */ + "onCloseTryAgain"?: (event: CustomEvent) => void; + /** + * State value of API processing received from parent element ('loading' or 'success'). + */ + "state"?: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS'; + /** + * Instance of TranslationService passed from parent component. + */ + "translationService"?: TranslationService; + /** + * Element visibility, default is 'false'. + */ + "visible"?: boolean; + } interface MbButton { /** * Set to 'true' if button should be disabled, and if click events should not be triggered. @@ -355,6 +513,10 @@ declare namespace LocalJSX { * Passed image from parent component. */ "imageSrcDefault"?: string; + /** + * Set to string which should be displayed below the icon. If omitted, nothing will show. + */ + "label"?: string; /** * Event which is triggered when user clicks on button element. This event is not triggered when the button is disabled. */ @@ -373,14 +535,34 @@ declare namespace LocalJSX { "visible"?: boolean; } interface MbCameraExperience { + /** + * Api state passed from root component. + */ + "apiState"?: string; + /** + * Camera horizontal state passed from root component. Horizontal camera image can be mirrored + */ + "cameraFlipped"?: boolean; /** * Emitted when user clicks on 'X' button. */ "onClose"?: (event: CustomEvent) => void; + /** + * Emitted when user clicks on Flip button. + */ + "onFlipCameraAction"?: (event: CustomEvent) => void; + /** + * Show camera feedback message on camera for Barcode scanning + */ + "showCameraFeedbackBarcodeMessage"?: boolean; /** * 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. */ "showOverlay"?: boolean; + /** + * Show scanning line on camera + */ + "showScanningLine"?: boolean; /** * Instance of TranslationService passed from root component. */ @@ -395,6 +577,10 @@ declare namespace LocalJSX { * See description in public component. */ "allowHelloMessage"?: boolean; + /** + * Camera device ID passed from root component. + */ + "cameraId"?: string | null; /** * See description in public component. */ @@ -407,11 +593,17 @@ declare namespace LocalJSX { * See description in public component. */ "hideLoadingAndErrorUi"?: boolean; + /** + * See description in public component. + */ "iconCameraActive"?: string; /** * See description in public component. */ "iconCameraDefault"?: string; + /** + * See description in public component. + */ "iconGalleryActive"?: string; /** * See description in public component. @@ -424,7 +616,11 @@ declare namespace LocalJSX { /** * See description in public component. */ - "iconSpinner"?: string; + "iconSpinnerFromGalleryExperience"?: string; + /** + * See description in public component. + */ + "iconSpinnerScreenLoading"?: string; /** * See description in public component. */ @@ -433,6 +629,10 @@ declare namespace LocalJSX { * See description in public component. */ "licenseKey"?: string; + /** + * See event 'cameraScanStarted' in public component. + */ + "onCameraScanStarted"?: (event: CustomEvent) => void; /** * See event 'fatalError' in public component. */ @@ -441,6 +641,10 @@ declare namespace LocalJSX { * Event containing FeedbackMessage which can be passed to MbFeedback component. */ "onFeedback"?: (event: CustomEvent) => void; + /** + * See event 'imageScanStarted' in public component. + */ + "onImageScanStarted"?: (event: CustomEvent) => void; /** * See event 'ready' in public component. */ @@ -456,7 +660,7 @@ declare namespace LocalJSX { /** * See description in public component. */ - "recognizerOptions"?: Array; + "recognizerOptions"?: { [key: string]: any }; /** * See description in public component. */ @@ -473,10 +677,38 @@ declare namespace LocalJSX { * See description in public component. */ "scanFromImage"?: boolean; + /** + * Instance of SdkService passed from root component. + */ + "sdkService"?: SdkService; + /** + * See description in public component. + */ + "showActionLabels"?: boolean; + /** + * See description in public component. + */ + "showCameraFeedbackBarcodeMessage"?: boolean; + /** + * See description in public component. + */ + "showModalWindows"?: boolean; + /** + * See description in public component. + */ + "showScanningLine"?: boolean; + /** + * See description in public component. + */ + "thoroughScanFromImage"?: boolean; /** * Instance of TranslationService passed from root component. */ "translationService"?: TranslationService; + /** + * See description in public component. + */ + "wasmType"?: string | null; } interface MbContainer { } @@ -488,13 +720,25 @@ declare namespace LocalJSX { } interface MbModal { /** - * Passed content from parent component + * Passed body content from parent component */ - "content"?: ModalContent; + "content"?: string; /** - * Emitted when user clicks on 'Close' button. + * Center content inside modal + */ + "contentCentered"?: boolean; + /** + * Passed title content from parent component + */ + "modalTitle"?: string; + /** + * Emitted when user clicks on 'X' button. */ "onClose"?: (event: CustomEvent) => void; + /** + * Show modal content + */ + "visible"?: boolean; } interface MbOverlay { /** @@ -527,6 +771,10 @@ declare namespace LocalJSX { * Write a hello message to the browser console when license check is successfully performed. Hello message will contain the name and version of the SDK, which are required information for all support tickets. Default value is true. */ "allowHelloMessage"?: boolean; + /** + * Camera device ID passed from root component. Client can choose which camera to turn on if array of cameras exists. + */ + "cameraId"?: string | null; /** * Set to 'false' if component should not enable drag and drop functionality. Default value is 'true'. */ @@ -566,7 +814,11 @@ declare namespace LocalJSX { /** * Provide alternative loading icon. CSS rotation is applied to this icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. */ - "iconSpinner"?: string; + "iconSpinnerFromGalleryExperience"?: string; + /** + * Provide alternative loading icon. CSS rotation is applied to this icon. Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be used to provide location, base64 or any URL of alternative gallery icon. Image is scaled to 24x24 pixels. + */ + "iconSpinnerScreenLoading"?: string; /** * Set to 'true' if success frame should be included in final scanning results. Default value is 'false'. */ @@ -575,6 +827,10 @@ declare namespace LocalJSX { * License key which is going to be used to unlock WASM library. Keep in mind that UI component will reinitialize every time license key is changed. */ "licenseKey"?: string; + /** + * Event which is emitted when camera scan is started, i.e. when user clicks on _scan from camera_ button. + */ + "onCameraScanStarted"?: (event: CustomEvent) => void; /** * Event which is emitted during initialization of UI component. Each event contains `code` property which has deatils about fatal errror. */ @@ -583,6 +839,10 @@ declare namespace LocalJSX { * Event which is emitted during positive or negative user feedback. If attribute/property `hideFeedback` is set to `false`, UI component will display the feedback. */ "onFeedback"?: (event: CustomEvent) => void; + /** + * Event which is emitted when image scan is started, i.e. when user clicks on _scan from gallery button. + */ + "onImageScanStarted"?: (event: CustomEvent) => void; /** * Event which is emitted when UI component is successfully initialized and ready for use. */ @@ -595,10 +855,6 @@ declare namespace LocalJSX { * Event which is emitted after successful scan. This event contains recognition results. */ "onScanSuccess"?: (event: CustomEvent) => void; - /** - * Specify additional recognizer options. Example: @TODO - */ - "rawRecognizerOptions"?: string; /** * List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting HTML attribute "recognizers", for example: `` */ @@ -608,9 +864,9 @@ declare namespace LocalJSX { */ "rawTranslations"?: string; /** - * Specify additional recognizer options. Example: @TODO + * Specify recognizer options. This option can only bet set as a JavaScript property. Pass an object to `recognizerOptions` property where each key represents a recognizer, while the value represents desired recognizer options. ``` photopay.recognizerOptions = { 'CroatiaBaseBarcodePaymentRecognizer': { 'shouldSanitize': true } } ``` For a full list of available recognizer options see source code of a recognizer. For example, list of available recognizer options for CroatiaBaseBarcodePaymentRecognizer can be seen in the `src/Recognizers/PhotoPay/Croatia/CroatiaBaseBarcodePaymentRecognizer.ts` file. */ - "recognizerOptions"?: Array; + "recognizerOptions"?: { [key: string]: any }; /** * List of recognizers which should be used. Available recognizers for PhotoPay: - AustriaQrCodePaymentRecognizer - CroatiaPdf417PaymentRecognizer - CroatiaQrCodePaymentRecognizer - CzechiaQrCodePaymentRecognizer - GermanyQrCodePaymentRecognizer - KosovoCode128PaymentRecognizer - SepaQrCodePaymentRecognizer - SerbiaPdf417PaymentRecognizer - SerbiaQrCodePaymentRecognizer - SlovakiaCode128PaymentRecognizer - SlovakiaDataMatrixPaymentRecognizer - SlovakiaQrCodePaymentRecognizer - SloveniaQrCodePaymentRecognizer - SwitzerlandQrCodePaymentRecognizer Recognizers can be defined by setting JS property "recognizers", for example: ``` const photopay = document.querySelector('photopay-in-browser'); photopay.recognizers = ['CroatiaPdf417PaymentRecognizer', 'CroatiaQrCodePaymentRecognizer']; ``` */ @@ -623,12 +879,33 @@ declare namespace LocalJSX { * Set to 'true' if scan from image should be enabled. Default value is 'true'. */ "scanFromImage"?: boolean; + /** + * Set to 'true' if text labels should be displayed below action buttons. Default value is 'false'. + */ + "showActionLabels"?: boolean; + /** + * Set to 'true' if for Barcode scanning camera feedback message should be displayed on camera screen. Default value is 'false'. + */ + "showCameraFeedbackBarcodeMessage"?: boolean; + /** + * Set to 'true' if modal window should be displayed in case of an error. Default value is 'false'. + */ + "showModalWindows"?: boolean; + /** + * Scan line animation option passed from root component. Client can choose if scan line animation will be present in UI. Default value is 'false' + */ + "showScanningLine"?: boolean; /** * Set custom translations for UI component. List of available translation keys can be found in `src/utils/translation.service.ts` file. */ "translations"?: { [key: string]: string }; + /** + * Defines the type of the WebAssembly build that will be loaded. If omitted, SDK will determine the best possible WebAssembly build which should be loaded based on the browser support. Available WebAssembly builds: - 'BASIC' - 'ADVANCED' - 'ADVANCED_WITH_THREADS' For more information about different WebAssembly builds, check out the `src/MicroblinkSDK/WasmType.ts` file. + */ + "wasmType"?: string; } interface IntrinsicElements { + "mb-api-process-status": MbApiProcessStatus; "mb-button": MbButton; "mb-camera-experience": MbCameraExperience; "mb-component": MbComponent; @@ -645,6 +922,7 @@ export { LocalJSX as JSX }; declare module "@stencil/core" { export namespace JSX { interface IntrinsicElements { + "mb-api-process-status": LocalJSX.MbApiProcessStatus & JSXBase.HTMLAttributes; "mb-button": LocalJSX.MbButton & JSXBase.HTMLAttributes; "mb-camera-experience": LocalJSX.MbCameraExperience & JSXBase.HTMLAttributes; "mb-component": LocalJSX.MbComponent & JSXBase.HTMLAttributes; diff --git a/ui/src/components/photopay-in-browser/photopay-in-browser.scss b/ui/src/components/photopay-in-browser/photopay-in-browser.scss index 58ad2db..9b979ca 100644 --- a/ui/src/components/photopay-in-browser/photopay-in-browser.scss +++ b/ui/src/components/photopay-in-browser/photopay-in-browser.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + @import "../shared/styles/globals"; :host { diff --git a/ui/src/components/photopay-in-browser/photopay-in-browser.tsx b/ui/src/components/photopay-in-browser/photopay-in-browser.tsx index 763e70a..12e70b7 100644 --- a/ui/src/components/photopay-in-browser/photopay-in-browser.tsx +++ b/ui/src/components/photopay-in-browser/photopay-in-browser.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Element, @@ -5,7 +9,8 @@ import { EventEmitter, Host, h, - Prop + Prop, + Method } from '@stencil/core'; import { @@ -17,8 +22,8 @@ import { MicroblinkUI } from '../../utils/data-structures'; +import { SdkService } from '../../utils/sdk.service'; import { TranslationService } from '../../utils/translation.service'; - import * as GenericHelpers from '../../utils/generic.helpers'; @Component({ @@ -59,6 +64,20 @@ export class PhotopayInBrowser implements MicroblinkUI { */ @Prop() licenseKey: string; + /** + * Defines the type of the WebAssembly build that will be loaded. If omitted, SDK will determine + * the best possible WebAssembly build which should be loaded based on the browser support. + * + * Available WebAssembly builds: + * + * - 'BASIC' + * - 'ADVANCED' + * - 'ADVANCED_WITH_THREADS' + * + * For more information about different WebAssembly builds, check out the `src/MicroblinkSDK/WasmType.ts` file. + */ + @Prop() wasmType: string = ''; + /** * List of recognizers which should be used. * @@ -115,18 +134,24 @@ export class PhotopayInBrowser implements MicroblinkUI { @Prop() recognizers: Array; /** - * Specify additional recognizer options. + * Specify recognizer options. This option can only bet set as a JavaScript property. * - * Example: @TODO - */ - @Prop({ attribute: 'recognizer-options' }) rawRecognizerOptions: string; - - /** - * Specify additional recognizer options. + * Pass an object to `recognizerOptions` property where each key represents a recognizer, while + * the value represents desired recognizer options. + * + * ``` + * photopay.recognizerOptions = { + * 'CroatiaBaseBarcodePaymentRecognizer': { + * 'shouldSanitize': true + * } + * } + * ``` * - * Example: @TODO + * For a full list of available recognizer options see source code of a recognizer. For example, + * list of available recognizer options for CroatiaBaseBarcodePaymentRecognizer can be seen in the + * `src/Recognizers/PhotoPay/Croatia/CroatiaBaseBarcodePaymentRecognizer.ts` file. */ - @Prop() recognizerOptions: Array; + @Prop() recognizerOptions: { [key: string]: any }; /** * Set to 'true' if success frame should be included in final scanning results. @@ -177,6 +202,37 @@ export class PhotopayInBrowser implements MicroblinkUI { */ @Prop() scanFromImage: boolean = true; + /** + * Set to 'true' if text labels should be displayed below action buttons. + * + * Default value is 'false'. + */ + @Prop() showActionLabels: boolean = false; + + /** + * Scan line animation option passed from root component. + * + * Client can choose if scan line animation will be present in UI. + * + * Default value is 'false' + * + */ + @Prop() showScanningLine: boolean = false; + + /** + * Set to 'true' if modal window should be displayed in case of an error. + * + * Default value is 'false'. + */ + @Prop() showModalWindows: boolean = false; + + /** + * Set to 'true' if for Barcode scanning camera feedback message should be displayed on camera screen. + * + * Default value is 'false'. + */ + @Prop() showCameraFeedbackBarcodeMessage: boolean = false; + /** * Set custom translations for UI component. List of available translation keys can be found in * `src/utils/translation.service.ts` file. @@ -237,7 +293,25 @@ export class PhotopayInBrowser implements MicroblinkUI { * * Image is scaled to 24x24 pixels. */ - @Prop() iconSpinner: string; + @Prop() iconSpinnerScreenLoading: string; + + /** + * Provide alternative loading icon. CSS rotation is applied to this icon. + * + * Every value that is placed here is passed as a value of `src` attribute to element. This attribute can be + * used to provide location, base64 or any URL of alternative gallery icon. + * + * Image is scaled to 24x24 pixels. + */ + @Prop() iconSpinnerFromGalleryExperience: string; + + /** + * Camera device ID passed from root component. + * + * Client can choose which camera to turn on if array of cameras exists. + * + */ + @Prop() cameraId: string | null = null; /** * Event which is emitted during initialization of UI component. @@ -267,18 +341,52 @@ export class PhotopayInBrowser implements MicroblinkUI { */ @Event() feedback: EventEmitter; + /** + * Event which is emitted when camera scan is started, i.e. when user clicks on + * _scan from camera_ button. + */ + @Event() cameraScanStarted: EventEmitter; + + /** + * Event which is emitted when image scan is started, i.e. when user clicks on + * _scan from gallery button. + */ + @Event() imageScanStarted: EventEmitter; + + /** + * Control UI state of camera overlay. + * + * Possible values are 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS'. + * + * In case of state `ERROR` and if `showModalWindows` is set to `true`, modal window + * with error message will be displayed. Otherwise, UI will close. + */ + @Method() + async setUiState(state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS') { + this.mbComponentEl.setUiState(state); + } + + /** + * Show message alongside UI component. + * + * Possible values for `state` are 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK'. + */ + @Method() + async setUiMessage(state: 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK', message: string) { + this.feedbackEl.show({ state, message }); + } + @Element() hostEl: HTMLElement; async componentWillRender() { const rawRecognizers = GenericHelpers.stringToArray(this.rawRecognizers); this.finalRecognizers = this.recognizers ? this.recognizers : rawRecognizers; - const rawRecognizerOptions = GenericHelpers.stringToArray(this.rawRecognizerOptions); - this.finalRecognizerOptions = this.recognizerOptions ? this.recognizerOptions : rawRecognizerOptions; - const rawTranslations = GenericHelpers.stringToObject(this.rawTranslations); this.finalTranslations = this.translations ? this.translations : rawTranslations; this.translationService = new TranslationService(this.finalTranslations || {}); + + this.sdkService = new SdkService(); } render() { @@ -286,23 +394,32 @@ export class PhotopayInBrowser implements MicroblinkUI { this.mbComponentEl = el as HTMLMbComponentElement } allowHelloMessage={ this.allowHelloMessage } engineLocation={ this.engineLocation } licenseKey={ this.licenseKey } + wasmType={ this.wasmType } recognizers={ this.finalRecognizers } - recognizerOptions={ this.finalRecognizerOptions } + recognizerOptions={ this.recognizerOptions } includeSuccessFrame={ this.includeSuccessFrame } enableDrag={ this.enableDrag } hideLoadingAndErrorUi={ this.hideLoadingAndErrorUi } scanFromCamera={ this.scanFromCamera } scanFromImage={ this.scanFromImage } + showScanningLine={ this.showScanningLine } + showActionLabels={ this.showActionLabels } + showModalWindows={ this.showModalWindows } + showCameraFeedbackBarcodeMessage={ this.showCameraFeedbackBarcodeMessage } iconCameraDefault={ this.iconCameraDefault} iconCameraActive={ this.iconCameraActive } iconGalleryDefault={ this.iconGalleryDefault } iconGalleryActive={ this.iconGalleryActive } - iconInvalid-format={ this.iconInvalidFormat } - iconSpinner={ this.iconSpinner } + iconInvalidFormat={ this.iconInvalidFormat } + iconSpinnerScreenLoading={ this.iconSpinnerScreenLoading } + iconSpinnerFromGalleryExperience={ this.iconSpinnerFromGalleryExperience } + sdkService={ this.sdkService } translationService={ this.translationService } + cameraId={ this.cameraId } onFeedback={ (ev: CustomEvent) => this.feedbackEl.show(ev.detail) }> @@ -315,11 +432,12 @@ export class PhotopayInBrowser implements MicroblinkUI { ); } + private sdkService: SdkService; private translationService: TranslationService; private finalRecognizers: Array; - private finalRecognizerOptions: Array; private finalTranslations: { [key: string]: string }; private feedbackEl!: HTMLMbFeedbackElement; + private mbComponentEl!: HTMLMbComponentElement; } diff --git a/ui/src/components/photopay-in-browser/test/photopay-in-browser.e2e.ts b/ui/src/components/photopay-in-browser/test/photopay-in-browser.e2e.ts index 04167ad..16e32b6 100644 --- a/ui/src/components/photopay-in-browser/test/photopay-in-browser.e2e.ts +++ b/ui/src/components/photopay-in-browser/test/photopay-in-browser.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('photopay-in-browser', () => { diff --git a/ui/src/components/photopay-in-browser/test/photopay-in-browser.spec.tsx b/ui/src/components/photopay-in-browser/test/photopay-in-browser.spec.tsx index 8ccd2aa..225cc99 100644 --- a/ui/src/components/photopay-in-browser/test/photopay-in-browser.spec.tsx +++ b/ui/src/components/photopay-in-browser/test/photopay-in-browser.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { PhotopayInBrowser } from '../photopay-in-browser'; diff --git a/ui/src/components/shared/mb-api-process-status/mb-api-process-status.scss b/ui/src/components/shared/mb-api-process-status/mb-api-process-status.scss new file mode 100644 index 0000000..57c219b --- /dev/null +++ b/ui/src/components/shared/mb-api-process-status/mb-api-process-status.scss @@ -0,0 +1,65 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +@import "../styles/_globals-sass"; +@import "../styles/reticle"; + +*::after, +*::before { + box-sizing: border-box; +} + +:host { + + .message { + display: block; + + position: absolute; + top: 100%; + left: 50%; + transform-origin: center; + transform: translate(-50%, 0); + + margin: 2 * $base-unit 0 0 0; + margin-top: 20px; + padding: 2 * $base-unit 3 * $base-unit; + + font-weight: 500; + text-align: center; + text-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1); + white-space: nowrap; + + color: #fff; + background-color: map-get(map-get(map-get($base-colors, text-quaternary), onlight), foreground); + + -webkit-backdrop-filter: blur(27px); + backdrop-filter: blur(27px); + + border-radius: 2 * $base-unit; + } + + .reticle-container { + position: absolute; + top: 50%; + left: 50%; + + width: 96px; + height: 96px; + transform-origin: center; + transform: translate(-50%, -50%); + + perspective: 600px; + } + +} + +:host button.modal-action-button { + width: 126px; + height: 32px; + border-radius: 0; + border: 0; + background: #48B2E8; + color: #ffffff; + cursor: pointer; +} diff --git a/ui/src/components/shared/mb-api-process-status/mb-api-process-status.tsx b/ui/src/components/shared/mb-api-process-status/mb-api-process-status.tsx new file mode 100644 index 0000000..1ae5207 --- /dev/null +++ b/ui/src/components/shared/mb-api-process-status/mb-api-process-status.tsx @@ -0,0 +1,110 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +import { + Component, + Host, + h, + Prop, + Event, + EventEmitter, +} from '@stencil/core'; + +import { TranslationService } from '../../../utils/translation.service'; + +@Component({ + tag: 'mb-api-process-status', + styleUrl: 'mb-api-process-status.scss', + shadow: true, +}) +export class MbApiProcessStatus { + /** + * Element visibility, default is 'false'. + */ + @Prop() visible: boolean = false; + + /** + * State value of API processing received from parent element ('loading' or 'success'). + */ + @Prop() state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS'; + + /** + * Instance of TranslationService passed from parent component. + */ + @Prop() translationService: TranslationService; + + /** + * Emitted when user clicks on 'Retry' button. + */ + @Event() closeTryAgain: EventEmitter; + + /** + * Emitted when user clicks on 'x' button. + */ + @Event() closeFromStart: EventEmitter; + + render() { + return ( + + + { this.state === 'LOADING' && +
+
+
+
+
+
+
+
+
+ +

{this.translationService.i('process-api-message').toString()}

+
+ } + + { this.state === 'SUCCESS' && +
+
+
+
+
+
+
+
+ + +
+
+ } + + { this.state === 'ERROR' && + this.closeFromStart.emit()} + > +
+ +
+
+ } + +
+ ); + } + + getClassName(): string { + const classNames = []; + + if (this.visible) { + classNames.push('visible'); + } + + return classNames.join(' '); + } +} diff --git a/ui/src/components/shared/mb-api-process-status/test/mb-api-process-status.e2e.ts b/ui/src/components/shared/mb-api-process-status/test/mb-api-process-status.e2e.ts new file mode 100644 index 0000000..40d9b3b --- /dev/null +++ b/ui/src/components/shared/mb-api-process-status/test/mb-api-process-status.e2e.ts @@ -0,0 +1,15 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +import { newE2EPage } from '@stencil/core/testing'; + +describe('mb-api-process-status', () => { + it('renders', async () => { + const page = await newE2EPage(); + await page.setContent(''); + + const element = await page.find('mb-api-process-status'); + expect(element).toHaveClass('hydrated'); + }); +}); diff --git a/ui/src/components/shared/mb-api-process-status/test/mb-api-process-status.spec.tsx b/ui/src/components/shared/mb-api-process-status/test/mb-api-process-status.spec.tsx new file mode 100644 index 0000000..ef03923 --- /dev/null +++ b/ui/src/components/shared/mb-api-process-status/test/mb-api-process-status.spec.tsx @@ -0,0 +1,22 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +import { newSpecPage } from '@stencil/core/testing'; +import { MbApiProcessStatus } from '../mb-api-process-status'; + +describe('mb-api-process-status', () => { + it('renders', async () => { + const page = await newSpecPage({ + components: [MbApiProcessStatus], + html: ``, + }); + expect(page.root).toEqualHtml(` + + + + + + `); + }); +}); diff --git a/ui/src/components/shared/mb-button/mb-button.scss b/ui/src/components/shared/mb-button/mb-button.scss index 725dcae..f429377 100644 --- a/ui/src/components/shared/mb-button/mb-button.scss +++ b/ui/src/components/shared/mb-button/mb-button.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + @import "../styles/globals-sass"; :host { @@ -5,23 +9,21 @@ display: none; - width: $button-size; - height: $button-size; - - background: var(--mb-component-button-background); - border-color: var(--mb-component-button-border-color); + a { + display: block; + margin: 0 auto; - border-radius: var(--mb-component-button-border-radius); - border-style: var(--mb-component-button-border-style); - border-width: var(--mb-component-button-border-width); + width: calc(var(--mb-component-button-size) - 2 * var(--mb-component-button-border-width)); + height: calc(var(--mb-component-button-size) - 2 * var(--mb-component-button-border-width)); - box-shadow: var(--mb-component-button-box-shadow); + background: var(--mb-component-button-background); + border-color: var(--mb-component-button-border-color); - a { - display: block; + border-radius: var(--mb-component-button-border-radius); + border-style: var(--mb-component-button-border-style); + border-width: var(--mb-component-button-border-width); - width: calc(#{$button-size} - 2 * var(--mb-component-button-border-width)); - height: calc(#{$button-size} - 2 * var(--mb-component-button-border-width)); + box-shadow: var(--mb-component-button-box-shadow); text-decoration: none; } @@ -33,29 +35,28 @@ } } -:host(:focus) { +:host a:focus { border-color: var(--mb-component-button-border-color-focus); - outline: 0; } -:host(:hover) { +:host a:hover { border-color: var(--mb-component-button-border-color-hover); } :host(.disabled) { - border-color: var(--mb-component-button-border-color-disabled); - box-shadow: var(--mb-component-button-box-shadow-disabled); - a { + border-color: var(--mb-component-button-border-color-disabled); + box-shadow: var(--mb-component-button-box-shadow-disabled); cursor: default; - outline: 0; } + + img { opacity: .5; } } :host(.visible) { display: block; } -:host(.visible.icon) { +:host(.icon) { a { display: flex; align-items: center; @@ -73,6 +74,20 @@ :host img { display: block; - width: $button-icon-size; - height: $button-icon-size; + width: var(--mb-component-button-icon-size); + height: var(--mb-component-button-icon-size); +} + +/** + * Action labels + */ +:host span { + display: block; + padding-top: $padding-unit-medium; + + font-size: var(--mb-component-font-size); + font-weight: var(--mb-component-font-weight); + line-height: var(--mb-component-line-height); + + color: var(--mb-feedback-font-color-info); } diff --git a/ui/src/components/shared/mb-button/mb-button.tsx b/ui/src/components/shared/mb-button/mb-button.tsx index e66e3d3..83b7d5d 100644 --- a/ui/src/components/shared/mb-button/mb-button.tsx +++ b/ui/src/components/shared/mb-button/mb-button.tsx @@ -1,11 +1,14 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Event, EventEmitter, Host, h, - Prop, - Listen + Prop } from '@stencil/core'; import { TranslationService } from '../../../utils/translation.service'; @@ -52,6 +55,13 @@ export class MbButton { */ @Prop() imageAlt: string = ''; + /** + * Set to string which should be displayed below the icon. + * + * If omitted, nothing will show. + */ + @Prop() label: string = ''; + /** * Instance of TranslationService passed from root component. */ @@ -63,18 +73,10 @@ export class MbButton { */ @Event() buttonClick: EventEmitter; - @Listen('mouseover') handleMouseOver() { - this.iconElem.setAttribute('src', this.imageSrcActive); - } - - @Listen('mouseout') handleMouseOut() { - this.iconElem.setAttribute('src', this.imageSrcDefault); - } - render() { return ( - this.handleClick(ev) }> - + this.handleClick(ev) } ref={el => this.buttonElement = el as HTMLDivElement}> + this.anchorElement = el as HTMLAnchorElement} href="javascript:void(0)"> { this.imageSrcDefault && this.imageAlt === 'action-alt-camera' && { this.iconElem = el as HTMLOrSVGImageElement } /> @@ -87,10 +89,21 @@ export class MbButton { } + { + this.label !== '' && + { this.label } + } ); } + componentDidRender() { + this.iconElem.setAttribute('src', this.imageSrcDefault); + + this.anchorElement.addEventListener('mouseover', () => this.handleMouseOver()); + this.anchorElement.addEventListener('mouseout', () => this.handleMouseOut()); + } + private getClassNames(): string { const classNames = []; @@ -122,6 +135,15 @@ export class MbButton { this.buttonClick.emit(ev); } - private iconElem: HTMLOrSVGImageElement; + private handleMouseOver() { + if (!this.buttonElement.classList.contains('disabled')) this.iconElem.setAttribute('src', this.imageSrcActive); + } + + private handleMouseOut() { + if (!this.buttonElement.classList.contains('disabled')) this.iconElem.setAttribute('src', this.imageSrcDefault); + } + private iconElem: HTMLOrSVGImageElement; + private buttonElement: HTMLDivElement; + private anchorElement: HTMLAnchorElement; } diff --git a/ui/src/components/shared/mb-button/test/mb-button.e2e.ts b/ui/src/components/shared/mb-button/test/mb-button.e2e.ts index f181152..907349c 100644 --- a/ui/src/components/shared/mb-button/test/mb-button.e2e.ts +++ b/ui/src/components/shared/mb-button/test/mb-button.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-button', () => { diff --git a/ui/src/components/shared/mb-button/test/mb-button.spec.tsx b/ui/src/components/shared/mb-button/test/mb-button.spec.tsx index 6f9e033..1d997b6 100644 --- a/ui/src/components/shared/mb-button/test/mb-button.spec.tsx +++ b/ui/src/components/shared/mb-button/test/mb-button.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbButton } from '../mb-button'; diff --git a/ui/src/components/shared/mb-camera-experience/mb-camera-experience.scss b/ui/src/components/shared/mb-camera-experience/mb-camera-experience.scss index 9da8d58..cc25081 100644 --- a/ui/src/components/shared/mb-camera-experience/mb-camera-experience.scss +++ b/ui/src/components/shared/mb-camera-experience/mb-camera-experience.scss @@ -1,400 +1,537 @@ -@import "../styles/globals-sass"; +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ -@import "../styles/reticle"; +@import "../styles/_globals-sass"; + +@import "../styles/_reticle"; +@import "../styles/_blinkcard-rectangle"; +@import "../styles/_barcode-rectangle"; *::after, -*::before { - box-sizing: border-box; -} +*::before { box-sizing: border-box; } :host { display: block; - .close-button { - display: block; - width: 24px; - height: 24px; + .gradient-overlay { + position: absolute; + width: 100%; + height: 112px; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.35625) 0%, rgba(0, 0, 0, 0.25) 20.83%, rgba(0, 0, 0, 0) 100%); + + &.top { top: 0; } + &.bottom { + bottom: 0; + transform: matrix(1, 0, 0, -1, 0, 0); + } + } + .controls { position: absolute; - top: 54px; - right: 16px; + width: 100%; + min-height: 100px; + top: 0; + z-index: 0; svg { + width: 24px; + height: 24px; filter: drop-shadow(0px 1px 4px rgba(0, 0, 0, 0.4)); } - cursor: pointer; - - img { + .close-button { display: block; - width: 100%; - height: 100%; + position: absolute; + width: 24px; + height: 24px; + top: 54px; + right: 16px; + cursor: pointer; } - } - #barcode, - #card-identity { - display: none; + #flipBtn { + background-color: transparent; + background-size: auto; + position: absolute; + top: 54px; + left: 16px; + width: 24px; + height: 24px; + margin: 0; + padding: 0; + user-select: none; + border: 1px solid transparent; + outline: 0; + + transform-style: preserve-3d; + -webkit-perspective: 600px; + -ms-perspective: 600px; + -o-perspective: 600px; + perspective: 600px; + + -webkit-transition: 800ms; + -o-transition: 800ms; + transition: 800ms; + + cursor: pointer; + } - &.visible { - display: block; + #flipBtn.flipped { + -webkit-transform: rotateY(180deg); + -ms-transform: rotateY(180deg); + -o-transform: rotateY(180deg); + transform: rotateY(180deg); } } } +:host(.is-error) .controls { display: none; } + :host::after { width: 92px + 30px; height: 24px + 30px; position: absolute; - bottom: 40px - 15px; + bottom: 10px; left: calc(50% - #{46px + 15px}); - background: no-repeat center url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIyIiBoZWlnaHQ9IjU0IiB2aWV3Qm94PSIwIDAgMTIyIDU0IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9kZCkiPgo8cGF0aCBkPSJNMjIuNjcxNCAyOS44ODgyTDE5LjgxNjIgMzYuNjA0NEgxOS43ODE3TDE2LjkyNyAyOS44ODgySDE1LjEwM0gxNVYzOC43MzA0SDE2LjIzODNWMzEuNDQwNkgxNi4yNzI4TDE5LjMzNDYgMzguNzMwNEgyMC4xNjA4TDIzLjIyMTYgMzEuNDQwNkgyMy4yNTYxVjM4Ljc2NDRIMjQuNDk1NFYyOS44ODgySDIyLjY3MTRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjkuNTE4MSAyOS44ODc3SDI4LjI3ODhWMzguNzYzOUgyOS41MTgxVjI5Ljg4NzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMzkuNDI1NiAzMS44MTIyQzM5LjE4NDUgMzEuNDc0MSAzOC44NDEgMzEuMjM4MSAzOC40NjI5IDMxLjA3QzM4LjA4MzggMzAuOTAxIDM3LjY3MDcgMzAuNzk5NSAzNy4yNTgxIDMwLjc5OTVDMzYuNzQyNCAzMC43OTk1IDM2LjI5NTMgMzAuOTAxIDM1Ljg4MjIgMzEuMDdDMzUuNDY5NiAzMS4yNzI2IDM1LjEyNTUgMzEuNTA4NiAzNC44MTU1IDMxLjg0NTdDMzQuNTA1NCAzMi4xODM4IDM0LjI5ODggMzIuNTU0NCAzNC4xMjczIDMyLjk5M0MzMy45NTUyIDMzLjQzMTYgMzMuODg2MiAzMy45MDUyIDMzLjg4NjIgMzQuNDEwOUMzMy44ODYyIDM0Ljg4MiAzMy45NTUyIDM1LjMyMTEgMzQuMTI3MyAzNS43MjYyQzM0LjI2NTMgMzYuMTMxMyAzNC41MDU0IDM2LjUwMjkgMzQuNzgwOSAzNi44NEMzNS4wNTY1IDM3LjE0NCAzNS40MzQ2IDM3LjQxNDEgMzUuODQ3NyAzNy41ODMyQzM2LjI2MDggMzcuNzUyMiAzNi43NDI0IDM3Ljg1MjcgMzcuMjU4MSAzNy44NTI3QzM3Ljc3NDcgMzcuODUyNyAzOC4yMjE4IDM3Ljc1MTcgMzguNjM0OSAzNy41NDkxQzM5LjAxMyAzNy4zNDY2IDM5LjM1NjYgMzcuMDQyIDM5LjYzMjIgMzYuNjcxOUw0MC42NjM5IDM3LjQ0NzZDNDAuNTk0OSAzNy41NDkxIDQwLjQ5MTkgMzcuNjgzNyA0MC4zMjAzIDM3Ljg1MjdDNDAuMTQ3MyAzOC4wMjE4IDM5Ljk0MTIgMzguMTkwOCAzOS42MzIyIDM4LjM1ODhDMzkuMzU2NiAzOC41Mjc5IDM5LjAxMyAzOC42OTY5IDM4LjYzNDkgMzguNzk3NUMzOC4yNTU4IDM4Ljg5OSAzNy43NzQ3IDM5IDM3LjI1ODEgMzlDMzYuNTM2NCAzOSAzNS45MTY3IDM4Ljg2NTUgMzUuMzMxNiAzOC41OTQ5QzM0Ljc0NjkgMzguMzI1MyAzNC4yNjU4IDM3Ljk4ODMgMzMuODUyMiAzNy41NDk2QzMzLjQ0MDYgMzcuMTExIDMzLjEzMDUgMzYuNjM3NCAzMi45MjQgMzYuMDY0OEMzMi43MTc0IDM1LjUyNDEgMzIuNjEzOSAzNC45NTEgMzIuNjEzOSAzNC40MTE5QzMyLjYxMzkgMzMuNzM3MiAzMi43MTc0IDMzLjA5NTUgMzIuOTU4NSAzMi41MjE0QzMzLjE5OTYgMzEuOTQ4MiAzMy41MDg2IDMxLjQ0MTYgMzMuOTIxMiAzMS4wMzY1QzM0LjMzMzggMzAuNjMyNCAzNC44MTYgMzAuMjk0MyAzNS40MDA2IDMwLjA1ODNDMzUuOTg2MiAyOS44MjIyIDM2LjYwNTQgMjkuNzIwMiAzNy4zMjcxIDI5LjcyMDJDMzcuOTExNyAyOS43MjAyIDM4LjQ5NjQgMjkuODIxNyAzOS4wODIgMzAuMDU4M0MzOS42NjY3IDMwLjI5NDMgNDAuMTEzMyAzMC42MzI0IDQwLjQ5MjQgMzEuMTA0NUwzOS40MjU2IDMxLjgxMjJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDMuNTE5MSAyOS44ODgySDQ2LjY4MzRDNDcuMjY5MSAyOS44ODgyIDQ3Ljc1MDIgMjkuOTU1MiA0OC4xMjkzIDMwLjEyNDNDNDguNTA3NCAzMC4yOTMzIDQ4Ljc4MjkgMzAuNDYyNCA0OS4wMjMgMzAuNjk4NEM0OS4yMjk2IDMwLjkzNDUgNDkuNDAyMSAzMS4yMDQ1IDQ5LjQ3MDEgMzEuNTA4NkM0OS41MzkxIDMxLjgxMjIgNDkuNjA3MiAzMi4wODE4IDQ5LjYwNzIgMzIuMzUxM0M0OS42MDcyIDMyLjYyMTkgNDkuNTczNiAzMi45MjU1IDQ5LjQ3MDEgMzMuMTYxNUM0OS4zNjc2IDMzLjQzMTEgNDkuMjI5MSAzMy42NjgyIDQ5LjAyMyAzMy44NzAyQzQ4LjgxNjUgMzQuMDcyOCA0OC42MTA0IDM0LjI3NTMgNDguMzM1OCAzNC40MTA4QzQ4LjA2MDMgMzQuNTQ2NCA0Ny43NTAyIDM0LjY0NTkgNDcuNDA2NiAzNC42ODA0TDQ5Ljk4NzIgMzguNzI5NEg0OC40NzM0TDQ2LjEwMDMgMzQuODQ4SDQ0LjcyMzRWMzguNzYyOUg0My40ODUxVjI5Ljg4ODJINDMuNTE5MVpNNDQuNzIzNCAzMy43Njk3SDQ2LjMzOThDNDYuNTgwOSAzMy43Njk3IDQ2LjgyMiAzMy43MzYyIDQ3LjA2MjUgMzMuNzAyN0M0Ny4zMDM2IDMzLjY2OTIgNDcuNTA5NiAzMy42MDExIDQ3LjY4MTcgMzMuNTAwMUM0Ny44NTM3IDMzLjM5ODYgNDguMDI1MyAzMy4yNjQxIDQ4LjEyODggMzMuMDYxNUM0OC4yMzEzIDMyLjg1OSA0OC4zMDA4IDMyLjYyMjkgNDguMzAwOCAzMi4zMTgzQzQ4LjMwMDggMzIuMDE0NyA0OC4yMzE4IDMxLjc3ODcgNDguMTI4OCAzMS41NzYxQzQ4LjAyNTMgMzEuMzczNiA0Ny44ODc3IDMxLjIzODEgNDcuNjgxNyAzMS4xMzc1QzQ3LjUwOTYgMzEuMDM2IDQ3LjMwMzYgMzAuOTY4NSA0Ny4wNjI1IDMwLjkzNUM0Ni44MjE1IDMwLjkwMTUgNDYuNTgwNCAzMC44NjggNDYuMzM5OCAzMC44NjhINDQuNzIzNFYzMy43Njk3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTU2LjgzMyAzOC45NjZDNTYuMTQ0OCAzOC45NjYgNTUuNTI1NiAzOC44MzE0IDU0Ljk0MSAzOC41OTQ0QzU0LjM1NjMgMzguMzU4MyA1My44NzQyIDM4LjAyMTIgNTMuNDYxNiAzNy42MTYxQzUzLjA0OSAzNy4yMTEgNTIuNzM4OSAzNi43MDQ5IDUyLjQ5ODkgMzYuMTMxM0M1Mi4yNTc4IDM1LjU1NzEgNTIuMTU0MyAzNC45NTA1IDUyLjE1NDMgMzQuMjc1OEM1Mi4xNTQzIDMzLjYwMTEgNTIuMjU3OCAzMi45OTM1IDUyLjQ5ODkgMzIuNDIwM0M1Mi43Mzg5IDMxLjg0NjIgNTMuMDQ5IDMxLjM3NDEgNTMuNDYxNiAzMC45MzU1QzUzLjg3NDIgMzAuNTMwNCA1NC4zNTYzIDMwLjE5MzMgNTQuOTQxIDI5Ljk1NjJDNTUuNTI1NiAyOS43MjAyIDU2LjE0NDggMjkuNTg1NiA1Ni44MzMgMjkuNTg1NkM1Ny41MjAxIDI5LjU4NTYgNTguMTQwMyAyOS43MjAyIDU4LjcyNDkgMjkuOTU2MkM1OS4zMDk2IDMwLjE5MzMgNTkuNzkxNyAzMC41MzA0IDYwLjIwNDMgMzAuOTM1NUM2MC42MTY5IDMxLjM0MDYgNjAuOTI2IDMxLjg0NjcgNjEuMTY3IDMyLjQyMDNDNjEuNDA4MSAzMi45OTM1IDYxLjUxMDYgMzMuNjAxMSA2MS41MTA2IDM0LjI3NThDNjEuNTEwNiAzNC45NTA1IDYxLjQwODEgMzUuNTU3NiA2MS4xNjcgMzYuMTMxM0M2MC45MjYgMzYuNzA0OSA2MC42MTY5IDM3LjE3NzUgNjAuMjA0MyAzNy42MTYxQzU5Ljc5MTcgMzguMDIxMiA1OS4zMDk2IDM4LjM1ODMgNTguNzI0OSAzOC41OTQ0QzU4LjE0MDMgMzguODMxNCA1Ny41MjAxIDM4Ljk2NiA1Ni44MzMgMzguOTY2Wk01Ni44MzMgMzcuODUyMkM1Ny4zNDk2IDM3Ljg1MjIgNTcuODMwMiAzNy43NTEyIDU4LjI0MzMgMzcuNTgyNkM1OC42NTU5IDM3LjM4MDEgNTkgMzcuMTQ0IDU5LjMxMDEgMzYuODM5NEM1OS42MTkyIDM2LjUzNTkgNTkuODI2NyAzNi4xNjQ4IDU5Ljk5ODMgMzUuNzI1N0M2MC4xNzAzIDM1LjI4NzEgNjAuMjM4MyAzNC44NDg5IDYwLjIzODMgMzQuMzQzM0M2MC4yMzgzIDMzLjg3MTIgNjAuMTcwMyAzMy4zOTc2IDU5Ljk5ODMgMzIuOTU5QzU5LjgyNjIgMzIuNTIwNCA1OS42MTkyIDMyLjE0OTggNTkuMzEwMSAzMS44NDUyQzU5IDMxLjU0MTYgNTguNjU2NCAzMS4yNzIxIDU4LjI0MzMgMzEuMTAzQzU3LjgzMDcgMzAuOTAwNSA1Ny4zNDk2IDMwLjgzMjQgNTYuODMzIDMwLjgzMjRDNTYuMzE2MyAzMC44MzI0IDU1LjgzNDcgMzAuOTM0IDU1LjQyMjYgMzEuMTAzQzU1LjAxIDMxLjMwNTYgNTQuNjY1NCAzMS41NDE2IDU0LjM1NTggMzEuODQ1MkM1NC4wNDU4IDMyLjE0OTMgNTMuODM5MiAzMi41MTk5IDUzLjY2NzcgMzIuOTU5QzUzLjQ5NTYgMzMuMzk3NiA1My40MjY2IDMzLjgzNjIgNTMuNDI2NiAzNC4zNDMzQzUzLjQyNjYgMzQuODE0NCA1My40OTU2IDM1LjI4NzEgNTMuNjY3NyAzNS43MjU3QzUzLjgzOTcgMzYuMTY0MyA1NC4wNDU4IDM2LjUzNTkgNTQuMzU1OCAzNi44Mzk0QzU0LjY2NDkgMzcuMTQzNSA1NS4wMDk1IDM3LjQxMzYgNTUuNDIyNiAzNy41ODI2QzU1LjgzNDIgMzcuNzUxNyA1Ni4zMTYzIDM3Ljg1MjIgNTYuODMzIDM3Ljg1MjJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNjQuNTM4OSAyOS44ODgySDY3LjY2OTdDNjguMDQ3OCAyOS44ODgyIDY4LjQyNjQgMjkuOTIxNyA2OC43NyAzMC4wMjM3QzY5LjExNDUgMzAuMTI0OCA2OS4zOTAxIDMwLjI1OTggNjkuNjMxMiAzMC40Mjc4QzY5Ljg3MTIgMzAuNTk2OSA3MC4wNzgzIDMwLjgzMjkgNzAuMjE0OCAzMS4xMDM1QzcwLjM1MjkgMzEuMzczMSA3MC40MjI0IDMxLjcxMDIgNzAuNDIyNCAzMi4wODE4QzcwLjQyMjQgMzIuNTg3OSA3MC4yODQzIDMyLjk5MyA2OS45NzUzIDMzLjMzMDZDNjkuNjk5NyAzMy42Njc3IDY5LjMyMTYgMzMuODcwMiA2OC44NDA1IDM0LjAzOTNWMzQuMDcyOEM2OS4xMTUgMzQuMDcyOCA2OS4zNTYxIDM0LjE3NDMgNjkuNTk3MiAzNC4yNzUzQzY5LjgzODIgMzQuNDEwOCA3MC4wNDM4IDM0LjU0NDkgNzAuMjE1MyAzNC43NDY0QzcwLjM4ODQgMzQuOTQ5IDcwLjUyNTQgMzUuMTg1IDcwLjYyODkgMzUuNDIyMUM3MC43MzE0IDM1LjY5MTcgNzAuNzY2IDM1Ljk2MTcgNzAuNzY2IDM2LjI2NDhDNzAuNzY2IDM2LjY2OTkgNzAuNjk2OSAzNy4wMDggNzAuNTI0OSAzNy4zMTExQzcwLjM1MjkgMzcuNjE1MSA3MC4xNDY4IDM3Ljg4NTIgNjkuODM3NyAzOC4wODc4QzY5LjU2MjIgMzguMjkwMyA2OS4yMTc2IDM4LjQ1OTQgNjguODQwNSAzOC41NTk5QzY4LjQ2MTQgMzguNjYxNCA2OC4wNDgzIDM4LjcyODkgNjcuNjAyMiAzOC43Mjg5SDY0LjUwNTRWMjkuODg4Mkg2NC41Mzg5Wk02NS43NDI3IDMzLjU2NzFINjcuNDI4NkM2Ny42Njk3IDMzLjU2NzEgNjcuODc1NyAzMy41MzM2IDY4LjA4MjMgMzMuNTAwMUM2OC4yODc4IDMzLjQ2NTYgNjguNDYwNCAzMy4zNjQ2IDY4LjYzMjQgMzMuMjY0MUM2OC43Njk1IDMzLjE2MjUgNjguOTA4IDMzLjAyNyA2OS4wMTA1IDMyLjg1OUM2OS4xMTQgMzIuNjg5OSA2OS4xNDg2IDMyLjQ4NzQgNjkuMTQ4NiAzMi4yNTEzQzY5LjE0ODYgMzEuOTEzMiA2OS4wNDUgMzEuNjEwMSA2OC44MDQgMzEuMzQwMUM2OC41NjI5IDMxLjA2OTUgNjguMjE5MyAzMC45Njg1IDY3LjcwMzcgMzAuOTY4NUg2NS43NzcyVjMzLjU2NzFINjUuNzQyN1pNNjUuNzQyNyAzNy42ODM3SDY3LjUzMTJDNjcuNzA0MiAzNy42ODM3IDY3LjkxMDIgMzcuNjUwMiA2OC4xNTEzIDM3LjYxNjZDNjguMzkyNCAzNy41ODMxIDY4LjU5ODQgMzcuNTE1MSA2OC43Njk1IDM3LjM4MDZDNjguOTc2IDM3LjI3OTEgNjkuMTQ4NiAzNy4xMSA2OS4yNTE2IDM2LjkwNzVDNjkuMzg5NiAzNi43MDQ5IDY5LjQ1ODEgMzYuNDY4OSA2OS40NTgxIDM2LjEzMThDNjkuNDU4MSAzNS41OTExIDY5LjI4NjEgMzUuMjIwNSA2OC45NDI1IDM0Ljk1QzY4LjU5ODkgMzQuNjgxNCA2OC4xMTY4IDM0LjU0NTkgNjcuNTMxMiAzNC41NDU5SDY1LjcwODJWMzcuNjg0Mkg2NS43NDI3VjM3LjY4MzdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzQuMTAyMyAyOS44ODgySDc1LjM0MDZWMzcuNjE2MUg3OS40Njk2VjM4LjcyOTlINzQuMTAyM1YyOS44ODgyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTgzLjQ5NDYgMjkuODg4N0g4Mi4yNTYzVjM4Ljc2NEg4My40OTQ2VjI5Ljg4ODdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNODcuMzEyNiAyOS44ODgySDg4LjkzTDkzLjkxODMgMzcuMTc3NUg5My45NTE4VjI5Ljg4ODJIOTUuMTkwMVYzOC43NjM0SDkzLjY0MjdMODguNjE5OSAzMS40NzQxSDg4LjU4NTRWMzguNzYzNEg4Ny4zNDcxVjI5Ljg4ODJIODcuMzEyNloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik05OC44NzIgMjkuODg4MkgxMDAuMTFWMzMuNzAyMkgxMDAuMjEzTDEwNC4yMDUgMjkuODg4MkgxMDUuOTI0TDEwMS41NTUgMzMuOTcyMkwxMDYuMTk5IDM4LjcyOTlIMTA0LjQxMUwxMDAuMjEzIDM0LjMwOTNIMTAwLjExVjM4LjcyOTlIOTguODcyVjI5Ljg4ODJaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMzYuOTQzIDE1LjQyNTFIMzguODc2NUMzOS4yMTY2IDE1LjQyNTEgMzkuNTI3MSAxNS40NjA2IDM5LjgwNzcgMTUuNTMxNkM0MC4wODc4IDE1LjU5NzIgNDAuMzI2OCAxNS42OTk3IDQwLjUyMzkgMTUuODQxN0M0MC43MjA0IDE1Ljk4MjcgNDAuODcyNSAxNi4xNjMzIDQwLjk4MSAxNi4zODIzQzQxLjA4ODUgMTYuNjAwNCA0MS4xNDE1IDE2Ljg2MzUgNDEuMTQxNSAxNy4xNzE1QzQxLjE0MTUgMTcuNDg0NiA0MS4wODIgMTcuNzUzMiA0MC45NjMgMTcuOTc3N0M0MC44NDk1IDE4LjIwMTMgNDAuNjg4NCAxOC4zODQ4IDQwLjQ3OTQgMTguNTI2OUM0MC4yNzY4IDE4LjY2NzkgNDAuMDMyMyAxOC43NzQ0IDM5Ljc0NTcgMTguODQ1NUMzOS40NjU2IDE4LjkxMSAzOS4xNiAxOC45NDI1IDM4LjgzMiAxOC45NDI1SDM3LjgyOTdWMjEuNjk4N0gzNi45NDM1VjE1LjQyNTFIMzYuOTQzWk0zNy44Mjg3IDE4LjE5ODhIMzguNzY4NEMzOC45ODM1IDE4LjE5ODggMzkuMTgwMSAxOC4xODEzIDM5LjM1OTYgMTguMTQ1OEMzOS41NDQ2IDE4LjEwNDMgMzkuNzAyMiAxOC4wNDQ4IDM5LjgzMzcgMTcuOTY4MkMzOS45NjUyIDE3Ljg4NTcgNDAuMDY2OCAxNy43NzkyIDQwLjEzNzggMTcuNjQ5N0M0MC4yMDg4IDE3LjUxOTEgNDAuMjQ1MyAxNy4zNTk2IDQwLjI0NTMgMTcuMTcxNUM0MC4yNDUzIDE2Ljk4MTUgNDAuMjA2OCAxNi44MjUgNDAuMTI4MyAxNi43MDA5QzQwLjA1NzMgMTYuNTcxNCAzOS45NTUyIDE2LjQ2NzkgMzkuODI0NyAxNi4zOTA5QzM5LjY5OTIgMTYuMzA4MyAzOS41NDcxIDE2LjI1MjggMzkuMzY3NiAxNi4yMjI4QzM5LjE4ODEgMTYuMTg3MyAzOC45OTQgMTYuMTY5MyAzOC43ODYgMTYuMTY5M0gzNy44Mjc3VjE4LjE5ODhIMzcuODI4N1oiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik00NS45MzMyIDE5LjU5MDFDNDUuOTMzMiAxOS45MTU3IDQ1Ljg3MzcgMjAuMjEzMyA0NS43NTQ3IDIwLjQ4NTlDNDUuNjQwNyAyMC43NTY0IDQ1LjQ3OTEgMjAuOTkzNSA0NS4yNzExIDIxLjE5NDVDNDUuMDY3NSAyMS4zODk2IDQ0LjgyNjUgMjEuNTQzMSA0NC41NDU0IDIxLjY1NTJDNDQuMjY1MyAyMS43NjE3IDQzLjk2MzcgMjEuODE0NyA0My42NDEyIDIxLjgxNDdDNDMuMzE5NiAyMS44MTQ3IDQzLjAxOCAyMS43NjE3IDQyLjczNjkgMjEuNjU1MkM0Mi40NTY5IDIxLjU0MjYgNDIuMjE1OCAyMS4zODkxIDQyLjAxMzMgMjEuMTk0NUM0MS44MDk3IDIwLjk5NCA0MS42NDg3IDIwLjc1NjkgNDEuNTI5NiAyMC40ODU5QzQxLjQxNjEgMjAuMjEzMyA0MS4zNTg2IDE5LjkxNTcgNDEuMzU4NiAxOS41OTAxQzQxLjM1ODYgMTkuMjY1NiA0MS40MTYxIDE4Ljk3IDQxLjUyOTYgMTguNzAzOUM0MS42NDg3IDE4LjQzMjQgNDEuODA5NyAxOC4xOTg4IDQyLjAxMzMgMTguMDA0MkM0Mi4yMTU4IDE3LjgwOTIgNDIuNDU2OSAxNy42NTg3IDQyLjczNjkgMTcuNTUyMUM0My4wMTggMTcuNDM5NiA0My4zMTk2IDE3LjM4NDEgNDMuNjQxMiAxNy4zODQxQzQzLjk2MzcgMTcuMzg0MSA0NC4yNjUzIDE3LjQzOTYgNDQuNTQ1NCAxNy41NTIxQzQ0LjgyNjUgMTcuNjU4NyA0NS4wNjc1IDE3LjgwOTIgNDUuMjcxMSAxOC4wMDQyQzQ1LjQ3OTYgMTguMTk5MyA0NS42NDA3IDE4LjQzMjQgNDUuNzU0NyAxOC43MDM5QzQ1Ljg3MzcgMTguOTcgNDUuOTMzMiAxOS4yNjU2IDQ1LjkzMzIgMTkuNTkwMVpNNDUuMDU2IDE5LjU5MDFDNDUuMDU2IDE5LjM4OTYgNDUuMDIzNSAxOS4xOTc1IDQ0Ljk1NyAxOS4wMTRDNDQuODk3NSAxOC44MzA1IDQ0LjgwOCAxOC42NzE0IDQ0LjY4ODkgMTguNTM1OUM0NC41Njg5IDE4LjM5MzggNDQuNDIwNCAxOC4yODEzIDQ0LjI0MDggMTguMTk4OEM0NC4wNjc4IDE4LjExNjMgNDMuODY4MiAxOC4wNzQ4IDQzLjY0MDcgMTguMDc0OEM0My40MTUxIDE4LjA3NDggNDMuMjExNiAxOC4xMTYzIDQzLjAzMyAxOC4xOTg4QzQyLjg1OTUgMTguMjgxMyA0Mi43MTM0IDE4LjM5MzggNDIuNTk0NCAxOC41MzU5QzQyLjQ3NDQgMTguNjcxNCA0Mi4zODI0IDE4LjgzMDUgNDIuMzE1OCAxOS4wMTRDNDIuMjU2MyAxOS4xOTc1IDQyLjIyNjMgMTkuMzg5NiA0Mi4yMjYzIDE5LjU5MDFDNDIuMjI2MyAxOS43OTA3IDQyLjI1NjMgMTkuOTgyNyA0Mi4zMTU4IDIwLjE2NjNDNDIuMzgxOSAyMC4zNDk4IDQyLjQ3NDQgMjAuNTExOSA0Mi41OTQ0IDIwLjY1MzlDNDIuNzEzNCAyMC43OTQ5IDQyLjg1OTUgMjAuOTA4NSA0My4wMzMgMjAuOTlDNDMuMjExNiAyMS4wNzM1IDQzLjQxNTEgMjEuMTE1IDQzLjY0MDcgMjEuMTE1QzQzLjg2ODIgMjEuMTE1IDQ0LjA2NzggMjEuMDczNSA0NC4yNDA4IDIwLjk5QzQ0LjQyMDQgMjAuOTA4NSA0NC41Njg5IDIwLjc5NDkgNDQuNjg4OSAyMC42NTM5QzQ0LjgwOCAyMC41MTE5IDQ0Ljg5NzUgMjAuMzQ5OCA0NC45NTcgMjAuMTY2M0M0NS4wMjM1IDE5Ljk4MjcgNDUuMDU2IDE5Ljc5MDcgNDUuMDU2IDE5LjU5MDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNDcuNDEzNiAxNy40OTg2TDQ4LjM3MTQgMjAuNzA2NEg0OC4zODk5TDQ5LjQwMDYgMTcuNDk4Nkg1MC4yODc4TDUxLjMxNjYgMjAuNzA2NEg1MS4zMzQxTDUyLjI5MjggMTcuNDk4Nkg1My4xNzhMNTEuNzczNyAyMS42OTg3SDUwLjg5NTVMNDkuODQ4NyAxOC41NDQ0SDQ5LjgzMDdMNDguNzkyIDIxLjY5ODdINDcuOTE0N0w0Ni41MDA0IDE3LjQ5ODZINDcuNDEzNloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik01Ny4xMyAxOS4yMjY2QzU3LjEyNCAxOS4wNjE1IDU3LjA5NDUgMTguOTA4IDU3LjA0MDUgMTguNzY1OUM1Ni45OTI1IDE4LjYxNzkgNTYuOTE4NSAxOC40OTE0IDU2LjgxNyAxOC4zODQ4QzU2LjcyMDkgMTguMjc5MyA1Ni41OTg5IDE4LjE5NTggNTYuNDUwNCAxOC4xMzczQzU2LjMwNjMgMTguMDcxOCA1Ni4xMzYzIDE4LjAzOTMgNTUuOTM5NyAxOC4wMzkzQzU1Ljc2MDIgMTguMDM5MyA1NS41OTAyIDE4LjA3MTggNTUuNDI5MSAxOC4xMzczQzU1LjI3NDYgMTguMTk1OCA1NS4xMzcgMTguMjc5MyA1NS4wMTggMTguMzg0OEM1NC45MDQgMTguNDkxNCA1NC44MDkgMTguNjE3OSA1NC43MzA5IDE4Ljc2NTlDNTQuNjU5OSAxOC45MDggNTQuNjE3NCAxOS4wNjE1IDU0LjYwNTQgMTkuMjI2Nkg1Ny4xM1pNNTcuOTgwOCAxOS41NjM2VjE5LjcwNDdDNTcuOTgwOCAxOS43NTI3IDU3Ljk3NzggMTkuNzk5NyA1Ny45NzIzIDE5Ljg0NjdINTQuNjA1NEM1NC42MTE5IDIwLjAyNDMgNTQuNjUwNCAyMC4xOTIzIDU0LjcyMjQgMjAuMzUxOEM1NC44IDIwLjUwNTQgNTQuOTAxIDIwLjY0MTkgNTUuMDI2NSAyMC43NTk5QzU1LjE1MTYgMjAuODcyNSA1NS4yOTUxIDIwLjk2MDUgNTUuNDU2NiAyMS4wMjZDNTUuNjIyNyAyMS4wOTE1IDU1Ljc5OTIgMjEuMTI0IDU1Ljk4MzggMjEuMTI0QzU2LjI3MDggMjEuMTI0IDU2LjUxODQgMjEuMDYxNSA1Ni43MjY5IDIwLjkzOEM1Ni45MzYgMjAuODEzIDU3LjEwMDUgMjAuNjYzNCA1Ny4yMjA2IDIwLjQ4NTlMNTcuODEwNyAyMC45NTU1QzU3LjU4MzIgMjEuMjUxMSA1Ny4zMTU2IDIxLjQ2OTEgNTcuMDA0NSAyMS42MTEyQzU2LjcwMDkgMjEuNzQ2NyA1Ni4zNjA0IDIxLjgxNDcgNTUuOTg0MyAyMS44MTQ3QzU1LjY2MjcgMjEuODE0NyA1NS4zNjQxIDIxLjc2MTcgNTUuMDg5NSAyMS42NTUyQzU0LjgxNSAyMS41NDg2IDU0LjU3OTkgMjEuNDAxNiA1NC4zODE5IDIxLjIxMjZDNTQuMTg1MyAyMS4wMTc1IDU0LjAzMDMgMjAuNzg0NCA1My45MTcyIDIwLjUxMTlDNTMuODAzMiAyMC4yNDEzIDUzLjc0NjIgMTkuOTM5NyA1My43NDYyIDE5LjYwODdDNTMuNzQ2MiAxOS4yODQxIDUzLjc5OTcgMTguOTg1NSA1My45MDcyIDE4LjcxMzlDNTQuMDIwOCAxOC40MzY0IDU0LjE3NTggMTguMTk5MyA1NC4zNzI5IDE4LjAwNDJDNTQuNTY5NCAxNy44MDkyIDU0LjgwMiAxNy42NTg3IDU1LjA3MDUgMTcuNTUyMUM1NS4zMzkxIDE3LjQ0MDYgNTUuNjI5MiAxNy4zODQxIDU1LjkzOTIgMTcuMzg0MUM1Ni4yNDkzIDE3LjM4NDEgNTYuNTI5NCAxNy40MzQxIDU2Ljc4IDE3LjUzNDZDNTcuMDM3IDE3LjYzNTcgNTcuMjUyMSAxNy43ODAyIDU3LjQyNTEgMTcuOTY4N0M1Ny42MDM3IDE4LjE1NzggNTcuNzQxMiAxOC4zODgzIDU3LjgzNjcgMTguNjU5OUM1Ny45MzI3IDE4LjkyNTUgNTcuOTgwOCAxOS4yMjY2IDU3Ljk4MDggMTkuNTYzNloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik01OS4xNTYxIDE4LjQwMjRDNTkuMTU2MSAxOC4yODk4IDU5LjE1MzEgMTguMTQ1MyA1OS4xNDc2IDE3Ljk2ODJDNTkuMTQxMSAxNy43OTA3IDU5LjEzMjEgMTcuNjM0MiA1OS4xMTk2IDE3LjQ5ODZINTkuOTE2MkM1OS45Mjg3IDE3LjYwNDIgNTkuOTM3MyAxNy43MjkyIDU5Ljk0MzMgMTcuODcwMkM1OS45NDk4IDE4LjAwNjggNTkuOTUyOCAxOC4xMTg4IDU5Ljk1MjggMTguMjA3M0g1OS45Nzk4QzYwLjA5ODggMTcuOTU5NyA2MC4yNzE4IDE3Ljc2MTcgNjAuNDk4NCAxNy42MTQyQzYwLjczMTQgMTcuNDYwNiA2MC45OTIgMTcuMzgyNiA2MS4yNzgxIDE3LjM4MjZDNjEuNDA5NiAxNy4zODI2IDYxLjUyMDEgMTcuMzk1MSA2MS42MDkyIDE3LjQxOTFMNjEuNTczNyAxOC4xODk4QzYxLjQ1NDYgMTguMTU5OCA2MS4zMjYxIDE4LjE0NDggNjEuMTg3NiAxOC4xNDQ4QzYwLjk4NSAxOC4xNDQ4IDYwLjgwOTUgMTguMTgzMyA2MC42NTk0IDE4LjI2MDhDNjAuNTEwNCAxOC4zMzA4IDYwLjM4NDkgMTguNDI4OSA2MC4yODM4IDE4LjU1MjlDNjAuMTg3OCAxOC42NzY5IDYwLjExNTggMTguODIxNSA2MC4wNjg4IDE4Ljk4N0M2MC4wMjA4IDE5LjE0NjUgNTkuOTk2OCAxOS4zMTUxIDU5Ljk5NjggMTkuNDkyMVYyMS42OTkySDU5LjE1NjFWMTguNDAyNFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02NS4zNDc2IDE5LjIyNjZDNjUuMzQxNiAxOS4wNjE1IDY1LjMxMjEgMTguOTA4IDY1LjI1ODEgMTguNzY1OUM2NS4yMTEgMTguNjE3OSA2NS4xMzYgMTguNDkxNCA2NS4wMzQ1IDE4LjM4NDhDNjQuOTM4NSAxOC4yNzkzIDY0LjgxNzUgMTguMTk1OCA2NC42NjY5IDE4LjEzNzNDNjQuNTI0OSAxOC4wNzE4IDY0LjM1MzggMTguMDM5MyA2NC4xNTczIDE4LjAzOTNDNjMuOTc3NyAxOC4wMzkzIDYzLjgwODcgMTguMDcxOCA2My42NDc3IDE4LjEzNzNDNjMuNDkyMSAxOC4xOTU4IDYzLjM1NTEgMTguMjc5MyA2My4yMzUxIDE4LjM4NDhDNjMuMTIyNSAxOC40OTE0IDYzLjAyNjUgMTguNjE3OSA2Mi45NDkgMTguNzY1OUM2Mi44NzggMTguOTA4IDYyLjgzNTUgMTkuMDYxNSA2Mi44MjM1IDE5LjIyNjZINjUuMzQ3NlpNNjYuMTk4MyAxOS41NjM2VjE5LjcwNDdDNjYuMTk4MyAxOS43NTI3IDY2LjE5NTMgMTkuNzk5NyA2Ni4xODk4IDE5Ljg0NjdINjIuODIzQzYyLjgyOTUgMjAuMDI0MyA2Mi44NjggMjAuMTkyMyA2Mi45MzkgMjAuMzUxOEM2My4wMTc1IDIwLjUwNTQgNjMuMTE4NSAyMC42NDE5IDYzLjI0NDEgMjAuNzU5OUM2My4zNjk2IDIwLjg3MjUgNjMuNTEyNiAyMC45NjA1IDYzLjY3MzIgMjEuMDI2QzYzLjg0MTIgMjEuMDkxNSA2NC4wMTY4IDIxLjEyNCA2NC4yMDIzIDIxLjEyNEM2NC40ODg0IDIxLjEyNCA2NC43MzY5IDIxLjA2MTUgNjQuOTQ0NSAyMC45MzhDNjUuMTUzNSAyMC44MTMgNjUuMzE4MSAyMC42NjM0IDY1LjQzODEgMjAuNDg1OUw2Ni4wMjgyIDIwLjk1NTVDNjUuODAxNyAyMS4yNTExIDY1LjUzMzEgMjEuNDY5MSA2NS4yMjIxIDIxLjYxMTJDNjQuOTE4NSAyMS43NDY3IDY0LjU3ODkgMjEuODE0NyA2NC4yMDIzIDIxLjgxNDdDNjMuODc5NyAyMS44MTQ3IDYzLjU4MTEgMjEuNzYxNyA2My4zMDY2IDIxLjY1NTJDNjMuMDMyIDIxLjU0ODYgNjIuNzk3IDIxLjQwMTYgNjIuNTk5OSAyMS4yMTI2QzYyLjQwMjQgMjEuMDE3NSA2Mi4yNDc4IDIwLjc4NDQgNjIuMTM0MyAyMC41MTE5QzYyLjAyMDMgMjAuMjQxMyA2MS45NjQyIDE5LjkzOTcgNjEuOTY0MiAxOS42MDg3QzYxLjk2NDIgMTkuMjg0MSA2Mi4wMTc4IDE4Ljk4NTUgNjIuMTI0OCAxOC43MTM5QzYyLjIzODMgMTguNDM2NCA2Mi4zOTM0IDE4LjE5OTMgNjIuNTkwNCAxOC4wMDQyQzYyLjc4NyAxNy44MDkyIDYzLjAxOTUgMTcuNjU4NyA2My4yODkxIDE3LjU1MjFDNjMuNTU3MSAxNy40NDA2IDYzLjg0NjcgMTcuMzg0MSA2NC4xNTY4IDE3LjM4NDFDNjQuNDY2OSAxNy4zODQxIDY0Ljc0NjkgMTcuNDM0MSA2NC45OTg1IDE3LjUzNDZDNjUuMjU1MSAxNy42MzU3IDY1LjQ2OTYgMTcuNzgwMiA2NS42NDI3IDE3Ljk2ODdDNjUuODIxMiAxOC4xNTc4IDY1Ljk1OTIgMTguMzg4MyA2Ni4wNTQzIDE4LjY1OTlDNjYuMTUwMyAxOC45MjU1IDY2LjE5ODMgMTkuMjI2NiA2Ni4xOTgzIDE5LjU2MzZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzAuNzU3NCAyMS4wMzQ1QzcwLjU5NjQgMjEuMjg5MSA3MC4zNzUzIDIxLjQ4MzYgNzAuMDk1MyAyMS42MTkyQzY5LjgyMTcgMjEuNzQ5NyA2OS41MzE2IDIxLjgxNDIgNjkuMjI2NiAyMS44MTQyQzY4Ljg5OSAyMS44MTQyIDY4LjYwMzQgMjEuNzU4NyA2OC4zNDEzIDIxLjY0NjJDNjguMDc4MyAyMS41MjgxIDY3Ljg1NDcgMjEuMzY4NiA2Ny42NjkyIDIxLjE2N0M2Ny40ODQxIDIwLjk2NjUgNjcuMzQxMSAyMC43MzA0IDY3LjIzOTEgMjAuNDU4NEM2Ny4xMzggMjAuMTg2OCA2Ny4wODc1IDE5Ljg5NjcgNjcuMDg3NSAxOS41ODk2QzY3LjA4NzUgMTkuMjgyNiA2Ny4xMzg1IDE4Ljk5NjUgNjcuMjM5MSAxOC43MzA0QzY3LjM0MDYgMTguNDU4OSA2Ny40ODM2IDE4LjIyMjggNjcuNjY5MiAxOC4wMjA4QzY3Ljg2MDIgMTcuODIwMiA2OC4wODY4IDE3LjY2MzcgNjguMzQ4OCAxNy41NTExQzY4LjYxMTkgMTcuNDM4NiA2OC45MDIgMTcuMzgzMSA2OS4yMTg2IDE3LjM4MzFDNjkuNTY0MSAxNy4zODMxIDY5Ljg2NDcgMTcuNDU3MSA3MC4xMjE4IDE3LjYwNDdDNzAuMzc4OSAxNy43NDU3IDcwLjU4MTQgMTcuOTIwNyA3MC43MzA0IDE4LjEyNjhINzAuNzQ3OVYxNUg3MS41ODk3VjIxLjY5ODdINzAuNzc1NFYyMS4wMzQ1SDcwLjc1NzRaTTY3Ljk2NDcgMTkuNTkwMUM2Ny45NjQ3IDE5Ljc4NTIgNjcuOTk0MiAxOS45NzQyIDY4LjA1NDMgMjAuMTU3M0M2OC4xMTM4IDIwLjM0MDggNjguMjAzMyAyMC41MDI5IDY4LjMyMjggMjAuNjQ0OUM2OC40NDE5IDIwLjc4NTkgNjguNTg3OSAyMC45MDE1IDY4Ljc2MTQgMjAuOTg5NUM2OC45MzQ1IDIxLjA3MyA2OS4xMzc1IDIxLjExNDUgNjkuMzcwMSAyMS4xMTQ1QzY5LjU4NDIgMjEuMTE0NSA2OS43NzkyIDIxLjA3MyA2OS45NTE3IDIwLjk4OTVDNzAuMTMxMyAyMC45MDggNzAuMjgyOCAyMC43OTg0IDcwLjQwODkgMjAuNjYxOUM3MC41MzM5IDIwLjUxOTkgNzAuNjI4OSAyMC4zNTc4IDcwLjY5NDkgMjAuMTc0M0M3MC43NjU5IDE5Ljk5MDcgNzAuODAyNSAxOS43OTk3IDcwLjgwMjUgMTkuNTk4MUM3MC44MDI1IDE5LjM5NzYgNzAuNzY1OSAxOS4yMDU2IDcwLjY5NDkgMTkuMDIzQzcwLjYyODkgMTguODM5NSA3MC41MzM5IDE4LjY3NjQgNzAuNDA4OSAxOC41MzU0QzcwLjI4MzMgMTguMzkzMyA3MC4xMzEzIDE4LjI4MDggNjkuOTUxNyAxOC4xOTgzQzY5Ljc3ODcgMTguMTE1OCA2OS41ODQyIDE4LjA3NDMgNjkuMzcwMSAxOC4wNzQzQzY5LjEzNzUgMTguMDc0MyA2OC45MzQ1IDE4LjExNTggNjguNzYxNCAxOC4xOTgzQzY4LjU4NzkgMTguMjgwOCA2OC40NDE5IDE4LjM5MzMgNjguMzIyOCAxOC41MzU0QzY4LjIwMjggMTguNjc2NCA2OC4xMTM4IDE4LjgzOTUgNjguMDU0MyAxOS4wMjNDNjcuOTk0MiAxOS4yMDYxIDY3Ljk2NDcgMTkuMzk1MSA2Ny45NjQ3IDE5LjU5MDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNzYuMTI1OCAyMS4wMzQ1VjIxLjY5ODdINzUuMzExNlYxNUg3Ni4xNTMzVjE4LjEyNzhINzYuMTc5M0M3Ni4zMjMzIDE3LjkyMTIgNzYuNTIyOSAxNy43NDY3IDc2Ljc3OTQgMTcuNjA1N0M3Ny4wMzY1IDE3LjQ1NzYgNzcuMzQxMSAxNy4zODQxIDc3LjY5MjIgMTcuMzg0MUM3OC4wMDg4IDE3LjM4NDEgNzguMjk0OCAxNy40Mzk2IDc4LjU1MjQgMTcuNTUyMUM3OC44MTQ1IDE3LjY2NDcgNzkuMDM5IDE3LjgyMDcgNzkuMjIzNiAxOC4wMjE4Qzc5LjQxNDYgMTguMjIzMyA3OS41NjE2IDE4LjQ1OTQgNzkuNjYyMiAxOC43MzE0Qzc5Ljc2MzcgMTguOTk3NSA3OS44MTM3IDE5LjI4MzYgNzkuODEzNyAxOS41OTA2Qzc5LjgxMzcgMTkuODk3NyA3OS43NjM3IDIwLjE4NzggNzkuNjYyMiAyMC40NTk0Qzc5LjU2MTEgMjAuNzMwOSA3OS40MTc2IDIwLjk2NyA3OS4yMzMxIDIxLjE2OEM3OS4wNDcgMjEuMzY5NiA3OC44MjQgMjEuNTI5MSA3OC41NjA5IDIxLjY0NzJDNzguMjk3OCAyMS43NTk3IDc4LjAwMzMgMjEuODE1MiA3Ny42NzQ3IDIxLjgxNTJDNzcuMzY5NiAyMS44MTUyIDc3LjA4MTUgMjEuNzUwNyA3Ni44MDYgMjEuNjIwMkM3Ni41MzE0IDIxLjQ4NDYgNzYuMzEwOCAyMS4yOTAxIDc2LjE0MzggMjEuMDM1NUg3Ni4xMjU4VjIxLjAzNDVaTTc4Ljk0NjUgMTkuNTkwMUM3OC45NDY1IDE5LjM5NTEgNzguOTE2NSAxOS4yMDYxIDc4Ljg1NyAxOS4wMjM1Qzc4Ljc5NzQgMTguODQgNzguNzA3NCAxOC42NzY5IDc4LjU4ODQgMTguNTM1OUM3OC40Njk0IDE4LjM5MzggNzguMzIwMyAxOC4yODEzIDc4LjE0MTMgMTguMTk4OEM3Ny45Njc3IDE4LjExNjMgNzcuNzY1NyAxOC4wNzQ4IDc3LjUzMTYgMTguMDc0OEM3Ny4zMTc2IDE4LjA3NDggNzcuMTIwNSAxOC4xMTYzIDc2Ljk0MTUgMTguMTk4OEM3Ni43Njc5IDE4LjI4MTMgNzYuNjE4OSAxOC4zOTM4IDc2LjQ5NDQgMTguNTM1OUM3Ni4zNjg4IDE4LjY3NjkgNzYuMjcwOCAxOC44NCA3Ni4xOTg4IDE5LjAyMzVDNzYuMTMyOCAxOS4yMDYxIDc2LjA5OTggMTkuMzk4MSA3Ni4wOTk4IDE5LjU5ODZDNzYuMDk5OCAxOS44MDAyIDc2LjEzMjMgMTkuOTkxMiA3Ni4xOTg4IDIwLjE3NDhDNzYuMjcwOCAyMC4zNTgzIDc2LjM2ODggMjAuNTIwNCA3Ni40OTQ0IDIwLjY2MjRDNzYuNjE5NCAyMC43OTg5IDc2Ljc2NzkgMjAuOTA4IDc2Ljk0MTUgMjAuOTlDNzcuMTIxIDIxLjA3MzUgNzcuMzE3NiAyMS4xMTUgNzcuNTMxNiAyMS4xMTVDNzcuNzY1NyAyMS4xMTUgNzcuOTY3MiAyMS4wNzM1IDc4LjE0MTMgMjAuOTlDNzguMzIwOCAyMC45MDE1IDc4LjQ2OTQgMjAuNzg2NCA3OC41ODg0IDIwLjY0NTRDNzguNzA3NCAyMC41MDM0IDc4Ljc5NzQgMjAuMzQxMyA3OC44NTcgMjAuMTU3OEM3OC45MTY1IDE5Ljk3MzcgNzguOTQ2NSAxOS43ODQ3IDc4Ljk0NjUgMTkuNTkwMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik04Mi41OTY5IDIwLjc5NUg4Mi42MTQ5TDgzLjc4NjIgMTcuNDk4MUg4NC42ODE5TDgyLjYyMjkgMjIuNjkxOUM4Mi41NTE5IDIyLjg2ODUgODIuNDczOSAyMy4wMjUgODIuMzkwNCAyMy4xNjE2QzgyLjMwNjggMjMuMzAyNiA4Mi4yMDg4IDIzLjQyMDYgODIuMDk1OCAyMy41MTU2QzgxLjk4MTggMjMuNjE1NyA4MS44NDgyIDIzLjY5MzIgODEuNjkyNyAyMy43NDYyQzgxLjU0MzEgMjMuNzk5MiA4MS4zNjQ2IDIzLjgyNTcgODEuMTU1IDIzLjgyNTdDODEuMDY1NSAyMy44MjU3IDgwLjk3MzUgMjMuODE5NyA4MC44Nzc1IDIzLjgwODJDODAuNzg4IDIzLjgwMTcgODAuNjk1OSAyMy43ODcyIDgwLjU5OTkgMjMuNzYzMkw4MC42ODA0IDIzLjAyOEM4MC44MjM1IDIzLjA3NSA4MC45NjM1IDIzLjA5OSA4MS4xMDEgMjMuMDk5QzgxLjMyMTYgMjMuMDk5IDgxLjQ4NjEgMjMuMDM2NSA4MS41OTM3IDIyLjkxNEM4MS43MDEyIDIyLjc5NSA4MS44MDAyIDIyLjYyMzkgODEuODg5MiAyMi4zOTk0TDgyLjE1NzggMjEuNjk5N0w4MC4zMzkzIDE3LjQ5ODZIODEuMjgwMUw4Mi41OTY5IDIwLjc5NVoiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZGQiIHg9IjAiIHk9IjAiIHdpZHRoPSIxMjEuMTk5IiBoZWlnaHQ9IjU0IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIi8+CjxmZU9mZnNldC8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjIiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuNCAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIi8+CjxmZU9mZnNldC8+CjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjcuNSIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xNSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3ciIHJlc3VsdD0iZWZmZWN0Ml9kcm9wU2hhZG93Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0Ml9kcm9wU2hhZG93IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPg==); + background: no-repeat center url('data:image/png;base64,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'); + background-size: 100%; content: "\00a0"; } -:host(.no-overlay)::after { - display: none; -} +:host(.no-overlay)::after { display: none; } /** - * Barcode + * Wrapper */ -@mixin positionEdges($horizontal-padding, $vertical-padding) { - svg#top-right { - top: $vertical-padding; - right: $horizontal-padding; - } +:host { + #card-identity, + #barcode, + #blinkcard { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; - svg#bottom-right { - bottom: $vertical-padding; - right: $horizontal-padding; + &.visible { display: block; } } - svg#top-left { - top: $vertical-padding; - left: $horizontal-padding; - } + .message { + display: block; + opacity: 0; + visibility: hidden; + + position: absolute; + transform-origin: center; + transform: translate(-50%, 0); + margin: 0; + padding: 2 * $base-unit 3 * $base-unit; + + font-weight: 500; + text-align: center; + text-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1); + white-space: nowrap; - svg#bottom-left { - bottom: $vertical-padding; - left: $horizontal-padding; + color: #fff; + background-color: map-get(map-get(map-get($base-colors, text-quaternary), onlight), foreground); + -webkit-backdrop-filter: blur(27px); + backdrop-filter: blur(27px); + border-radius: 2 * $base-unit; + transition: all 200ms cubic-bezier(.42,.01,.35,1.74); + + &.is-active { + opacity: 1; + visibility: visible; + margin: 2 * $base-unit 0 0 0; + } } -} -@mixin animation ($delay, $duration, $animation) { - -webkit-animation-delay: $delay; - -webkit-animation-duration: $duration; - -webkit-animation-name: $animation; + #card-identity .reticle-container { + position: absolute; + top: 50%; + left: 50%; - -moz-animation-delay: $delay; - -moz-animation-duration: $duration; - -moz-animation-name: $animation; + width: 96px; + height: 96px; + transform-origin: center; + transform: translate(-50%, -50%); - animation-delay: $delay; - animation-duration: $duration; - animation-name: $animation; -} + perspective: 600px; -$rectangle-animation-duration: 200ms; + .message { + top: 100%; + left: 50%; + } + } -:host #barcode { - svg { + #barcode .rectangle-container { position: absolute; - width: 82px; - height: 82px; + top: 112px; + left: 20px; - transition: opacity 0.1s ease; - } + width: calc(100% - 40px); + height: calc(100% - 224px); - @include positionEdges(0px, 88px); + perspective: 600px; - .rectangle.is-done-all { - svg#top-left { @include animation(0, $rectangle-animation-duration, topLeftAnimation_mobile_portrait); } - svg#top-right { @include animation(0, $rectangle-animation-duration, topRightAnimation_mobile_portrait); } - svg#bottom-left { @include animation(0, $rectangle-animation-duration, bottomLeftAnimation_mobile_portrait); } - svg#bottom-right { @include animation(0, $rectangle-animation-duration, bottomRightAnimation_mobile_portrait); } + .message { + top: -70px; + left: 50%; + } } -} -:host(.hide) #barcode { - svg { - opacity: 0; - } -} + #blinkcard .rectangle-container { + width: 100%; + height: 100%; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + transform-origin: center; + perspective: 600px; + -webkit-transform: translate3d(0,0,0); + -webkit-backface-visibility: hidden; + + .box { + align-self: stretch; + flex: 1 1 auto; + + &.wrapper, .wrapper { background: rgba(0, 0, 0, 0.2); } + + &.body { + flex: 0 1 230px; + display: flex; + position: relative; + + .middle-left { + order: 0; + flex: 1 1 auto; + } + + .middle-right { + order: 2; + flex: 1 1 auto; + } + } + } -// Photopay cursor animation for tablet and desktop screens -@keyframes topLeftAnimation { - 0% { - top: 88px; - left: 88px; - } - 50% { - top: 107px; - left: 99px; - } - 100% { - top: 88px; - left: 88px; + .message { + top: -70px; + left: 50%; + } } } -@keyframes topRightAnimation { - 0% { - top: 88px; - right: 88px; - } - 50% { - top: 107px; - right: 99px; - } - 100% { - top: 88px; - right: 88px; - } -} +// Mobile in landscape +@media only screen and (min-width: $breakpoint-width-mobile-landscape) and (orientation: landscape) { + :host { + &::after { + bottom: 40px; + left: unset; + right: 5%; + } -@keyframes bottomLeftAnimation { - 0% { - bottom: 88px; - left: 88px; - } - 50% { - bottom: 107px; - left: 99px; - } - 100% { - bottom: 88px; - left: 88px; - } -} + .gradient-overlay { height: 88px; } -@keyframes bottomRightAnimation { - 0% { - bottom: 88px; - right: 88px; - } - 50% { - bottom: 107px; - right: 99px; - } - 100% { - bottom: 88px; - right: 88px; - } -} + #barcode .rectangle-container { + top: 88px; + left: 186px; -// Photopay cursor animation for mobiles in landscape -@keyframes topLeftAnimation_mobile_landscape { - 0% { - top: 68px; - left: 88px; - } - 50% { - top: 87px; - left: 99px; - } - 100% { - top: 68px; - left: 88px; - } -} + width: calc(100% - 372px); + height: calc(100% - 128px); -@keyframes topRightAnimation_mobile_landscape { - 0% { - top: 68px; - right: 88px; - } - 50% { - top: 87px; - right: 99px; - } - 100% { - top: 68px; - right: 88px; - } -} + .message { + top: -50px; + left: 50%; + } + } -@keyframes bottomLeftAnimation_mobile_landscape { - 0% { - bottom: 68px; - left: 88px; - } - 50% { - bottom: 87px; - left: 99px; - } - 100% { - bottom: 68px; - left: 88px; + #blinkcard .rectangle-container .box.body { + flex: 0 1 263px !important; + .rectangle { + flex: 0 1 374px !important; + } + } } } -@keyframes bottomRightAnimation_mobile_landscape { - 0% { - bottom: 68px; - right: 88px; - } - 50% { - bottom: 87px; - right: 99px; - } - 100% { - bottom: 68px; - right: 88px; - } -} +// Tablet screens portrait +@media only screen and (min-width: $breakpoint-width-tablet) and (orientation: portrait) { + :host { + &::after { + bottom: 10px; + left: calc(50% - #{46px + 15px}); + } -// Photopay cursor animation for mobiles in portrait -@keyframes topLeftAnimation_mobile_portrait { - 0% { - top: 88px; - left: 0px; - } - 50% { - top: 107px; - left: 11px; - } - 100% { - top: 88px; - left: 0px; - } -} + .gradient-overlay { height: 112px; } -@keyframes topRightAnimation_mobile_portrait { - 0% { - top: 88px; - right: 0px; - } - 50% { - top: 107px; - right: 11px; - } - 100% { - top: 88px; - right: 0px; - } -} + #barcode .rectangle-container { + top: 112px; + left: 20px; -@keyframes bottomLeftAnimation_mobile_portrait { - 0% { - bottom: 88px; - left: 0px; - } - 50% { - bottom: 107px; - left: 11px; - } - 100% { - bottom: 88px; - left: 0px; + width: calc(100% - 40px); + height: calc(100% - 224px); + + perspective: 600px; + + .message { + top: -70px; + left: 50%; + } + } + + #blinkcard .rectangle-container .box.body { + flex: 0 1 472px !important; + .rectangle { + flex: 0 1 672px !important; + } + } } } -@keyframes bottomRightAnimation_mobile_portrait { - 0% { - bottom: 88px; - right: 0px; - } - 50% { - bottom: 107px; - right: 11px; - } - 100% { - bottom: 88px; - right: 0px; +// Tablet screens landscape +@media only screen and (min-width: $breakpoint-width-tablet-landscape) and (orientation: landscape) { + :host { + &::after { + bottom: 10px; + left: calc(50% - #{46px + 15px}); + } + + .gradient-overlay { height: 112px; } + + #barcode .rectangle-container { + top: 112px; + left: 20px; + + width: calc(100% - 40px); + height: calc(100% - 224px); + + perspective: 600px; + + .message { + top: -70px; + left: 50%; + } + } + + #blinkcard .rectangle-container .box.body { + flex: 0 1 548px !important; + .rectangle { + flex: 0 1 780px !important; + } + } } } -// Tablet screens and above (desktop) -@media only screen and (min-width: $breakpoint-width-tablet) { - :host #barcode { - @include positionEdges(88px, 88px); +// Laptop screens 1 +@media only screen and (min-width: $breakpoint-width-laptop-1280) { + :host { + &::after { + bottom: 10px; + left: calc(50% - #{46px + 15px}); + } + + .controls { + width: calc(100% - 374px); + left: 188px; + } + + .gradient-overlay { height: 112px; } + + #barcode .rectangle-container { + top: 112px; + left: 188px; + + width: calc(100% - 374px); + height: calc(100% - 224px); + + perspective: 600px; + + .message { + top: -70px; + left: 50%; + } + } - .rectangle.is-done-all { - svg#top-left { @include animation(0, $rectangle-animation-duration, topLeftAnimation); } - svg#top-right { @include animation(0, $rectangle-animation-duration, topRightAnimation); } - svg#bottom-left { @include animation(0, $rectangle-animation-duration, bottomLeftAnimation); } - svg#bottom-right { @include animation(0, $rectangle-animation-duration, bottomRightAnimation); } + #blinkcard .rectangle-container .box.body { + flex: 0 1 500px !important; + .rectangle { + flex: 0 1 712px !important; + } } } } -// Mobile screens in landscape -@media only screen and (min-width: $breakpoint-width-mobile-landscape) and (max-height: $breakpoint-width-mobile-landscape) { - :host #barcode { - @include positionEdges(88px, 63px); +// Laptop screens 2 +@media only screen and (min-width: $breakpoint-width-laptop-1440) { + :host { + &::after { + bottom: 10px; + left: calc(50% - #{46px + 15px}); + } - .rectangle.is-done-all { - svg#top-left { @include animation(0, $rectangle-animation-duration, topLeftAnimation_mobile_landscape); } - svg#top-right { @include animation(0, $rectangle-animation-duration, topRightAnimation_mobile_landscape); } - svg#bottom-left { @include animation(0, $rectangle-animation-duration, bottomLeftAnimation_mobile_landscape); } - svg#bottom-right { @include animation(0, $rectangle-animation-duration, bottomRightAnimation_mobile_landscape); } + .controls { + width: calc(100% - 374px); + left: 188px; + } + + .gradient-overlay { height: 112px; } + + #barcode .rectangle-container { + top: 112px; + left: 188px; + + width: calc(100% - 374px); + height: calc(100% - 224px); + + perspective: 600px; + + .message { + top: -70px; + left: 50%; + } + } + + #blinkcard .rectangle-container .box.body { + flex: 0 1 680px !important; + .rectangle { + flex: 0 1 968px !important; + } } } } -/** - * Card - */ -:host { - #card-identity { - position: absolute; +// Desktop screens +@media only screen and (min-width: $breakpoint-width-desktop) { + :host { + &::after { + bottom: 10px; + left: calc(50% - #{46px + 15px}); + } - top: 0; - bottom: 0; - left: 0; - right: 0; - } + .controls { + width: calc(100% - 374px); + left: 188px; + } - .reticle-container { - position: absolute; - top: 50%; - left: 50%; + .gradient-overlay { height: 112px; } - width: 96px; - height: 96px; - transform-origin: center; - transform: translate(-50%, -50%); + #barcode .rectangle-container { + top: 112px; + left: 188px; - perspective: 600px; + width: calc(100% - 374px); + height: calc(100% - 224px); + + perspective: 600px; + + .message { + top: -70px; + left: 50%; + } + } + + #blinkcard .rectangle-container .box.body { + flex: 0 1 860px !important; + .rectangle { + flex: 0 1 1224px !important; + } + } } +} - .message { - display: block; +// Mobile small screen - landscape +@media only screen and (max-height: 299px) and (orientation: landscape) { + :host { + &::after { + bottom: 10px; + left: unset; + right: 20px; + } - opacity: 0; - visibility: hidden; + .gradient-overlay { height: 88px; } - position: absolute; - top: 100%; - left: 50%; - transform-origin: center; - transform: translate(-50%, 0); + #blinkcard .rectangle-container .box.body { + flex: 0 1 180px !important; + .rectangle { + flex: 0 1 260px !important; + } + } + } +} - margin: 0; - padding: 2 * $base-unit 3 * $base-unit; +// Mobile small screen - landscape +@media only screen and (min-height: 300px) and (max-height: 499px) and (orientation: landscape) { + :host { + &::after { + bottom: 30px; + left: unset; + right: 20px; + } - font-weight: 500; - text-align: center; - text-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1); - white-space: nowrap; + .gradient-overlay { height: 88px; } - color: #fff; - background-color: map-get(map-get(map-get($base-colors, text-quaternary), onlight), foreground); + #blinkcard .rectangle-container .box.body { + flex: 0 1 240px !important; + .rectangle { + flex: 0 1 340px !important; + } + } + } +} - -webkit-backdrop-filter: blur(27px); - backdrop-filter: blur(27px); +// Mobile small screen - portrait +@media only screen and (max-width: 360px) and (orientation: portrait) { + :host { + &::after { + bottom: 10px; + left: calc(50% - #{46px + 15px}); + } - border-radius: 2 * $base-unit; + .gradient-overlay { height: 88px;; } - transition: all 200ms cubic-bezier(.42,.01,.35,1.74); + #blinkcard .rectangle-container .box.body { + flex: 0 1 300px !important; + .rectangle { + flex: 0 1 500px !important; + } + } + } +} - &.is-active { - opacity: 1; - visibility: visible; - margin: 2 * $base-unit 0 0 0; +// Mobile small screen - landscape +@media only screen and (min-height: 500px) and (max-height: 699px) and (orientation: landscape) { + :host { + &::after { + bottom: 10px; + left: calc(50% - #{46px + 15px}); + } + + .gradient-overlay { height: 88px;; } + + #blinkcard .rectangle-container .box.body { + flex: 0 1 300px !important; + .rectangle { + flex: 0 1 500px !important; + } } } } diff --git a/ui/src/components/shared/mb-camera-experience/mb-camera-experience.tsx b/ui/src/components/shared/mb-camera-experience/mb-camera-experience.tsx index 3d1c54b..8c1b1cb 100644 --- a/ui/src/components/shared/mb-camera-experience/mb-camera-experience.tsx +++ b/ui/src/components/shared/mb-camera-experience/mb-camera-experience.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Event, @@ -5,7 +9,8 @@ import { Host, h, Method, - Prop + Prop, + Watch } from '@stencil/core'; import { @@ -42,63 +47,137 @@ export class MbCameraExperience { */ @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() 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; + /** * Emitted when user clicks on 'X' button. */ @Event() close: EventEmitter; + @Watch('apiState') + apiStateHandler(apiState: string, _oldValue: string) { + if (apiState === '' && (this.type === CameraExperience.CardSingleSide || this.type === CameraExperience.CardCombined)) + this.cardIdentityElement.classList.add('visible'); + else + this.cardIdentityElement.classList.remove('visible'); + } + + /** + * 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) { + if (isFlipped) + this.cameraFlipBtn.classList.add('flipped'); + else + this.cameraFlipBtn.classList.remove('flipped'); + } + + /** * Set camera scanning state. */ @Method() setState(state: CameraExperienceState, isBackSide: boolean = false, force: boolean = false): Promise { return new Promise((resolve) => { - if (!force && (!state || this.cameraStateInProgress)) { + if (!force && (!state || this.cameraStateInProgress || this.flipCameraStateInProgress)) { resolve(); return; } - this.cameraStateInProgress = true; - this.cameraReticle.setAttribute('class', this.getStateClass(state, this.type)); - this.cameraRectangle.setAttribute('class', this.getStateClass(state, this.type)); + this.cameraStateInProgress = true; + let cameraStateChangeId = this.cameraStateChangeId + 1; + this.cameraStateChangeId = cameraStateChangeId; - // Clear cameraMessage and set new message - while (this.cameraMessage.firstChild) { - this.cameraMessage.removeChild(this.cameraMessage.firstChild); + if (state === CameraExperienceState.Flip) { + this.flipCameraStateInProgress = true; } - const message = this.getStateMessage(state, isBackSide); - if (message) { - this.cameraMessage.appendChild(message); + const stateClass = this.getStateClass(state); + + switch (this.type) { + case CameraExperience.CardSingleSide: + case CameraExperience.CardCombined: + this.cameraCursorIdentityCard.setAttribute('class', `reticle ${stateClass}`); + break; + case CameraExperience.Barcode: + stateClass === 'is-detection' && this.showScanningLine ? this.scanningLineBarcode.classList.add('is-active') : this.scanningLineBarcode.classList.remove('is-active'); + this.cameraCursorBarcode.setAttribute('class', `rectangle ${stateClass}`); + break; + case CameraExperience.BlinkCard: + stateClass === 'is-default' && this.showScanningLine ? this.scanningLineBlinkCard.classList.add('is-active') : this.scanningLineBlinkCard.classList.remove('is-active'); + this.cameraCursorBlinkCard.setAttribute('class', `rectangle ${stateClass}`); + break; } - this.cameraMessage.setAttribute('class', message !== null ? 'message is-active' : 'message'); + this.setMessage(state, isBackSide, this.type); window.setTimeout(() => { - this.cameraStateInProgress = false; + if (this.flipCameraStateInProgress && state === CameraExperienceState.Flip) { + this.flipCameraStateInProgress = false; + } + if (this.cameraStateChangeId === cameraStateChangeId) { + this.cameraStateInProgress = false; + } resolve(); }, CameraExperienceStateDuration.get(state)); }); } + + render() { return ( +
+ + {/* Barcode camera experience */}
-
this.cameraRectangle = el as HTMLDivElement }> - { this.getEdge('top-right') } - { this.getEdge('bottom-right') } - { this.getEdge('top-left') } - { this.getEdge('bottom-left') } +
+
this.cameraCursorBarcode = el as HTMLDivElement }> +
+
+
+
+
+ +
this.scanningLineBarcode = el as HTMLDivElement }>
+
+
+

this.cameraMessageBarcode = el as HTMLParagraphElement }>

-
+ {/* Identity card camera experience */} +
this.cardIdentityElement = el as HTMLDivElement} class={ this.type === CameraExperience.CardSingleSide || this.type === CameraExperience.CardCombined ? 'visible': '' }>
-
this.cameraReticle = el as HTMLDivElement }> +
this.cameraCursorIdentityCard = el as HTMLDivElement }>
@@ -107,24 +186,76 @@ export class MbCameraExperience {
-

this.cameraMessage = el as HTMLParagraphElement }>

+

this.cameraMessageIdentityCard = el as HTMLParagraphElement }>

+
+
+ + {/* BlinkCard camera experience */} +
+
+
+
+
+
this.cameraCursorBlinkCard = el as HTMLDivElement }> +
+
+
+
+
+ +
this.scanningLineBlinkCard = el as HTMLDivElement }>
+
+
+

this.cameraMessageBlinkCard = el as HTMLParagraphElement }>

+
+
+
- this.handleStop(ev) } class="close-button"> - - - - - +
+ +
+ { this.apiState !== 'error' && + this.handleStop(ev) } class="close-button"> + + + + + + } + + +
); } - private cameraReticle!: HTMLDivElement; - private cameraRectangle!: HTMLDivElement; - private cameraMessage!: HTMLParagraphElement; + private cameraCursorBarcode!: HTMLDivElement; + private cameraCursorIdentityCard!: HTMLDivElement; + private cameraCursorBlinkCard!: HTMLDivElement; + private cameraMessageIdentityCard!: HTMLParagraphElement; + private cameraMessageBlinkCard!: HTMLParagraphElement; + private cameraMessageBarcode!: HTMLParagraphElement; private cameraStateInProgress: boolean = false; + private cameraStateChangeId: number = 0; + private cardIdentityElement: HTMLDivElement; + private cameraFlipBtn: HTMLButtonElement; + private flipCameraStateInProgress: boolean = false; + + private scanningLineBarcode!: HTMLDivElement; + private scanningLineBlinkCard!: HTMLDivElement; + + private flipCamera(): void { + this.flipCameraAction.emit(); + } private handleStop(ev: UIEvent) { ev.preventDefault(); @@ -132,129 +263,119 @@ export class MbCameraExperience { this.close.emit(); } - private getEdge(id: string): SVGElement { - return ( - - - { this.getPath(id) } - - - - - - - - - - - - - - - - - - - ) - } - - private getPath(direction: string): HTMLElement { - if (direction === 'top-left') { - return () - } - - if (direction === 'top-right') { - return () - } - - if (direction === 'bottom-left') { - return () - } - - if (direction === 'bottom-right') { - return () - } - } - - private getStateClass(state: CameraExperienceState, type: CameraExperience): string { - let classNames; - - if (type === CameraExperience.Barcode) { - classNames = ['rectangle']; - } - else { - classNames = ['reticle']; - } + private getStateClass(state: CameraExperienceState): string { + let stateClass = 'is-default'; switch (state) { case CameraExperienceState.Classification: - classNames.push('is-classification'); + stateClass = 'is-classification'; break; case CameraExperienceState.Default: - classNames.push('is-default'); + stateClass = 'is-default'; break; case CameraExperienceState.Detection: - classNames.push('is-detection'); + stateClass = 'is-detection'; break; case CameraExperienceState.MoveFarther: - classNames.push('is-error-move-farther'); + stateClass = 'is-error-move-farther'; break; case CameraExperienceState.MoveCloser: - classNames.push('is-error-move-closer'); + stateClass = 'is-error-move-closer'; break; case CameraExperienceState.AdjustAngle: - classNames.push('is-error-adjust-angle'); + stateClass = 'is-error-adjust-angle'; break; case CameraExperienceState.Flip: - classNames.push('is-flip'); + stateClass = 'is-flip'; break; case CameraExperienceState.Done: - classNames.push('is-done'); + stateClass = 'is-done'; break; case CameraExperienceState.DoneAll: - classNames.push('is-done-all'); + stateClass = 'is-done-all'; break; default: // Reset class } - return classNames.join(' '); + return stateClass; + } + + private setMessage(state: CameraExperienceState, isBackSide: boolean, type: CameraExperience): void { + const message = this.getStateMessage(state, isBackSide, type); + + switch(type) { + case CameraExperience.CardSingleSide: + case CameraExperience.CardCombined: + while (this.cameraMessageIdentityCard.firstChild) { + this.cameraMessageIdentityCard.removeChild(this.cameraMessageIdentityCard.firstChild); + } + if (message) this.cameraMessageIdentityCard.appendChild(message); + + this.cameraMessageIdentityCard.setAttribute('class', message && message !== null ? 'message is-active' : 'message'); + break; + case CameraExperience.BlinkCard: + while (this.cameraMessageBlinkCard.firstChild) { + this.cameraMessageBlinkCard.removeChild(this.cameraMessageBlinkCard.firstChild); + } + if (message) this.cameraMessageBlinkCard.appendChild(message); + + this.cameraMessageBlinkCard.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): HTMLSpanElement|null { + private getStateMessage(state: CameraExperienceState, isBackSide: boolean = false, type: CameraExperience): HTMLSpanElement|null { const getStateMessageAsHTML = (message: string|Array): HTMLSpanElement => { - const messageArray = typeof message === 'string' ? [ message ] : message; - const children = []; + if (message) { + const messageArray = typeof message === 'string' ? [ message ] : message; + const children = []; - while (messageArray.length) { - const sentence = messageArray.shift(); - children.push(document.createTextNode(sentence)); + while (messageArray.length) { + const sentence = messageArray.shift(); + children.push(document.createTextNode(sentence)); - if (messageArray.length) { - children.push(document.createElement('br')); + if (messageArray.length) { + children.push(document.createElement('br')); + } } - } - const spanElement = document.createElement('span'); + const spanElement = document.createElement('span'); - while (children.length) { - spanElement.appendChild(children.shift()); - } + while (children.length) { + spanElement.appendChild(children.shift()); + } - return spanElement; + return spanElement; + } } switch (state) { case CameraExperienceState.Default: + if (type === CameraExperience.Barcode) { + 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)); @@ -272,6 +393,7 @@ export class MbCameraExperience { 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: diff --git a/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.e2e.ts b/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.e2e.ts index 3f0b339..4ef634f 100644 --- a/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.e2e.ts +++ b/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-camera-experience', () => { diff --git a/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.spec.tsx b/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.spec.tsx index c9c5db3..1984478 100644 --- a/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.spec.tsx +++ b/ui/src/components/shared/mb-camera-experience/test/mb-camera-experience.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbCameraExperience } from '../mb-camera-experience'; diff --git a/ui/src/components/shared/mb-component/mb-component.scss b/ui/src/components/shared/mb-component/mb-component.scss index 290bcbe..9266a74 100644 --- a/ui/src/components/shared/mb-component/mb-component.scss +++ b/ui/src/components/shared/mb-component/mb-component.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + @import "../styles/globals-sass"; :host { @@ -42,7 +46,7 @@ align-items: center; .action-label { - display: block; + display: var(--mb-component-action-label); margin: 0 $padding-unit-large 0 0; &:dir(rtl) { @@ -52,12 +56,11 @@ .action-buttons { display: flex; - justify-content: flex-end; - - width: 2 * $button-size + $padding-unit-medium; + width: var(--mb-component-action-buttons-width); + justify-content: var(--mb-component-action-buttons-justify-content); mb-button:last-child { - margin: 0 0 0 $padding-unit-medium; + margin: var(--mb-component-action-buttons-last-margin); } } } @@ -210,6 +213,14 @@ left: 0; right: 0; } + + mb-camera-experience.is-muted { + background-color: rgba(0, 0, 0, .6); + } + + mb-camera-experience.is-error { + background-color:rgba(0, 0, 0, 1); + } } :host #overlay-camera-experience.visible { @@ -223,3 +234,13 @@ opacity: 0; clip: rect(1px, 1px, 1px, 1px); } + +:host button.modal-action-button { + width: 126px; + height: 32px; + border-radius: 0; + border: 0; + background: #48B2E8; + color: #ffffff; + cursor: pointer; +} diff --git a/ui/src/components/shared/mb-component/mb-component.tsx b/ui/src/components/shared/mb-component/mb-component.tsx index 74ce0d1..d2a8d53 100644 --- a/ui/src/components/shared/mb-component/mb-component.tsx +++ b/ui/src/components/shared/mb-component/mb-component.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Event, @@ -5,9 +9,12 @@ import { Host, h, Prop, - Element + Element, + Method } from '@stencil/core'; +import * as PhotoPaySDK from '../../../../../es/photopay-sdk'; + import { CameraExperienceState, Code, @@ -17,7 +24,6 @@ import { EventScanSuccess, FeedbackCode, FeedbackMessage, - FeedbackState, RecognitionEvent, RecognitionStatus, ImageRecognitionConfiguration, @@ -56,6 +62,11 @@ export class MbComponent { */ @Prop() licenseKey: string; + /** + * See description in public component. + */ + @Prop({ mutable: true }) wasmType: string | null; + /** * See description in public component. */ @@ -64,7 +75,7 @@ export class MbComponent { /** * See description in public component. */ - @Prop({ mutable: true }) recognizerOptions: Array; + @Prop({ mutable: true }) recognizerOptions: { [key: string]: any }; /** * See description in public component. @@ -96,16 +107,49 @@ export class MbComponent { */ @Prop() scanFromImage: boolean = true; + /** + * See description in public component. + */ + @Prop() thoroughScanFromImage: boolean = false; + + /** + * See description in public component. + */ + @Prop() showActionLabels: boolean = false; + + /** + * See description in public component. + */ + @Prop() showModalWindows: boolean = false; + + /** + * See description in public component. + */ + @Prop() showCameraFeedbackBarcodeMessage: boolean = false; + + /** + * See description in public component. + */ + @Prop() showScanningLine: boolean = false; + /** * See description in public component. */ @Prop() iconCameraDefault: string = 'data:image/svg+xml;utf8,'; + + /** + * See description in public component. + */ @Prop() iconCameraActive: string = 'data:image/svg+xml;utf8,'; /** * See description in public component. */ @Prop() iconGalleryDefault: string = 'data:image/svg+xml;utf8,'; + + /** + * See description in public component. + */ @Prop() iconGalleryActive: string = 'data:image/svg+xml;utf8,'; /** @@ -116,13 +160,28 @@ export class MbComponent { /** * See description in public component. */ - @Prop() iconSpinner: string; + @Prop() iconSpinnerScreenLoading: string; + + /** + * See description in public component. + */ + @Prop() iconSpinnerFromGalleryExperience: string; + + /** + * Instance of SdkService passed from root component. + */ + @Prop() sdkService: SdkService; /** * Instance of TranslationService passed from root component. */ @Prop() translationService: TranslationService; + /** + * Camera device ID passed from root component. + */ + @Prop() cameraId: string | null = null; + /** * See event 'fatalError' in public component. */ @@ -148,16 +207,72 @@ export class MbComponent { */ @Event() feedback: EventEmitter; + /** + * See event 'cameraScanStarted' in public component. + */ + @Event() cameraScanStarted: EventEmitter; + + /** + * See event 'imageScanStarted' in public component. + */ + @Event() imageScanStarted: EventEmitter; + /** * Host element as variable for manipulation (CSS in this case) */ @Element() hostEl: HTMLElement; + /** + * Method is exposed outside which allow us to control UI state from parent component. + * + * In case of state `ERROR` and if `showModalWindows` is set to `true`, modal window + * with error message will be displayed. + */ + @Method() + async setUiState(state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS') { + window.setTimeout(() => { + if (this.overlays.camera.visible) { + if (state === 'ERROR' && !this.showModalWindows) { + this.apiProcessStatusElement.state = 'NONE'; + this.apiProcessStatusElement.visible = false; + this.stopRecognition(); + return; + } + + this.apiProcessStatusElement.state = state; + this.apiProcessStatusElement.visible = true; + + if (state !== 'ERROR') { + this.cameraExperience.classList.add('is-muted'); + } + else { + this.cameraExperience.classList.add('is-error'); + } + + this.cameraExperience.apiState = state; + } + else if (this.overlays.processing.visible) { + if (state === 'ERROR') { + if (this.showModalWindows) { + this.galleryExperienceModalErrorWindow.visible = true; + } + else { + this.galleryExperienceModalErrorWindow.visible = false; + this.stopRecognition(); + } + } + } + + if (state === 'SUCCESS') { + window.setTimeout(() => this.stopRecognition(), 400); + } + }, 400); + } + /** * Lifecycle hooks */ connectedCallback() { - this.sdkService = new SdkService(); this.hostEl.addEventListener('keyup', (ev: any) => { if (ev.key === 'Escape' || ev.code === 'Escape') { this.stopRecognition(); @@ -180,7 +295,7 @@ export class MbComponent { visible={!this.hideLoadingAndErrorUi} ref={el => this.screens.loading = el as HTMLMbScreenElement} > - + this.scanFromCameraButton = el as HTMLMbButtonElement} preventDefault={true} - visible={false} + visible={true} + disabled={false} icon={true} onButtonClick={() => this.startScanFromCamera()} imageSrcDefault={this.iconCameraDefault} @@ -249,11 +365,24 @@ export class MbComponent { this.overlays.processing = el as HTMLMbOverlayElement} > - +

{this.translationService.i('process-image-message').toString()}

+ this.galleryExperienceModalErrorWindow = el as HTMLMbModalElement} + visible={false} + modalTitle={this.translationService.i('feedback-scan-unsuccessful-title').toString()} + content={this.translationService.i('feedback-scan-unsuccessful').toString()} + onClose={() => this.closeGalleryExperienceModal()} + > +
+ +
+
this.cameraExperience = el as HTMLMbCameraExperienceElement} translationService={this.translationService} + showScanningLine={this.showScanningLine} + showCameraFeedbackBarcodeMessage={this.showCameraFeedbackBarcodeMessage} onClose={() => this.stopRecognition()} + onFlipCameraAction={() => this.flipCameraAction()} class="overlay-camera-element" > + this.apiProcessStatusElement = el as HTMLMbApiProcessStatusElement} + translationService={this.translationService} + onCloseTryAgain={() => this.closeApiProcessStatus(true)} + onCloseFromStart={() => this.stopRecognition()} + >
this.overlays.modal = el as HTMLMbOverlayElement} > this.modalElement = el as HTMLMbModalElement} - onClose={() => this.closeModal()} - class="overlay-modal-element" - > + ref={el => this.licenseExperienceModal = el as HTMLMbModalElement} + modalTitle="Error" + > +
+ +
+
); } - closeModal() { - this.showOverlay(''); + async closeApiProcessStatus(restart: boolean = false): Promise { + window.setTimeout(() => { + this.apiProcessStatusElement.visible = false; + this.apiProcessStatusElement.state = 'NONE'; + this.cameraExperience.classList.remove('is-muted'); + this.cameraExperience.classList.remove('is-error'); + }, 600); + + if (restart) { + await this.checkInputProperties() + .then(() => this.sdkService.resumeRecognition()) + .then(() => { + window.setTimeout(() => this.cameraExperience.apiState = '', 400); + this.isBackSide = false; + this.cameraExperience.setState(CameraExperienceState.Default, this.isBackSide, true); + }); + } } async componentDidRender() { @@ -319,7 +478,8 @@ export class MbComponent { const initEvent: EventReady | EventFatalError = await this.sdkService.initialize(this.licenseKey, { allowHelloMessage: this.allowHelloMessage, - engineLocation: this.engineLocation + engineLocation: this.engineLocation, + wasmType: this.getSDKWasmType(this.wasmType) }); this.cameraExperience.showOverlay = this.sdkService.showOverlay; @@ -329,23 +489,42 @@ export class MbComponent { return; } + if (this.showActionLabels) { + this.scanFromCameraButton.label = this.translationService.i('action-message-camera').toString(); + this.scanFromImageButton.label = this.translationService.i('action-message-image').toString(); + } + if (this.scanFromCamera) { this.scanFromCameraButton.visible = true; const hasVideoDevices = await DeviceHelpers.hasVideoDevices(); + this.scanFromCameraButton.disabled = !hasVideoDevices; if (!hasVideoDevices) { this.feedback.emit({ code: FeedbackCode.CameraDisabled, - state: FeedbackState.Info, + state: 'FEEDBACK_INFO', message: this.translationService.i('camera-disabled').toString() }); + + if (this.showActionLabels) { + this.scanFromCameraButton.label = this.translationService.i('action-message-camera-disabled').toString(); + } } } - if (this.scanFromImage && this.sdkService.isScanFromImageAvailable(this.recognizers)) { + if (this.scanFromImage) { this.scanFromImageButton.visible = true; + + const imageScanIsAvailable = this.sdkService.isScanFromImageAvailable(this.recognizers, this.recognizerOptions); + this.scanFromImageButton.disabled = !imageScanIsAvailable; + + if (!imageScanIsAvailable) { + if (this.showActionLabels) { + this.scanFromImageButton.label = this.translationService.i('action-message-image-not-supported').toString(); + } + } } this.ready.emit(initEvent); @@ -360,7 +539,6 @@ export class MbComponent { /** * Private methods and properties */ - private sdkService: SdkService; /* Element references */ private screens: { [key: string]: HTMLMbScreenElement | null } = { @@ -383,13 +561,22 @@ export class MbComponent { private scanFromImageButton!: HTMLMbButtonElement; private scanFromImageInput!: HTMLInputElement; private videoElement!: HTMLVideoElement; - private modalElement!: HTMLMbModalElement; + private licenseExperienceModal!: HTMLMbModalElement; + private apiProcessStatusElement!: HTMLMbApiProcessStatusElement; + private galleryExperienceModalErrorWindow: HTMLMbModalElement; + private scanReset: boolean = false; private detectionSuccessLock = false; private isBackSide = false; private initialBodyOverflowValue: string; + private async flipCameraAction(): Promise { + await this.sdkService.flipCamera(); + const cameraFlipped = await this.sdkService.isCameraFlipped(); + this.cameraExperience.setCameraFlipState(cameraFlipped); + } + /* Helper methods */ private async checkInputProperties(): Promise { if (!this.licenseKey) { @@ -410,26 +597,7 @@ export class MbComponent { return false; } - this.cameraExperience.type = this.sdkService.getDesiredCameraExperience(this.recognizers); - - // Recognizer options - if (this.recognizerOptions && this.recognizerOptions.length) { - const conclusion: CheckConclusion = this.sdkService.checkRecognizerOptions( - this.recognizers, - this.recognizerOptions - ); - - if (!conclusion.status) { - const fatalError = new EventFatalError( - Code.InvalidRecognizerOptions, - conclusion.message - ); - - this.setFatalError(fatalError); - return false; - } - } - + this.cameraExperience.type = this.sdkService.getDesiredCameraExperience(this.recognizers, this.recognizerOptions); return true; } @@ -441,10 +609,11 @@ export class MbComponent { const configuration: VideoRecognitionConfiguration = { recognizers: this.recognizers, successFrame: this.includeSuccessFrame, - cameraFeed: this.videoElement + cameraFeed: this.videoElement, + cameraId: this.cameraId }; - if (this.recognizerOptions && this.recognizerOptions.length) { + if (this.recognizerOptions && Object.keys(this.recognizerOptions).length > 0) { configuration.recognizerOptions = this.recognizerOptions; } @@ -466,11 +635,12 @@ export class MbComponent { this.scanError.emit({ code: Code.EmptyResult, fatal: true, - message: 'Could not extract information from video feed!' + message: 'Could not extract information from video feed!', + recognizerName: recognitionEvent.data.recognizerName }); this.feedback.emit({ code: FeedbackCode.ScanUnsuccessful, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('feedback-scan-unsuccessful').toString() }); } @@ -499,6 +669,7 @@ export class MbComponent { window.setTimeout(() => { if (this.detectionSuccessLock) { this.cameraExperience.setState(CameraExperienceState.Detection); + this.scanReset = false; } }, 100); break; @@ -531,44 +702,87 @@ export class MbComponent { break; case RecognitionStatus.OnFirstSideResult: - this.cameraExperience.setState(CameraExperienceState.Flip) + this.cameraExperience.setState(CameraExperienceState.Done, false, true) .then(() => { - this.isBackSide = true; - this.cameraExperience.setState( - CameraExperienceState.Default, - this.isBackSide - ); - }); + this.cameraExperience.setState(CameraExperienceState.Flip, this.isBackSide, true) + .then(() => { + if (!this.scanReset) { + this.isBackSide = true; + this.cameraExperience.setState( + CameraExperienceState.Default, + this.isBackSide + ); + } + }); + }) break; case RecognitionStatus.ScanSuccessful: - this.cameraExperience.setState(CameraExperienceState.DoneAll, false, true) - .then(() => { - this.cameraExperience.classList.add('hide'); - this.showOverlay(''); + /* Which recognizer is it? ImageCapture or some other? + * + * Image capture has the 'imageCapture' flag set to true, we do not want to close camera overlay after image + * acquisition process is finished. Cause maybe backend service will failed and we can press retry to resume + * with the same video recognizer and try again + */ + if (!recognitionEvent.data.imageCapture) { + this.cameraExperience.setState(CameraExperienceState.DoneAll, false, true) + .then(() => { + this.cameraExperience.classList.add('hide'); + + this.showOverlay(''); + window.setTimeout(() => { this.cameraExperience.setState(CameraExperienceState.Default); }, 1000); + + this.scanSuccess.emit(recognitionEvent.data?.result); + this.feedback.emit({ + code: FeedbackCode.ScanSuccessful, + state: 'FEEDBACK_OK', + message: '' + }); }); - this.scanSuccess.emit(recognitionEvent.data); - this.feedback.emit({ - code: FeedbackCode.ScanSuccessful, - state: FeedbackState.Ok, - message: '' - }); + } + else { + const resultIsValid = recognitionEvent.data.result.recognizer.processingStatus === 0 && recognitionEvent.data.result.recognizer.state === 2; + + if (resultIsValid) { + this.scanSuccess.emit(recognitionEvent.data?.result); + this.feedback.emit({ + code: FeedbackCode.ScanSuccessful, + state: 'FEEDBACK_OK', + message: '' + }); + } + else if (!recognitionEvent.data.initiatedByUser) { + this.scanError.emit({ + code: Code.EmptyResult, + fatal: true, + message: 'Could not extract information from video feed!', + recognizerName: recognitionEvent.data.recognizerName + }); + } + } break; case RecognitionStatus.CameraNotAllowed: this.scanError.emit({ code: Code.CameraNotAllowed, fatal: true, - message: 'Cannot access camera!' + message: 'Cannot access camera!', + recognizerName: '' }); this.feedback.emit({ code: FeedbackCode.CameraNotAllowed, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('camera-not-allowed').toString() }); + window.setTimeout(() => { + this.scanFromCameraButton.disabled = true; + if (this.showActionLabels) { + this.scanFromCameraButton.label = this.translationService.i('action-message-camera-not-allowed').toString(); + } + }, 10); this.showOverlay(''); break; @@ -576,13 +790,21 @@ export class MbComponent { this.scanError.emit({ code: Code.CameraInUse, fatal: true, - message: 'Camera already in use!' + message: 'Camera already in use!', + recognizerName: '' }); this.feedback.emit({ code: FeedbackCode.CameraInUse, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('camera-in-use').toString() }); + + window.setTimeout(() => { + this.scanFromCameraButton.disabled = true; + if (this.showActionLabels) { + this.scanFromCameraButton.label = this.translationService.i('action-message-camera-in-use').toString(); + } + }, 10); this.showOverlay(''); break; @@ -592,13 +814,20 @@ export class MbComponent { this.scanError.emit({ code: Code.CameraGenericError, fatal: true, - message: `There was a problem while accessing camera ${recognitionEvent.status}` + message: `There was a problem while accessing camera ${recognitionEvent.status}`, + recognizerName: '' }); this.feedback.emit({ code: FeedbackCode.CameraGenericError, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('camera-generic-error').toString() }); + window.setTimeout(() => { + this.scanFromCameraButton.disabled = true; + if (this.showActionLabels) { + this.scanFromCameraButton.label = this.translationService.i('action-message-camera-disabled').toString(); + } + }, 10); this.showOverlay(''); break; @@ -609,24 +838,31 @@ export class MbComponent { try { this.cameraExperience.classList.remove('hide'); + this.cameraScanStarted.emit(); await this.sdkService.scanFromCamera(configuration, eventHandler); + + const cameraFlipped = this.sdkService.isCameraFlipped(); + this.cameraExperience.setCameraFlipState(cameraFlipped); } catch (error) { this.checkIfInternetIsAvailable() .then((isAvailable) => { if (isAvailable) { if (error?.code === 'UNLOCK_LICENSE_ERROR' ) { this.setFatalError(new EventFatalError(Code.LicenseError, 'Something is wrong with the license.', error)); - this.showLicenseErrorModal(error); + this.showLicenseInfoModal(error); } else { + console.log("error", error); + this.scanError.emit({ code: Code.GenericScanError, fatal: true, - message: `There was a problem during scan action.` + message: `There was a problem during scan action.`, + recognizerName: '' }); this.feedback.emit({ code: FeedbackCode.GenericScanError, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('feedback-error-generic').toString() }); @@ -635,7 +871,7 @@ export class MbComponent { } else { this.setFatalError(new EventFatalError(Code.InternetNotAvailable, this.translationService.i('check-internet-connection').toString())); - this.showLicenseErrorModal(this.translationService.i('check-internet-connection').toString()); + this.showLicenseInfoModal(this.translationService.i('check-internet-connection').toString()); } }); } @@ -665,46 +901,56 @@ export class MbComponent { this.scanError.emit({ code: Code.NoImageFileFound, fatal: true, - message: 'No image file was provided to SDK service!' + message: 'No image file was provided to SDK service!', + recognizerName: '' }); this.feedback.emit({ code: FeedbackCode.ScanUnsuccessful, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('feedback-scan-unsuccessful').toString() }); this.showOverlay(''); + this.scanFromImageInput.value = ''; break; case RecognitionStatus.DetectionFailed: // Do nothing, RecognitionStatus.EmptyResultState will handle negative outcome + this.scanFromImageInput.value = ''; break; case RecognitionStatus.EmptyResultState: this.scanError.emit({ code: Code.EmptyResult, fatal: true, - message: 'Could not extract information from image!' + message: 'Could not extract information from image!', + recognizerName: recognitionEvent.data.recognizerName }); this.feedback.emit({ code: FeedbackCode.ScanUnsuccessful, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('feedback-scan-unsuccessful').toString() }); this.showOverlay(''); + this.scanFromImageInput.value = ''; break; case RecognitionStatus.UnknownError: // Do nothing, RecognitionStatus.EmptyResultState will handle negative outcome + this.scanFromImageInput.value = ''; break; case RecognitionStatus.ScanSuccessful: this.scanSuccess.emit(recognitionEvent.data); this.feedback.emit({ code: FeedbackCode.ScanSuccessful, - state: FeedbackState.Ok, + state: 'FEEDBACK_OK', message: '' }); - this.showOverlay(''); + this.scanFromImageInput.value = ''; + + if (!recognitionEvent.data.imageCapture) { + this.showOverlay(''); + } break; default: @@ -713,6 +959,12 @@ export class MbComponent { }; try { + this.imageScanStarted.emit(); + + if (this.thoroughScanFromImage) { + configuration.thoroughScan = true; + } + await this.sdkService.scanFromImage(configuration, eventHandler); } catch (error) { this.checkIfInternetIsAvailable() @@ -720,17 +972,18 @@ export class MbComponent { if (isAvailable) { if (error?.code === 'UNLOCK_LICENSE_ERROR' ) { this.setFatalError(new EventFatalError(Code.LicenseError, 'Something is wrong with the license.', error)); - this.showLicenseErrorModal(error); + this.showLicenseInfoModal(error); } else { this.scanError.emit({ code: Code.GenericScanError, fatal: true, - message: `There was a problem during scan action.` + message: `There was a problem during scan action.`, + recognizerName: '' }); this.feedback.emit({ code: FeedbackCode.GenericScanError, - state: FeedbackState.Error, + state: 'FEEDBACK_ERROR', message: this.translationService.i('feedback-error-generic').toString() }); @@ -739,27 +992,22 @@ export class MbComponent { } else { this.setFatalError(new EventFatalError(Code.InternetNotAvailable, this.translationService.i('check-internet-connection').toString())); - this.showLicenseErrorModal(this.translationService.i('check-internet-connection').toString()); + this.showLicenseInfoModal(this.translationService.i('check-internet-connection').toString()); } }); } } - private showLicenseErrorModal(error: any) { - this.modalElement.content = { - title: 'Error', - body: '' - } - + private showLicenseInfoModal(error: any): void { if (typeof error === 'string') { - this.modalElement.content.body = error; + this.licenseExperienceModal.content = error; } else { if (error.type === 'NETWORK_ERROR') { - this.modalElement.content.body = this.translationService.i('network-error').toString(); + this.licenseExperienceModal.content = this.translationService.i('network-error').toString(); } else { - this.modalElement.content.body = this.translationService.i('scanning-not-available').toString(); + this.licenseExperienceModal.content = this.translationService.i('scanning-not-available').toString(); } } @@ -906,8 +1154,35 @@ export class MbComponent { } private stopRecognition() { - this.sdkService.stopRecognition(); this.cameraExperience.classList.add('hide'); + + this.sdkService.stopRecognition(); + this.scanReset = true; + + window.setTimeout(() => { + this.cameraExperience.setState(CameraExperienceState.Default, false, true); + this.cameraExperience.apiState = ''; + }, 500); + this.showOverlay(''); + this.closeApiProcessStatus(); + } + + private closeGalleryExperienceModal() { + this.galleryExperienceModalErrorWindow.visible = false; + this.stopRecognition(); + } + + private getSDKWasmType(wasmType: string): PhotoPaySDK.WasmType | null { + switch (wasmType) { + case 'BASIC': + return PhotoPaySDK.WasmType.Basic; + case 'ADVANCED': + return PhotoPaySDK.WasmType.Advanced; + case 'ADVANCED_WITH_THREADS': + return PhotoPaySDK.WasmType.AdvancedWithThreads; + default: + return null; + } } } diff --git a/ui/src/components/shared/mb-component/test/mb-component.e2e.ts b/ui/src/components/shared/mb-component/test/mb-component.e2e.ts index 2b93b6d..8053e94 100644 --- a/ui/src/components/shared/mb-component/test/mb-component.e2e.ts +++ b/ui/src/components/shared/mb-component/test/mb-component.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-component', () => { diff --git a/ui/src/components/shared/mb-component/test/mb-component.spec.tsx b/ui/src/components/shared/mb-component/test/mb-component.spec.tsx index 9a74e83..c8d6936 100644 --- a/ui/src/components/shared/mb-component/test/mb-component.spec.tsx +++ b/ui/src/components/shared/mb-component/test/mb-component.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbComponent } from '../mb-component'; diff --git a/ui/src/components/shared/mb-container/mb-container.scss b/ui/src/components/shared/mb-container/mb-container.scss index 7b173f6..31f1b2e 100644 --- a/ui/src/components/shared/mb-container/mb-container.scss +++ b/ui/src/components/shared/mb-container/mb-container.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + :host { display: block; min-width: 280px; diff --git a/ui/src/components/shared/mb-container/mb-container.tsx b/ui/src/components/shared/mb-container/mb-container.tsx index a09bb11..8def965 100644 --- a/ui/src/components/shared/mb-container/mb-container.tsx +++ b/ui/src/components/shared/mb-container/mb-container.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Host, h } from '@stencil/core'; @Component({ diff --git a/ui/src/components/shared/mb-container/test/mb-container.e2e.ts b/ui/src/components/shared/mb-container/test/mb-container.e2e.ts index d7353c3..12c2628 100644 --- a/ui/src/components/shared/mb-container/test/mb-container.e2e.ts +++ b/ui/src/components/shared/mb-container/test/mb-container.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-container', () => { diff --git a/ui/src/components/shared/mb-container/test/mb-container.spec.tsx b/ui/src/components/shared/mb-container/test/mb-container.spec.tsx index 282aa79..df958e2 100644 --- a/ui/src/components/shared/mb-container/test/mb-container.spec.tsx +++ b/ui/src/components/shared/mb-container/test/mb-container.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbContainer } from '../mb-container'; diff --git a/ui/src/components/shared/mb-feedback/mb-feedback.scss b/ui/src/components/shared/mb-feedback/mb-feedback.scss index 1a89f55..b5ac175 100644 --- a/ui/src/components/shared/mb-feedback/mb-feedback.scss +++ b/ui/src/components/shared/mb-feedback/mb-feedback.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + @import "../styles/globals-sass"; :host { diff --git a/ui/src/components/shared/mb-feedback/mb-feedback.tsx b/ui/src/components/shared/mb-feedback/mb-feedback.tsx index c3c3354..d99c062 100644 --- a/ui/src/components/shared/mb-feedback/mb-feedback.tsx +++ b/ui/src/components/shared/mb-feedback/mb-feedback.tsx @@ -1,8 +1,11 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Host, h, Method, Prop } from '@stencil/core'; import { - FeedbackMessage, - FeedbackState + FeedbackMessage } from '../../../utils/data-structures'; @@ -37,12 +40,12 @@ export class MbFeedback { private paragraphEl!: HTMLParagraphElement; - private getFeedbackClassName(state: FeedbackState): string { + private getFeedbackClassName(state: 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK'): string { switch (state) { - case FeedbackState.Error: + case 'FEEDBACK_ERROR': return 'error'; - case FeedbackState.Info: + case 'FEEDBACK_INFO': return 'info'; default: diff --git a/ui/src/components/shared/mb-feedback/test/mb-feedback.e2e.ts b/ui/src/components/shared/mb-feedback/test/mb-feedback.e2e.ts index 5139406..10a4989 100644 --- a/ui/src/components/shared/mb-feedback/test/mb-feedback.e2e.ts +++ b/ui/src/components/shared/mb-feedback/test/mb-feedback.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-feedback', () => { diff --git a/ui/src/components/shared/mb-feedback/test/mb-feedback.spec.tsx b/ui/src/components/shared/mb-feedback/test/mb-feedback.spec.tsx index 057316b..fe49b26 100644 --- a/ui/src/components/shared/mb-feedback/test/mb-feedback.spec.tsx +++ b/ui/src/components/shared/mb-feedback/test/mb-feedback.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbFeedback } from '../mb-feedback'; diff --git a/ui/src/components/shared/mb-modal/mb-modal.scss b/ui/src/components/shared/mb-modal/mb-modal.scss index e906c58..b19f094 100644 --- a/ui/src/components/shared/mb-modal/mb-modal.scss +++ b/ui/src/components/shared/mb-modal/mb-modal.scss @@ -1,63 +1,83 @@ -@import "../styles/globals-sass"; +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ :host { - background-color: rgba($color: #000000, $alpha: 0.2); - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - - display: flex; - justify-content: center; - align-items: flex-start; - overflow: hidden; - overflow-y: auto; - padding: 100px; -} - -:host .mb-modal { - - position: relative; - - background-color: #ffffff; - - width: 100%; - max-width: 320px; - - .title { - margin-top: 24px; - text-align: center; - font-weight: 500; - font-size: 16px; - line-height: 24px; - } + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + opacity: 0; + + visibility: hidden; + + display: flex; + justify-content: center; + align-items: flex-start; + overflow: hidden; + overflow-y: auto; + padding: 24px; + + .mb-modal { + width: 100%; + max-width: 552px; - .content { - margin: 24px 0 24px 0; - font-weight: 400; - font-size: 14px; - line-height: 20px; + position: relative; + background-color: var(--mb-component-background); + color: var(--mb-component-font-color); + + padding: 24px; + + .close-wrapper { + position: absolute; + right: 24px; + top: 24px; + cursor: pointer; + + svg { + width: 24px; + height: 24px; + } + } - &.centered { + .title { text-align: center; + font-weight: 500; + font-size: 16px; + line-height: 24px; + } + + .content { + margin: 24px 0; + font-weight: 400; + font-size: 14px; + line-height: 20px; + + &.centered { + text-align: center; + } } - } - .actions { - display: flex; - justify-content: center; - margin-bottom: 25px; - - button { - width: 126px; - height: 32px; - border-radius: 0; - border: 0; - background: #48B2E8; - color: #ffffff; - cursor: pointer; + .actions { + display: flex; + justify-content: center; + + button { + width: 126px; + height: 32px; + border-radius: 0; + border: 0; + background: #48B2E8; + color: #ffffff; + cursor: pointer; + } } } } - + +:host(.visible) { + visibility: visible; + opacity: 1; +} + diff --git a/ui/src/components/shared/mb-modal/mb-modal.tsx b/ui/src/components/shared/mb-modal/mb-modal.tsx index 2a9bb92..80ac65a 100644 --- a/ui/src/components/shared/mb-modal/mb-modal.tsx +++ b/ui/src/components/shared/mb-modal/mb-modal.tsx @@ -1,14 +1,16 @@ -import { +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +import { Component, Host, h, Prop, - Event, EventEmitter, + Event } from '@stencil/core'; -import { ModalContent } from '../../../utils/data-structures'; - @Component({ tag: 'mb-modal', @@ -18,37 +20,83 @@ import { ModalContent } from '../../../utils/data-structures'; export class MbModal { /** - * Passed content from parent component + * Show modal content + */ + @Prop() visible: boolean = false; + + /** + * Passed title content from parent component + */ + @Prop() modalTitle: string = ""; + + /** + * Passed body content from parent component + */ + @Prop() content: string = ""; + + /** + * Center content inside modal */ - @Prop() content: ModalContent; + @Prop() contentCentered: boolean = true; /** - * Emitted when user clicks on 'Close' button. + * Emitted when user clicks on 'X' button. */ @Event() close: EventEmitter; + render() { return ( - + -
+
-
{ this.content && this.content.title }
+
+
this.close.emit() }> + + + + +
+
-
+
{ this.modalTitle }
- { this.content && this.content.body } +
-
+ { this.content } -
- -
+
+ +
+ +
+
+ ); } + getHostClassName(): string { + const classNames = []; + + if (this.visible) { + classNames.push('visible'); + } + + return classNames.join(' '); + } + + getContentClassName(): string { + const classNames = ['content']; + + if (this.contentCentered) { + classNames.push('centered'); + } + + return classNames.join(' '); + } } diff --git a/ui/src/components/shared/mb-modal/test/mb-modal.e2e.ts b/ui/src/components/shared/mb-modal/test/mb-modal.e2e.ts index 3cea9a0..6a5b2a2 100644 --- a/ui/src/components/shared/mb-modal/test/mb-modal.e2e.ts +++ b/ui/src/components/shared/mb-modal/test/mb-modal.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-modal', () => { diff --git a/ui/src/components/shared/mb-modal/test/mb-modal.spec.tsx b/ui/src/components/shared/mb-modal/test/mb-modal.spec.tsx index 23c5a79..2478eff 100644 --- a/ui/src/components/shared/mb-modal/test/mb-modal.spec.tsx +++ b/ui/src/components/shared/mb-modal/test/mb-modal.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbModal } from '../mb-modal'; diff --git a/ui/src/components/shared/mb-overlay/mb-overlay.scss b/ui/src/components/shared/mb-overlay/mb-overlay.scss index 2a2810d..bdf8b3f 100644 --- a/ui/src/components/shared/mb-overlay/mb-overlay.scss +++ b/ui/src/components/shared/mb-overlay/mb-overlay.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + :host { display: block; width: 100%; diff --git a/ui/src/components/shared/mb-overlay/mb-overlay.tsx b/ui/src/components/shared/mb-overlay/mb-overlay.tsx index 5c1836f..8f5c2bc 100644 --- a/ui/src/components/shared/mb-overlay/mb-overlay.tsx +++ b/ui/src/components/shared/mb-overlay/mb-overlay.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Host, h, Prop } from '@stencil/core'; @Component({ diff --git a/ui/src/components/shared/mb-overlay/test/mb-overlay.e2e.ts b/ui/src/components/shared/mb-overlay/test/mb-overlay.e2e.ts index d75c94c..85f58fb 100644 --- a/ui/src/components/shared/mb-overlay/test/mb-overlay.e2e.ts +++ b/ui/src/components/shared/mb-overlay/test/mb-overlay.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-overlay', () => { diff --git a/ui/src/components/shared/mb-overlay/test/mb-overlay.spec.tsx b/ui/src/components/shared/mb-overlay/test/mb-overlay.spec.tsx index 75bf37c..0047e19 100644 --- a/ui/src/components/shared/mb-overlay/test/mb-overlay.spec.tsx +++ b/ui/src/components/shared/mb-overlay/test/mb-overlay.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbOverlay } from '../mb-overlay'; diff --git a/ui/src/components/shared/mb-screen/mb-screen.scss b/ui/src/components/shared/mb-screen/mb-screen.scss index 34be7ab..7e1f43b 100644 --- a/ui/src/components/shared/mb-screen/mb-screen.scss +++ b/ui/src/components/shared/mb-screen/mb-screen.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + @import "../styles/globals-sass"; :host { diff --git a/ui/src/components/shared/mb-screen/mb-screen.tsx b/ui/src/components/shared/mb-screen/mb-screen.tsx index 19148f8..db4b1d9 100644 --- a/ui/src/components/shared/mb-screen/mb-screen.tsx +++ b/ui/src/components/shared/mb-screen/mb-screen.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Host, h, Prop } from '@stencil/core'; @Component({ diff --git a/ui/src/components/shared/mb-screen/test/mb-screen.e2e.ts b/ui/src/components/shared/mb-screen/test/mb-screen.e2e.ts index 2f423ec..dfb948f 100644 --- a/ui/src/components/shared/mb-screen/test/mb-screen.e2e.ts +++ b/ui/src/components/shared/mb-screen/test/mb-screen.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-screen', () => { diff --git a/ui/src/components/shared/mb-screen/test/mb-screen.spec.tsx b/ui/src/components/shared/mb-screen/test/mb-screen.spec.tsx index 41358cf..cef5fbc 100644 --- a/ui/src/components/shared/mb-screen/test/mb-screen.spec.tsx +++ b/ui/src/components/shared/mb-screen/test/mb-screen.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbScreen } from '../mb-screen'; diff --git a/ui/src/components/shared/mb-spinner/mb-spinner.scss b/ui/src/components/shared/mb-spinner/mb-spinner.scss index c2ca69c..88437af 100644 --- a/ui/src/components/shared/mb-spinner/mb-spinner.scss +++ b/ui/src/components/shared/mb-spinner/mb-spinner.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + @import "../styles/_globals-sass"; :host { @@ -10,7 +14,7 @@ width: 24px; height: 24px; - animation: rotation 700ms linear infinite; + animation: rotation 700ms linear infinite; } } diff --git a/ui/src/components/shared/mb-spinner/mb-spinner.tsx b/ui/src/components/shared/mb-spinner/mb-spinner.tsx index f2ce02a..98e3437 100644 --- a/ui/src/components/shared/mb-spinner/mb-spinner.tsx +++ b/ui/src/components/shared/mb-spinner/mb-spinner.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { Component, Host, h, Prop } from '@stencil/core'; @Component({ diff --git a/ui/src/components/shared/mb-spinner/test/mb-spinner.e2e.ts b/ui/src/components/shared/mb-spinner/test/mb-spinner.e2e.ts index ed2b563..0fad7cd 100644 --- a/ui/src/components/shared/mb-spinner/test/mb-spinner.e2e.ts +++ b/ui/src/components/shared/mb-spinner/test/mb-spinner.e2e.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newE2EPage } from '@stencil/core/testing'; describe('mb-spinner', () => { diff --git a/ui/src/components/shared/mb-spinner/test/mb-spinner.spec.tsx b/ui/src/components/shared/mb-spinner/test/mb-spinner.spec.tsx index ae462e9..3403787 100644 --- a/ui/src/components/shared/mb-spinner/test/mb-spinner.spec.tsx +++ b/ui/src/components/shared/mb-spinner/test/mb-spinner.spec.tsx @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { newSpecPage } from '@stencil/core/testing'; import { MbSpinner } from '../mb-spinner'; diff --git a/ui/src/components/shared/styles/_barcode-rectangle.scss b/ui/src/components/shared/styles/_barcode-rectangle.scss new file mode 100644 index 0000000..61997cd --- /dev/null +++ b/ui/src/components/shared/styles/_barcode-rectangle.scss @@ -0,0 +1,190 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +/* --- Rectangle --- */ + +@import "./_globals-sass"; + +@mixin animation ($delay, $duration, $animation) { + -webkit-animation-delay: $delay; + -webkit-animation-duration: $duration; + -webkit-animation-name: $animation; + + -moz-animation-delay: $delay; + -moz-animation-duration: $duration; + -moz-animation-name: $animation; + + animation-delay: $delay; + animation-duration: $duration; + animation-name: $animation; +} + +$rectangle-shrink-animation-duration: 250ms; +$rectangle-error-animation-duration: 1800ms; +$rectangle-error-animation-duration-extended: 2400ms; +$rectangle-scanning-line-animation-duration: 2400ms; + +// Animations +// Process done animation +@keyframes rectangle-shrink-animation { + 0% { transform: scale(1); } + 50% { transform: scale(.95); } + 100% { transform: scale(1); } +} + +// Scanning line animation +@keyframes scanning-line-animation { + 0% { top: -60%; } + 45% { transform: matrix(1, 0, 0, 1, 0, 0); } + 50% { + top: 120%; + transform: matrix(1, 0, 0, -1, 0, 0); + } + 95% { transform: matrix(1, 0, 0, -1, 0, 0); } + 100% { + top: -60%; + transform: matrix(1, 0, 0, 1, 0, 0); + } +} + +// Shape & states +:host #barcode .rectangle { + width: 100%; + height: 100%; + box-sizing: border-box; + position: relative; + background-color: transparent; + background-position: center; + background-repeat: no-repeat; + transition: all .3s ease-in; + + &__cursor { + width: 100%; + height: 100%; + border-radius: 8px; + position: relative; + } + + &__el { + box-sizing: border-box; + position: absolute; + display: block; + width: 50%; + height: 50%; + overflow: hidden; + + &::after, + &::before { + content: ""; + position: absolute; + + display: block; + width: 32px; + height: 32px; + } + + &:nth-child(1) { + top: 0; + left: 0; + + &::after, + &::before { + top: 0; + left: 0; + border-top: 4px solid rgba(#fff, 0.5); + border-left: 4px solid rgba(#fff, 0.5); + border-top-left-radius: 8px; + box-shadow: inset 3px 3px 8px -6px rgb(0 0 0 / 20%), -3px -3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + + &:nth-child(2) { + top: 0; + right: 0; + + &::after, + &::before { + top: 0; + right: 0; + border-top: 4px solid rgba(#fff, 0.5); + border-right: 4px solid rgba(#fff, 0.5); + border-top-right-radius: 8px; + box-shadow: inset -3px 3px 8px -6px rgb(0 0 0 / 20%), 3px -3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + + &:nth-child(3) { + bottom: 0; + right: 0; + + &::after, + &::before { + bottom: 0; + right: 0; + border-bottom: 4px solid rgba(#fff, 0.5); + border-right: 4px solid rgba(#fff, 0.5); + border-bottom-right-radius: 8px; + box-shadow: inset -3px -3px 8px -6px rgb(0 0 0 / 20%), 3px 3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + + &:nth-child(4) { + bottom: 0; + left: 0; + + &::after, + &::before { + bottom: 0; + left: 0; + border-bottom: 4px solid rgba(#fff, 0.5); + border-left: 4px solid rgba(#fff, 0.5); + border-bottom-left-radius: 8px; + box-shadow: inset 3px -3px 8px -6px rgb(0 0 0 / 20%), -3px 3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + } + + // States + // States labels + &.is-default ~ .label[data-message="is-default"], + &.is-detection ~ .label[data-message="is-detection"], + &.is-classification ~ .label[data-message="is-classification"], + &.is-done ~ .label[data-message="is-done"], + &.is-done-all ~ .label[data-message="is-done-all"], + &.is-flip ~ .label[data-message="is-flip"], + &.is-error-move-farther ~ .label[data-message="is-error-move-farther"], + &.is-error-move-closer ~ .label[data-message="is-error-move-closer"], + &.is-error-adjust-angle ~ .label[data-message="is-error-adjust-angle"] { + opacity: 1; + visibility: visible; + margin: 2 * $base-unit 0 0 0; + } + + // Front side scanning is over + &.is-done, + &.is-done-all { @include animation(0, $rectangle-shrink-animation-duration, rectangle-shrink-animation); } +} + +:host .scanning-line { + opacity: 0; + visibility: hidden; + position: absolute; + width: 100%; + height: 115px; + left: 0px; + top: -125px; + + background: radial-gradient(100% 100% at 49.85% 100%, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%); + filter: blur(4px); + + &.is-active { + opacity: 1; + visibility: visible; + animation: scanning-line-animation $rectangle-scanning-line-animation-duration cubic-bezier(.13,.71,1,.82) infinite; + } +} diff --git a/ui/src/components/shared/styles/_blinkcard-rectangle.scss b/ui/src/components/shared/styles/_blinkcard-rectangle.scss new file mode 100644 index 0000000..4701709 --- /dev/null +++ b/ui/src/components/shared/styles/_blinkcard-rectangle.scss @@ -0,0 +1,405 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +/* --- Rectangle --- */ + +@import "./_globals-sass"; + +@mixin animation ($delay, $duration, $animation) { + -webkit-animation-delay: $delay; + -webkit-animation-duration: $duration; + -webkit-animation-name: $animation; + + -moz-animation-delay: $delay; + -moz-animation-duration: $duration; + -moz-animation-name: $animation; + + animation-delay: $delay; + animation-duration: $duration; + animation-name: $animation; +} + +$rectangle-shrink-animation-duration: 250ms; +$rectangle-error-animation-duration: 1800ms; +$rectangle-error-animation-duration-extended: 2400ms; +$rectangle-scanning-line-animation-duration: 2400ms; + +// Animations +// Process done animation +@keyframes rectangle-shrink-animation { + 0% { + transform: scale(1); + } + 50% { + transform: scale(.95); + } + 100% { + transform: scale(1); + } +} + +// Error animation +// Normal +@keyframes error-animation { + 0% { + width: 32px; + height: 32px; + } + 16% { + width: 100%; + height: 100%; + } + 84% { + width: 100%; + height: 100%; + } + 100% { + width: 32px; + height: 32px; + } +} +// Extended +@keyframes error-animation-extended { + 0% { + width: 32px; + height: 32px; + } + 20% { + width: 100%; + height: 100%; + } + 80% { + width: 100%; + height: 100%; + } + 100% { + width: 32px; + height: 32px; + } +} + +// Scanning line animation +@keyframes scanning-line-animation { + 0% { + top: -60%; + } + 45% { + transform: matrix(1, 0, 0, 1, 0, 0); + } + 50% { + top: 120%; + transform: matrix(1, 0, 0, -1, 0, 0); + } + 95% { + transform: matrix(1, 0, 0, -1, 0, 0); + } + 100% { + top: -60%; + transform: matrix(1, 0, 0, 1, 0, 0); + } +} + +// Flip animations +// Rectangle cursor +@keyframes rectangle-horizontal-flip { + 0% { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjA0IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDIwNCAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2QpIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zMC40ODc5IDIyLjE5NTNDMjYuOTA0NyAyMi4xOTUzIDI0IDI1LjEwMDEgMjQgMjguNjgzM1YxMTMuMDI3QzI0IDExNi42MSAyNi45MDQ3IDExOS41MTUgMzAuNDg3OSAxMTkuNTE1SDE3My4yMjJDMTc2LjgwNSAxMTkuNTE1IDE3OS43MSAxMTYuNjEgMTc5LjcxIDExMy4wMjdWMjguNjgzM0MxNzkuNzEgMjUuMTAwMSAxNzYuODA1IDIyLjE5NTMgMTczLjIyMiAyMi4xOTUzSDMwLjQ4NzlaTTQ1LjQ5MTIgNDQuMDkyMkM0My42OTk2IDQ0LjA5MjIgNDIuMjQ3MyA0NS41NDQ1IDQyLjI0NzMgNDcuMzM2MVY1OS4wOTU2QzQyLjI0NzMgNjAuODg3MiA0My42OTk2IDYyLjMzOTUgNDUuNDkxMiA2Mi4zMzk1SDY4LjE5ODlDNjkuOTkwNSA2Mi4zMzk1IDcxLjQ0MjkgNjAuODg3MiA3MS40NDI5IDU5LjA5NTZWNDcuMzM2MUM3MS40NDI5IDQ1LjU0NDUgNjkuOTkwNSA0NC4wOTIyIDY4LjE5ODkgNDQuMDkyMkg0NS40OTEyWk00Mi4yNDczIDc3Ljc0ODRDNDIuMjQ3MyA3NS45NTY4IDQzLjY5OTYgNzQuNTA0NSA0NS40OTEyIDc0LjUwNDVIMTU4LjIxOUMxNjAuMDEgNzQuNTA0NSAxNjEuNDYzIDc1Ljk1NjggMTYxLjQ2MyA3Ny43NDg0Vjc4LjU1OTRDMTYxLjQ2MyA4MC4zNTEgMTYwLjAxIDgxLjgwMzQgMTU4LjIxOSA4MS44MDM0SDQ1LjQ5MTJDNDMuNjk5NiA4MS44MDM0IDQyLjI0NzMgODAuMzUxIDQyLjI0NzMgNzguNTU5NFY3Ny43NDg0Wk00NS40OTEyIDkwLjMxODlDNDMuNjk5NiA5MC4zMTg5IDQyLjI0NzMgOTEuNzcxMiA0Mi4yNDczIDkzLjU2MjhWOTQuMzczOEM0Mi4yNDczIDk2LjE2NTQgNDMuNjk5NiA5Ny42MTc4IDQ1LjQ5MTIgOTcuNjE3OEgxMDMuNDc3QzEwNS4yNjkgOTcuNjE3OCAxMDYuNzIxIDk2LjE2NTQgMTA2LjcyMSA5NC4zNzM4VjkzLjU2MjhDMTA2LjcyMSA5MS43NzEyIDEwNS4yNjkgOTAuMzE4OSAxMDMuNDc3IDkwLjMxODlINDUuNDkxMloiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZCIgeD0iMCIgeT0iMC4xOTUzMTIiIHdpZHRoPSIyMDMuNzEiIGhlaWdodD0iMTQ1LjMxOSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIvPgo8ZmVPZmZzZXQgZHk9IjIiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMTIiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=); + opacity: 0; + } + + 5% { + opacity: 1; + } + + 15% { + transform: rotateY(0deg); + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjA0IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDIwNCAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2QpIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zMC40ODc5IDIyLjE5NTNDMjYuOTA0NyAyMi4xOTUzIDI0IDI1LjEwMDEgMjQgMjguNjgzM1YxMTMuMDI3QzI0IDExNi42MSAyNi45MDQ3IDExOS41MTUgMzAuNDg3OSAxMTkuNTE1SDE3My4yMjJDMTc2LjgwNSAxMTkuNTE1IDE3OS43MSAxMTYuNjEgMTc5LjcxIDExMy4wMjdWMjguNjgzM0MxNzkuNzEgMjUuMTAwMSAxNzYuODA1IDIyLjE5NTMgMTczLjIyMiAyMi4xOTUzSDMwLjQ4NzlaTTQ1LjQ5MTIgNDQuMDkyMkM0My42OTk2IDQ0LjA5MjIgNDIuMjQ3MyA0NS41NDQ1IDQyLjI0NzMgNDcuMzM2MVY1OS4wOTU2QzQyLjI0NzMgNjAuODg3MiA0My42OTk2IDYyLjMzOTUgNDUuNDkxMiA2Mi4zMzk1SDY4LjE5ODlDNjkuOTkwNSA2Mi4zMzk1IDcxLjQ0MjkgNjAuODg3MiA3MS40NDI5IDU5LjA5NTZWNDcuMzM2MUM3MS40NDI5IDQ1LjU0NDUgNjkuOTkwNSA0NC4wOTIyIDY4LjE5ODkgNDQuMDkyMkg0NS40OTEyWk00Mi4yNDczIDc3Ljc0ODRDNDIuMjQ3MyA3NS45NTY4IDQzLjY5OTYgNzQuNTA0NSA0NS40OTEyIDc0LjUwNDVIMTU4LjIxOUMxNjAuMDEgNzQuNTA0NSAxNjEuNDYzIDc1Ljk1NjggMTYxLjQ2MyA3Ny43NDg0Vjc4LjU1OTRDMTYxLjQ2MyA4MC4zNTEgMTYwLjAxIDgxLjgwMzQgMTU4LjIxOSA4MS44MDM0SDQ1LjQ5MTJDNDMuNjk5NiA4MS44MDM0IDQyLjI0NzMgODAuMzUxIDQyLjI0NzMgNzguNTU5NFY3Ny43NDg0Wk00NS40OTEyIDkwLjMxODlDNDMuNjk5NiA5MC4zMTg5IDQyLjI0NzMgOTEuNzcxMiA0Mi4yNDczIDkzLjU2MjhWOTQuMzczOEM0Mi4yNDczIDk2LjE2NTQgNDMuNjk5NiA5Ny42MTc4IDQ1LjQ5MTIgOTcuNjE3OEgxMDMuNDc3QzEwNS4yNjkgOTcuNjE3OCAxMDYuNzIxIDk2LjE2NTQgMTA2LjcyMSA5NC4zNzM4VjkzLjU2MjhDMTA2LjcyMSA5MS43NzEyIDEwNS4yNjkgOTAuMzE4OSAxMDMuNDc3IDkwLjMxODlINDUuNDkxMloiIGZpbGw9IndoaXRlIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZCIgeD0iMCIgeT0iMC4xOTUzMTIiIHdpZHRoPSIyMDMuNzEiIGhlaWdodD0iMTQ1LjMxOSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPgo8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPgo8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlQWxwaGEiIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIvPgo8ZmVPZmZzZXQgZHk9IjIiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iMTIiLz4KPGZlQ29sb3JNYXRyaXggdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAuMSAwIi8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93Ii8+CjxmZUJsZW5kIG1vZGU9Im5vcm1hbCIgaW49IlNvdXJjZUdyYXBoaWMiIGluMj0iZWZmZWN0MV9kcm9wU2hhZG93IiByZXN1bHQ9InNoYXBlIi8+CjwvZmlsdGVyPgo8L2RlZnM+Cjwvc3ZnPgo=); + } + + 20% { + // Back image + transform: rotateY(90deg); + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjA0IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDIwNCAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2QpIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNzMuMjIyIDIyLjE5NTNDMTc2LjgwNSAyMi4xOTUzIDE3OS43MSAyNS4xMDAxIDE3OS43MSAyOC42ODMzVjExMy4wMjdDMTc5LjcxIDExNi42MSAxNzYuODA1IDExOS41MTUgMTczLjIyMiAxMTkuNTE1SDMwLjQ4NzlDMjYuOTA0NyAxMTkuNTE1IDIzLjk5OTkgMTE2LjYxIDIzLjk5OTkgMTEzLjAyN1YyOC42ODMzQzIzLjk5OTkgMjUuMTAwMSAyNi45MDQ3IDIyLjE5NTMgMzAuNDg3OSAyMi4xOTUzSDE3My4yMjJaTTE1OC4yMTkgNDQuMDkyMkMxNjAuMDEgNDQuMDkyMiAxNjEuNDYzIDQ1LjU0NDUgMTYxLjQ2MyA0Ny4zMzYxVjU5LjA5NTZDMTYxLjQ2MyA2MC44ODcyIDE2MC4wMSA2Mi4zMzk1IDE1OC4yMTkgNjIuMzM5NUgxMzUuNTExQzEzMy43MTkgNjIuMzM5NSAxMzIuMjY3IDYwLjg4NzIgMTMyLjI2NyA1OS4wOTU2VjQ3LjMzNjFDMTMyLjI2NyA0NS41NDQ1IDEzMy43MTkgNDQuMDkyMiAxMzUuNTExIDQ0LjA5MjJIMTU4LjIxOVpNMTYxLjQ2MyA3Ny43NDg0QzE2MS40NjMgNzUuOTU2OCAxNjAuMDEgNzQuNTA0NSAxNTguMjE5IDc0LjUwNDVINDUuNDkxMkM0My42OTk2IDc0LjUwNDUgNDIuMjQ3MiA3NS45NTY4IDQyLjI0NzIgNzcuNzQ4NFY3OC41NTk0QzQyLjI0NzIgODAuMzUxIDQzLjY5OTYgODEuODAzNCA0NS40OTEyIDgxLjgwMzRIMTU4LjIxOUMxNjAuMDEgODEuODAzNCAxNjEuNDYzIDgwLjM1MSAxNjEuNDYzIDc4LjU1OTRWNzcuNzQ4NFpNMTU4LjIxOSA5MC4zMTg5QzE2MC4wMSA5MC4zMTg5IDE2MS40NjMgOTEuNzcxMiAxNjEuNDYzIDkzLjU2MjhWOTQuMzczOEMxNjEuNDYzIDk2LjE2NTQgMTYwLjAxIDk3LjYxNzggMTU4LjIxOSA5Ny42MTc4SDEwMC4yMzNDOTguNDQxNCA5Ny42MTc4IDk2Ljk4OSA5Ni4xNjU0IDk2Ljk4OSA5NC4zNzM4VjkzLjU2MjhDOTYuOTg5IDkxLjc3MTIgOTguNDQxNCA5MC4zMTg5IDEwMC4yMzMgOTAuMzE4OUgxNTguMjE5WiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kIiB4PSIwIiB5PSIwLjE5NTMxMiIgd2lkdGg9IjIwMy43MSIgaGVpZ2h0PSIxNDUuMzE5IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIi8+CjxmZU9mZnNldCBkeT0iMiIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxMiIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xIDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3ciLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3ciIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjwvZGVmcz4KPC9zdmc+Cg==); + } + + 25% { + transform: rotateY(-15deg); + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjA0IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDIwNCAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2QpIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNzMuMjIyIDIyLjE5NTNDMTc2LjgwNSAyMi4xOTUzIDE3OS43MSAyNS4xMDAxIDE3OS43MSAyOC42ODMzVjExMy4wMjdDMTc5LjcxIDExNi42MSAxNzYuODA1IDExOS41MTUgMTczLjIyMiAxMTkuNTE1SDMwLjQ4NzlDMjYuOTA0NyAxMTkuNTE1IDIzLjk5OTkgMTE2LjYxIDIzLjk5OTkgMTEzLjAyN1YyOC42ODMzQzIzLjk5OTkgMjUuMTAwMSAyNi45MDQ3IDIyLjE5NTMgMzAuNDg3OSAyMi4xOTUzSDE3My4yMjJaTTE1OC4yMTkgNDQuMDkyMkMxNjAuMDEgNDQuMDkyMiAxNjEuNDYzIDQ1LjU0NDUgMTYxLjQ2MyA0Ny4zMzYxVjU5LjA5NTZDMTYxLjQ2MyA2MC44ODcyIDE2MC4wMSA2Mi4zMzk1IDE1OC4yMTkgNjIuMzM5NUgxMzUuNTExQzEzMy43MTkgNjIuMzM5NSAxMzIuMjY3IDYwLjg4NzIgMTMyLjI2NyA1OS4wOTU2VjQ3LjMzNjFDMTMyLjI2NyA0NS41NDQ1IDEzMy43MTkgNDQuMDkyMiAxMzUuNTExIDQ0LjA5MjJIMTU4LjIxOVpNMTYxLjQ2MyA3Ny43NDg0QzE2MS40NjMgNzUuOTU2OCAxNjAuMDEgNzQuNTA0NSAxNTguMjE5IDc0LjUwNDVINDUuNDkxMkM0My42OTk2IDc0LjUwNDUgNDIuMjQ3MiA3NS45NTY4IDQyLjI0NzIgNzcuNzQ4NFY3OC41NTk0QzQyLjI0NzIgODAuMzUxIDQzLjY5OTYgODEuODAzNCA0NS40OTEyIDgxLjgwMzRIMTU4LjIxOUMxNjAuMDEgODEuODAzNCAxNjEuNDYzIDgwLjM1MSAxNjEuNDYzIDc4LjU1OTRWNzcuNzQ4NFpNMTU4LjIxOSA5MC4zMTg5QzE2MC4wMSA5MC4zMTg5IDE2MS40NjMgOTEuNzcxMiAxNjEuNDYzIDkzLjU2MjhWOTQuMzczOEMxNjEuNDYzIDk2LjE2NTQgMTYwLjAxIDk3LjYxNzggMTU4LjIxOSA5Ny42MTc4SDEwMC4yMzNDOTguNDQxNCA5Ny42MTc4IDk2Ljk4OSA5Ni4xNjU0IDk2Ljk4OSA5NC4zNzM4VjkzLjU2MjhDOTYuOTg5IDkxLjc3MTIgOTguNDQxNCA5MC4zMTg5IDEwMC4yMzMgOTAuMzE4OUgxNTguMjE5WiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kIiB4PSIwIiB5PSIwLjE5NTMxMiIgd2lkdGg9IjIwMy43MSIgaGVpZ2h0PSIxNDUuMzE5IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIi8+CjxmZU9mZnNldCBkeT0iMiIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxMiIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xIDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3ciLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3ciIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjwvZGVmcz4KPC9zdmc+Cg==); + } + + 30% { + transform: rotateY(0deg); + } + + 95% { + opacity: 1; + } + + 100% { + background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjA0IiBoZWlnaHQ9IjE0NiIgdmlld0JveD0iMCAwIDIwNCAxNDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2QpIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNzMuMjIyIDIyLjE5NTNDMTc2LjgwNSAyMi4xOTUzIDE3OS43MSAyNS4xMDAxIDE3OS43MSAyOC42ODMzVjExMy4wMjdDMTc5LjcxIDExNi42MSAxNzYuODA1IDExOS41MTUgMTczLjIyMiAxMTkuNTE1SDMwLjQ4NzlDMjYuOTA0NyAxMTkuNTE1IDIzLjk5OTkgMTE2LjYxIDIzLjk5OTkgMTEzLjAyN1YyOC42ODMzQzIzLjk5OTkgMjUuMTAwMSAyNi45MDQ3IDIyLjE5NTMgMzAuNDg3OSAyMi4xOTUzSDE3My4yMjJaTTE1OC4yMTkgNDQuMDkyMkMxNjAuMDEgNDQuMDkyMiAxNjEuNDYzIDQ1LjU0NDUgMTYxLjQ2MyA0Ny4zMzYxVjU5LjA5NTZDMTYxLjQ2MyA2MC44ODcyIDE2MC4wMSA2Mi4zMzk1IDE1OC4yMTkgNjIuMzM5NUgxMzUuNTExQzEzMy43MTkgNjIuMzM5NSAxMzIuMjY3IDYwLjg4NzIgMTMyLjI2NyA1OS4wOTU2VjQ3LjMzNjFDMTMyLjI2NyA0NS41NDQ1IDEzMy43MTkgNDQuMDkyMiAxMzUuNTExIDQ0LjA5MjJIMTU4LjIxOVpNMTYxLjQ2MyA3Ny43NDg0QzE2MS40NjMgNzUuOTU2OCAxNjAuMDEgNzQuNTA0NSAxNTguMjE5IDc0LjUwNDVINDUuNDkxMkM0My42OTk2IDc0LjUwNDUgNDIuMjQ3MiA3NS45NTY4IDQyLjI0NzIgNzcuNzQ4NFY3OC41NTk0QzQyLjI0NzIgODAuMzUxIDQzLjY5OTYgODEuODAzNCA0NS40OTEyIDgxLjgwMzRIMTU4LjIxOUMxNjAuMDEgODEuODAzNCAxNjEuNDYzIDgwLjM1MSAxNjEuNDYzIDc4LjU1OTRWNzcuNzQ4NFpNMTU4LjIxOSA5MC4zMTg5QzE2MC4wMSA5MC4zMTg5IDE2MS40NjMgOTEuNzcxMiAxNjEuNDYzIDkzLjU2MjhWOTQuMzczOEMxNjEuNDYzIDk2LjE2NTQgMTYwLjAxIDk3LjYxNzggMTU4LjIxOSA5Ny42MTc4SDEwMC4yMzNDOTguNDQxNCA5Ny42MTc4IDk2Ljk4OSA5Ni4xNjU0IDk2Ljk4OSA5NC4zNzM4VjkzLjU2MjhDOTYuOTg5IDkxLjc3MTIgOTguNDQxNCA5MC4zMTg5IDEwMC4yMzMgOTAuMzE4OUgxNTguMjE5WiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGZpbHRlciBpZD0iZmlsdGVyMF9kIiB4PSIwIiB5PSIwLjE5NTMxMiIgd2lkdGg9IjIwMy43MSIgaGVpZ2h0PSIxNDUuMzE5IiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiIGNvbG9yLWludGVycG9sYXRpb24tZmlsdGVycz0ic1JHQiI+CjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+CjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDEyNyAwIi8+CjxmZU9mZnNldCBkeT0iMiIvPgo8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIxMiIvPgo8ZmVDb2xvck1hdHJpeCB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4xIDAiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3ciLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3ciIHJlc3VsdD0ic2hhcGUiLz4KPC9maWx0ZXI+CjwvZGVmcz4KPC9zdmc+Cg==); + opacity: 0; + } +} + +// Shape & states +:host #blinkcard .rectangle { + box-sizing: border-box; + position: relative; + background-color: transparent; + background-position: center; + background-repeat: no-repeat; + transition: all .3s ease-in; + order: 1; + flex: 0 1 327px; + + &__cursor { + width: calc(100% + 4px); + height: calc(100% + 4px); + border-radius: 8px; + position: relative; + top: -2px; + left: -2px; + overflow: hidden; + } + + &__el { + box-sizing: border-box; + position: absolute; + display: block; + width: 50%; + height: 50%; + overflow: hidden; + + &::after, + &::before { + content: ""; + position: absolute; + + display: block; + width: 32px; + height: 32px; + } + + &:nth-child(1) { + top: 0; + left: 0; + + &::after, + &::before { + top: 0; + left: 0; + border-top: 4px solid rgba(#fff, 0.5); + border-left: 4px solid rgba(#fff, 0.5); + border-top-left-radius: 8px; + box-shadow: inset 3px 3px 8px -6px rgb(0 0 0 / 20%), -3px -3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + + &:nth-child(2) { + top: 0; + right: 0; + + &::after, + &::before { + top: 0; + right: 0; + border-top: 4px solid rgba(#fff, 0.5); + border-right: 4px solid rgba(#fff, 0.5); + border-top-right-radius: 8px; + box-shadow: inset -3px 3px 8px -6px rgb(0 0 0 / 20%), 3px -3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + + &:nth-child(3) { + bottom: 0; + right: 0; + + &::after, + &::before { + bottom: 0; + right: 0; + border-bottom: 4px solid rgba(#fff, 0.5); + border-right: 4px solid rgba(#fff, 0.5); + border-bottom-right-radius: 8px; + box-shadow: inset -3px -3px 8px -6px rgb(0 0 0 / 20%), 3px 3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + + &:nth-child(4) { + bottom: 0; + left: 0; + + &::after, + &::before { + bottom: 0; + left: 0; + border-bottom: 4px solid rgba(#fff, 0.5); + border-left: 4px solid rgba(#fff, 0.5); + border-bottom-left-radius: 8px; + box-shadow: inset 3px -3px 8px -6px rgb(0 0 0 / 20%), -3px 3px 8px -6px rgb(0 0 0 / 20%); + transition: border-color .15s linear; + } + } + } + + // States + + // States labels + &.is-default ~ .label[data-message="is-default"], + &.is-detection ~ .label[data-message="is-detection"], + &.is-classification ~ .label[data-message="is-classification"], + &.is-done ~ .label[data-message="is-done"], + &.is-done-all ~ .label[data-message="is-done-all"], + &.is-flip ~ .label[data-message="is-flip"], + &.is-error-move-farther ~ .label[data-message="is-error-move-farther"], + &.is-error-move-closer ~ .label[data-message="is-error-move-closer"], + &.is-error-adjust-angle ~ .label[data-message="is-error-adjust-angle"] { + opacity: 1; + visibility: visible; + margin: 2 * $base-unit 0 0 0; + } + + &.is-flip { + .rectangle__el { display: none }; + background: rgba(0, 0, 0, 0.2); + + .rectangle__cursor { + border-radius: 0; + background-color: transparent; + background-size: auto; + background-repeat: no-repeat; + background-position: center; + + -webkit-backdrop-filter: none; + backdrop-filter: none; + filter: drop-shadow(0px 2px 24px rgba(0, 0, 0, 0.1), 0px 2px 8px rgba(0, 0, 0, 0.05)); + + transform: rotate3d(0); + transform-style: preserve-3d; + + animation: rectangle-horizontal-flip 3.5s cubic-bezier(0.4, 0.02, 1, 1) both .5s; + } + } + + // Front side scanning is over + &.is-done, + &.is-done-all { + @include animation(0, $rectangle-shrink-animation-duration, rectangle-shrink-animation); + } + + &.is-error-move-farther, + &.is-error-move-closer, + &.is-error-adjust-angle { + .rectangle { + &__el { + &:nth-child(1) { + &::after, + &::before { + border-top: 4px solid #FF2D55; + border-left: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration 0s error-animation ease-in; + } + } + + &:nth-child(2) { + &::after, + &::before { + border-top: 4px solid #FF2D55; + border-right: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration 0s error-animation ease-in; + } + } + + &:nth-child(3) { + &::after, + &::before { + border-bottom: 4px solid #FF2D55; + border-right: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration 0s error-animation ease-in; + } + } + + &:nth-child(4) { + &::after, + &::before { + border-bottom: 4px solid #FF2D55; + border-left: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration 0s error-animation ease-in; + } + } + } + } + } +} + +:host .scanning-line { + opacity: 0; + visibility: hidden; + position: absolute; + width: 100%; + height: 115px; + left: 0px; + top: -125px; + + background: radial-gradient(100% 100% at 49.85% 100%, #FFFFFF 0%, rgba(255, 255, 255, 0) 100%); + filter: blur(4px); + + &.is-active { + opacity: 1; + visibility: visible; + animation: scanning-line-animation $rectangle-scanning-line-animation-duration cubic-bezier(.13,.71,1,.82) infinite; + } +} + +// Laptop screens 2 and beyond ( >1440px ) +@media only screen and (min-width: $breakpoint-width-laptop-1440) { + :host .rectangle { + &.is-error-move-farther, + &.is-error-move-closer, + &.is-error-adjust-angle { + .rectangle { + &__el { + &:nth-child(1) { + &::after, + &::before { + border-top: 4px solid #FF2D55; + border-left: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration-extended 0s error-animation-extended ease-in !important; + } + } + + &:nth-child(2) { + &::after, + &::before { + border-top: 4px solid #FF2D55; + border-right: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration-extended 0s error-animation-extended ease-in !important; + } + } + + &:nth-child(3) { + &::after, + &::before { + border-bottom: 4px solid #FF2D55; + border-right: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration-extended 0s error-animation-extended ease-in !important; + } + } + + &:nth-child(4) { + &::after, + &::before { + border-bottom: 4px solid #FF2D55; + border-left: 4px solid #FF2D55; + animation: $rectangle-error-animation-duration-extended 0s error-animation-extended ease-in !important; + } + } + } + } + } + } +} diff --git a/ui/src/components/shared/styles/_globals-sass.scss b/ui/src/components/shared/styles/_globals-sass.scss index d2d6ece..f5b57de 100644 --- a/ui/src/components/shared/styles/_globals-sass.scss +++ b/ui/src/components/shared/styles/_globals-sass.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + /** * SASS variables, not customizable via CSS variables */ @@ -8,9 +12,13 @@ $padding-unit-large: 16px; $button-size: 40px; $button-icon-size: 20px; -$breakpoint-width-tablet: 768px; -$breakpoint-width-desktop: 1280px; $breakpoint-width-mobile-landscape: 568px; +$breakpoint-width-tablet: 768px; +$breakpoint-width-tablet-landscape: 1024px; +$breakpoint-width-laptop-1280: 1280px; +$breakpoint-width-laptop-1440: 1440px; +$breakpoint-width-desktop: 1920px; + /** * Camera experiences diff --git a/ui/src/components/shared/styles/_globals.scss b/ui/src/components/shared/styles/_globals.scss index ccdfa85..075e1f4 100644 --- a/ui/src/components/shared/styles/_globals.scss +++ b/ui/src/components/shared/styles/_globals.scss @@ -1,3 +1,9 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + +@import "./globals-sass"; + /** * CSS variables * @@ -26,6 +32,8 @@ --mb-component-box-shadow: none; + --mb-component-button-size: #{$button-size}; + --mb-component-button-icon-size: #{$button-icon-size}; --mb-component-button-background: #FFF; --mb-component-button-border-color: rgba(120, 120, 128, 0.2); --mb-component-button-border-color-focus: rgba(72, 178, 232, 0.5); @@ -38,6 +46,12 @@ --mb-component-button-box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.1); --mb-component-button-box-shadow-disabled: none; + --mb-component-action-buttons-justify-content: flex-end; + --mb-component-action-buttons-width: 2 * #{$button-size} + #{$padding-unit-medium}; + --mb-component-action-buttons-last-margin: 0 0 0 8px; + + --mb-component-action-label: block; + /* User feedback (messages below buttons) */ --mb-feedback-font-color-error: #FF2D55; --mb-feedback-font-color-info: rgba(60, 60, 67, 0.7); diff --git a/ui/src/components/shared/styles/_reticle.scss b/ui/src/components/shared/styles/_reticle.scss index b5cbc3c..3701bd6 100644 --- a/ui/src/components/shared/styles/_reticle.scss +++ b/ui/src/components/shared/styles/_reticle.scss @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + /* --- Reticle --- */ $angle: 67.5; @@ -352,7 +356,10 @@ $reticle-bg: ( } // Front side scanning is over (BlinkID combined) - &.is-done, + &.is-done { + display: none; + } + &.is-done-all { background-color: map-get(map-get(map-get($base-colors, background-primary), onlight), foreground); box-shadow: 0px 2px 24px rgba(0, 0, 0, 0.1), 0px 2px 8px rgba(0, 0, 0, 0.05); diff --git a/ui/src/index.ts b/ui/src/index.ts index 07635cb..0aee50d 100644 --- a/ui/src/index.ts +++ b/ui/src/index.ts @@ -1 +1,5 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + export * from './components'; diff --git a/ui/src/utils/data-structures.ts b/ui/src/utils/data-structures.ts index be72ee0..d8ff6ca 100644 --- a/ui/src/utils/data-structures.ts +++ b/ui/src/utils/data-structures.ts @@ -1,17 +1,21 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import { EventEmitter } from '@stencil/core'; -import * as PhotoPaySDK from "../../../es/photopay-sdk"; +import * as PhotoPaySDK from '../../../es/photopay-sdk'; export interface MicroblinkUI { // SDK settings allowHelloMessage: boolean; engineLocation: string; licenseKey: string; + wasmType: string; rawRecognizers: string; recognizers: Array; - rawRecognizerOptions: string; - recognizerOptions: Array; - includeSuccessFrame: boolean; + recognizerOptions: { [key: string]: any }; + includeSuccessFrame?: boolean; // Functional properties enableDrag: boolean; @@ -23,23 +27,33 @@ export interface MicroblinkUI { // UI customization translations: { [key: string]: string }; rawTranslations: string; + showActionLabels: boolean; + showModalWindows: boolean; iconCameraDefault: string; iconCameraActive: string; iconGalleryDefault: string; iconGalleryActive: string; iconInvalidFormat: string; - iconSpinner: string; + iconSpinnerScreenLoading: string; + iconSpinnerFromGalleryExperience: string; // Events fatalError: EventEmitter; ready: EventEmitter; scanError: EventEmitter; scanSuccess: EventEmitter; + cameraScanStarted: EventEmitter; + imageScanStarted: EventEmitter; + + // Methods + setUiState: (state: 'ERROR' | 'LOADING' | 'NONE' | 'SUCCESS') => Promise; + setUiMessage: (state: 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK', message: string) => Promise; } export interface SdkSettings { allowHelloMessage: boolean; engineLocation: string; + wasmType?: PhotoPaySDK.WasmType; } /** @@ -69,26 +83,31 @@ export class EventReady { } export class EventScanError { - code: Code; - fatal: boolean; - message: string; + code: Code; + fatal: boolean; + message: string; + recognizerName: string; - constructor(code: Code, fatal: boolean, message: string) { + constructor(code: Code, fatal: boolean, message: string, recognizerName: string) { this.code = code; this.fatal = fatal; this.message = message; + this.recognizerName = recognizerName; } } export class EventScanSuccess { recognizer: PhotoPaySDK.RecognizerResult; + recognizerName: string; successFrame?: PhotoPaySDK.SuccessFrameGrabberRecognizerResult; constructor( recognizer: PhotoPaySDK.RecognizerResult, + recognizerName: string, successFrame?: PhotoPaySDK.SuccessFrameGrabberRecognizerResult ) { this.recognizer = recognizer; + this.recognizerName = recognizerName; if (successFrame) { this.successFrame = successFrame; @@ -137,26 +156,26 @@ export const AvailableRecognizers: { [key: string]: string } = { SlovakiaDataMatrixPaymentRecognizer: 'createSlovakiaDataMatrixPaymentRecognizer', SlovakiaQrCodePaymentRecognizer: 'createSlovakiaQrCodePaymentRecognizer', SloveniaQrCodePaymentRecognizer: 'createSloveniaQrCodePaymentRecognizer', - SwitzerlandQrCodePaymentRecognizer: 'createSwitzerlandQrCodePaymentRecognizer' -} - -export const AvailableRecognizerOptions: { [key: string]: Array } = { + SwitzerlandQrCodePaymentRecognizer: 'createSwitzerlandQrCodePaymentRecognizer', } export interface VideoRecognitionConfiguration { recognizers: Array, - recognizerOptions?: Array, + recognizerOptions?: any, successFrame: boolean, - cameraFeed: HTMLVideoElement + cameraFeed: HTMLVideoElement, + cameraId: string | null; } export interface ImageRecognitionConfiguration { recognizers: Array, - recognizerOptions?: Array, + recognizerOptions?: any, + thoroughScan?: boolean, fileList: FileList } export interface RecognizerInstance { + name: string, recognizer: PhotoPaySDK.Recognizer & { objectHandle: number }, successFrame?: PhotoPaySDK.SuccessFrameGrabberRecognizer & { objectHandle?: number } } @@ -200,14 +219,17 @@ export interface RecognitionEvent { } export interface RecognitionResults { - recognizer: PhotoPaySDK.RecognizerResult, - successFrame?: PhotoPaySDK.SuccessFrameGrabberRecognizerResult + recognizer: PhotoPaySDK.RecognizerResult, + recognizerName: string, + successFrame?: PhotoPaySDK.SuccessFrameGrabberRecognizerResult, + imageCapture?: boolean } export enum CameraExperience { Barcode = 'BARCODE', CardCombined = 'CARD_COMBINED', - CardSingleSide = 'CARD_SINGLE_SIDE' + CardSingleSide = 'CARD_SINGLE_SIDE', + BlinkCard = 'BLINKCARD' } export enum CameraExperienceState { @@ -228,8 +250,8 @@ export const CameraExperienceStateDuration = new Map([ [ CameraExperienceState.Done, 300 ], [ CameraExperienceState.DoneAll, 400 ], [ CameraExperienceState.Flip, 3500 ], - [ CameraExperienceState.MoveCloser, 2000 ], - [ CameraExperienceState.MoveFarther, 2000 ] + [ CameraExperienceState.MoveCloser, 2500 ], + [ CameraExperienceState.MoveFarther, 2500 ] ]); export enum CameraExperienceReticleAnimation { @@ -251,19 +273,8 @@ export enum FeedbackCode { ScanSuccessful = 'SCAN_SUCCESSFUL' } -export enum FeedbackState { - Error = 'ERROR_FEEDBACK', - Info = 'INFO_FEEDBACK', - Ok = 'OK' -} - export interface FeedbackMessage { - code: FeedbackCode; - state: FeedbackState; - message: string; + code? : FeedbackCode; + state : 'FEEDBACK_ERROR' | 'FEEDBACK_INFO' | 'FEEDBACK_OK'; + message : string; } - -export interface ModalContent { - title: string; - body: string; -} \ No newline at end of file diff --git a/ui/src/utils/device.helpers.ts b/ui/src/utils/device.helpers.ts index 993f992..8205746 100644 --- a/ui/src/utils/device.helpers.ts +++ b/ui/src/utils/device.helpers.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import * as PhotoPaySDK from "../../../es/photopay-sdk"; export function hasVideoDevices(): Promise { diff --git a/ui/src/utils/generic.helpers.ts b/ui/src/utils/generic.helpers.ts index 226534f..a5e69ec 100644 --- a/ui/src/utils/generic.helpers.ts +++ b/ui/src/utils/generic.helpers.ts @@ -1,3 +1,7 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + export function stringToArray(inputString: string): Array { if (!inputString || !inputString.length) { return []; diff --git a/ui/src/utils/sdk.service.ts b/ui/src/utils/sdk.service.ts index e707027..f8544da 100644 --- a/ui/src/utils/sdk.service.ts +++ b/ui/src/utils/sdk.service.ts @@ -1,8 +1,11 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + import * as PhotoPaySDK from "../../../es/photopay-sdk"; import { AvailableRecognizers, - AvailableRecognizerOptions, CameraExperience, Code, EventFatalError, @@ -28,6 +31,10 @@ export class SdkService { private cancelInitiatedFromOutside: boolean = false; + private recognizerName: string; + + private videoRecognizer: PhotoPaySDK.VideoRecognizer; + public showOverlay: boolean = false; constructor() { @@ -40,6 +47,10 @@ export class SdkService { loadSettings.allowHelloMessage = sdkSettings.allowHelloMessage; loadSettings.engineLocation = sdkSettings.engineLocation; + if (sdkSettings.wasmType) { + loadSettings.wasmType = sdkSettings.wasmType; + } + return new Promise((resolve) => { PhotoPaySDK.loadWasmModule(loadSettings) .then((sdk: PhotoPaySDK.WasmSDK) => { @@ -76,40 +87,7 @@ export class SdkService { } } - public checkRecognizerOptions(recognizers: Array, recognizerOptions: Array): CheckConclusion { - if (!recognizerOptions || !recognizerOptions.length) { - return { - status: true - } - } - - for (const recognizerOption of recognizerOptions) { - let optionExistInProvidedRecognizers = false; - - for (const recognizer of recognizers) { - const availableOptions = AvailableRecognizerOptions[recognizer]; - - if (availableOptions.indexOf(recognizerOption) > -1) { - optionExistInProvidedRecognizers = true; - break; - } - } - - if (!optionExistInProvidedRecognizers) { - return { - status: false, - message: `Recognizer option "${ recognizerOption }" is not supported by available recognizers!` - } - } - } - - return { - status: true - } - } - - public getDesiredCameraExperience(recognizers: Array): CameraExperience { - for (let i = 0; i < recognizers.length; ++i) {} + public getDesiredCameraExperience(_recognizers: Array = [], _recognizerOptions: any = {}): CameraExperience { return CameraExperience.Barcode; } @@ -119,6 +97,8 @@ export class SdkService { ): Promise { eventCallback({ status: RecognitionStatus.Preparing }); + this.cancelInitiatedFromOutside = false; + const recognizers = await this.createRecognizers( configuration.recognizers, configuration.recognizerOptions, @@ -131,16 +111,17 @@ export class SdkService { ); try { - const videoRecognizer = await PhotoPaySDK.VideoRecognizer.createVideoRecognizerFromCameraStream( + this.videoRecognizer = await PhotoPaySDK.VideoRecognizer.createVideoRecognizerFromCameraStream( configuration.cameraFeed, - recognizerRunner + recognizerRunner, + configuration.cameraId ); - await videoRecognizer.setVideoRecognitionMode(PhotoPaySDK.VideoRecognitionMode.Recognition); + await this.videoRecognizer.setVideoRecognitionMode(PhotoPaySDK.VideoRecognitionMode.Recognition); this.eventEmitter$.addEventListener('terminate', async () => { - if (videoRecognizer && typeof videoRecognizer.cancelRecognition === 'function') { - videoRecognizer.cancelRecognition(); + if (this.videoRecognizer && typeof this.videoRecognizer.cancelRecognition === 'function') { + this.videoRecognizer.cancelRecognition(); } if (recognizerRunner) { @@ -174,29 +155,36 @@ export class SdkService { } window.setTimeout(() => { - if (videoRecognizer) { - videoRecognizer.releaseVideoFeed(); + if (this.videoRecognizer) { + this.videoRecognizer.releaseVideoFeed(); } }, 1); }); - videoRecognizer.startRecognition( + this.videoRecognizer.startRecognition( async (recognitionState: PhotoPaySDK.RecognizerResultState) => { - videoRecognizer.pauseRecognition(); + this.videoRecognizer.pauseRecognition(); eventCallback({ status: RecognitionStatus.Processing }); if (recognitionState !== PhotoPaySDK.RecognizerResultState.Empty) { for (const recognizer of recognizers) { const results = await recognizer.recognizer.getResult(); + this.recognizerName = recognizer.recognizer.recognizerName; if (!results || results.state === PhotoPaySDK.RecognizerResultState.Empty) { eventCallback({ status: RecognitionStatus.EmptyResultState, - data: { initiatedByUser: this.cancelInitiatedFromOutside } + data: { + initiatedByUser: this.cancelInitiatedFromOutside, + recognizerName: this.recognizerName + } }); } else { - const recognitionResults: RecognitionResults = { recognizer: results } + const recognitionResults: RecognitionResults = { + recognizer: results, + recognizerName: this.recognizerName + } if (recognizer.successFrame) { const successFrameResults = await recognizer.successFrame.getResult(); @@ -208,7 +196,11 @@ export class SdkService { eventCallback({ status: RecognitionStatus.ScanSuccessful, - data: recognitionResults + data: { + result: recognitionResults, + initiatedByUser: this.cancelInitiatedFromOutside, + imageCapture: this.recognizerName === 'BlinkIdImageCaptureRecognizer' + } }); break; } @@ -216,13 +208,17 @@ export class SdkService { } else { eventCallback({ status: RecognitionStatus.EmptyResultState, - data: { initiatedByUser: this.cancelInitiatedFromOutside } + data: { + initiatedByUser: this.cancelInitiatedFromOutside, + recognizerName: '' + } }); } - window.setTimeout(() => void this.cancelRecognition(), 400); - } - ); + if (this.recognizerName !== 'BlinkIdImageCaptureRecognizer') { + window.setTimeout(() => void this.cancelRecognition(), 400); + } + }); } catch (error) { if (error && error.name === 'VideoRecognizerError') { const reason = (error as PhotoPaySDK.VideoRecognizerError).reason; @@ -256,8 +252,18 @@ export class SdkService { } } - public isScanFromImageAvailable(recognizers: Array): boolean { - for (let i = 0; i < recognizers.length; ++i) {} + public async flipCamera(): Promise { + await this.videoRecognizer.flipCamera(); + } + + public isCameraFlipped(): boolean { + if (!this.videoRecognizer) { + return false; + } + return this.videoRecognizer.cameraFlipped; + } + + public isScanFromImageAvailable(_recognizers: Array = [], _recognizerOptions: any = {}): boolean { return true; } @@ -294,9 +300,10 @@ export class SdkService { return; } - const imageElement = document.createElement('img'); + const imageElement = new Image(); imageElement.src = URL.createObjectURL(file); await imageElement.decode(); + const imageFrame = PhotoPaySDK.captureFrame(imageElement); this.eventEmitter$.addEventListener('terminate', async () => { @@ -321,6 +328,8 @@ export class SdkService { await recognizer.recognizer.delete(); } } + + this.eventEmitter$.dispatchEvent(new Event('terminate:done')); }); // Get results @@ -335,10 +344,17 @@ export class SdkService { if (!results || results.state === PhotoPaySDK.RecognizerResultState.Empty) { eventCallback({ status: RecognitionStatus.EmptyResultState, - data: { initiatedByUser: this.cancelInitiatedFromOutside } + data: { + initiatedByUser: this.cancelInitiatedFromOutside, + recognizerName: recognizer.name + } }); } else { - const recognitionResults: RecognitionResults = { recognizer: results } + const recognitionResults: RecognitionResults = { + recognizer: results, + imageCapture: recognizer.name === 'BlinkIdImageCaptureRecognizer', + recognizerName: recognizer.name + }; eventCallback({ status: RecognitionStatus.ScanSuccessful, data: recognitionResults @@ -347,9 +363,13 @@ export class SdkService { } } } else { + eventCallback({ status: RecognitionStatus.EmptyResultState, - data: { initiatedByUser: this.cancelInitiatedFromOutside } + data: { + initiatedByUser: this.cancelInitiatedFromOutside, + recognizerName: '' + } }); } @@ -360,6 +380,10 @@ export class SdkService { void await this.cancelRecognition(true); } + public async resumeRecognition(): Promise { + this.videoRecognizer.resumeRecognition(true); + } + ////////////////////////////////////////////////////////////////////////////// // // PRIVATE METHODS @@ -370,7 +394,7 @@ export class SdkService { private async createRecognizers( recognizers: Array, - recognizerOptions?: Array, + recognizerOptions?: any, successFrame: boolean = false ): Promise> { const pureRecognizers = []; @@ -380,14 +404,14 @@ export class SdkService { pureRecognizers.push(instance); } - if (recognizerOptions && recognizerOptions.length) { + if (recognizerOptions && Object.keys(recognizerOptions).length > 0) { for (const recognizer of pureRecognizers) { let settingsUpdated = false; const settings = await recognizer.currentSettings(); - for (const setting of recognizerOptions) { - if (setting in settings) { - settings[setting] = true; + for (const [key, value] of Object.entries(recognizerOptions[recognizer.recognizerName])) { + if (key in settings) { + settings[key] = value; settingsUpdated = true; } } @@ -400,8 +424,9 @@ export class SdkService { const recognizerInstances = []; - for (const recognizer of pureRecognizers) { - const instance: RecognizerInstance = { recognizer } + for (let i = 0; i < pureRecognizers.length; ++i) { + const recognizer = pureRecognizers[i]; + const instance: RecognizerInstance = { name: recognizers[i], recognizer } if (successFrame) { const successFrameGrabber = await PhotoPaySDK.createSuccessFrameGrabberRecognizer(this.sdk, recognizer); @@ -424,7 +449,6 @@ export class SdkService { eventCallback({ status: RecognitionStatus.DetectionStatusChange, data: quad }); const detectionStatus = quad.detectionStatus; - switch (detectionStatus) { case PhotoPaySDK.DetectionStatus.Fail: eventCallback({ status: RecognitionStatus.DetectionStatusSuccess }); diff --git a/ui/src/utils/translation.service.ts b/ui/src/utils/translation.service.ts index 78075ae..bd6f31d 100644 --- a/ui/src/utils/translation.service.ts +++ b/ui/src/utils/translation.service.ts @@ -1,26 +1,41 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ + export const defaultTranslations: { [key: string]: string|Array } = { 'action-alt-camera': 'Device camera', 'action-alt-gallery': 'From gallery', 'action-message': 'Scan or choose from gallery', + 'action-message-camera': 'Device camera', + 'action-message-camera-disabled': 'Camera disabled', + 'action-message-camera-not-allowed': 'Camera not allowed', + 'action-message-camera-in-use': 'Camera in use', + 'action-message-image': 'From gallery', + 'action-message-image-not-supported': 'Not supported', 'camera-disabled': 'Camera disabled', 'camera-not-allowed': 'Cannot access camera.', 'camera-in-use': 'Camera is already used by another application.', 'camera-generic-error': 'Cannot access camera.', 'camera-feedback-scan-front': ['Place the front side', 'of a document'], 'camera-feedback-scan-back': ['Place the back side', 'of a document'], + 'camera-feedback-flip': 'Flip the document', + 'camera-feedback-barcode-message': 'Scan the barcode', 'camera-feedback-move-farther': 'Move farther', 'camera-feedback-move-closer': 'Move closer', 'camera-feedback-adjust-angle': 'Adjust the angle', - 'camera-feedback-flip': 'Flip the document', 'drop-info': 'Drop image here', 'drop-error': 'Whoops, we don\'t support that image format. Please upload a JPEG or PNG file.', 'initialization-error': 'Failed to load component. Try using another device or update your browser.', 'process-image-message': 'Just a moment.', + 'process-api-message': 'Just a moment', + 'process-api-retry': 'Retry', + 'feedback-scan-unsuccessful-title': 'Scan unsuccessful', 'feedback-scan-unsuccessful': 'We weren\'t able to recognize your document. Please try again.', 'feedback-error-generic': 'Whoops, that didn\'t work. Please give it another go.', 'check-internet-connection': 'Check internet connection.', 'network-error': 'Network error.', - 'scanning-not-available': 'Scanning not available.' + 'scanning-not-available': 'Scanning not available.', + 'modal-window-close': 'Close', } export class TranslationService { @@ -30,8 +45,10 @@ export class TranslationService { this.translations = defaultTranslations; for (const key in alternativeTranslations) { - if (typeof this.translations[key] === 'string') { - this.translations[key] = alternativeTranslations[key]; + if (key in defaultTranslations) { + if (this.isExpectedValue(alternativeTranslations[key])) { + this.translations[key] = alternativeTranslations[key]; + } } } } @@ -44,4 +61,12 @@ export class TranslationService { return this.translations[key]; } } + + private isExpectedValue(value: string | Array): boolean { + if (Array.isArray(value)) { + const notValidFound = value.filter(item => typeof item !== 'string'); + return notValidFound.length == 0; + } + return typeof value === 'string'; + } } diff --git a/ui/stencil.config.ts b/ui/stencil.config.ts index 2004279..60b1865 100644 --- a/ui/stencil.config.ts +++ b/ui/stencil.config.ts @@ -1,6 +1,12 @@ +/** + * Copyright (c) Microblink Ltd. All rights reserved. + */ import { Config } from '@stencil/core'; +import { postcss } from '@stencil/postcss'; import { sass } from '@stencil/sass'; +import autoprefixer from 'autoprefixer'; + export const config: Config = { namespace: 'photopay-in-browser', taskQueue: 'async', @@ -19,6 +25,9 @@ export const config: Config = { sass({ // Add path to global SCSS files which should be included in every stylesheet injectGlobalPaths: [] + }), + postcss({ + plugins: [autoprefixer()] }) ] }; diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 86de6f5..623f7a4 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -19,5 +19,11 @@ "include": [ "./src/**/*", "./types/jsx.d.ts" + ], + "exclude": [ + "./src/components/shared/**/test", + "./src/components/**/test", + "./src/**/test", + "./src/test" ] }