Skip to content

Commit

Permalink
Merge pull request #158 from KeystoneHQ/feat/webcomponent-animated-qr
Browse files Browse the repository at this point in the history
feat: add qrcode web component version
  • Loading branch information
Charon-Fan authored Jun 11, 2024
2 parents ef469ba + a7aa851 commit c496da1
Show file tree
Hide file tree
Showing 21 changed files with 925 additions and 108 deletions.
23 changes: 23 additions & 0 deletions packages/animated-qr-base/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@keystonehq/animated-qr-base",
"version": "0.0.1",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"homepage": "https://github.com/KeystoneHQ/keystone-airgaped-base#readme",
"scripts": {
"dev": "pnpm run cleanup && tsc -w",
"build": "pnpm run cleanup && tsc",
"cleanup": "rimraf ./dist",
"test": "jest --passWithNoTests"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"rimraf": "^5.0.7"
},
"dependencies": {
"@ngraveio/bc-ur": "^1.1.13"
}
}
File renamed without changes.
4 changes: 4 additions & 0 deletions packages/animated-qr-base/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./constant";
export * from "./types";
export * from "./webcamUtils";
export * from "./getAnimatedScan";
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,7 @@ export enum CameraStatus {
UNKNOWN_ERROR = "UNKNOWN_CAMERA_ERROR",
}

export type CameraError = "NO_WEBCAM_FOUND" | "NO_WEBCAM_ACCESS"

export interface ScannerProps {
purpose?: Purpose
urTypes?: string[]
handleScan: (ur: { type: string; cbor: string }) => void
handleError: (error: string) => void
options?: {
width?: number | string
height?: number | string
blur?: boolean
}
videoLoaded?: (canPlay: boolean, error?: CameraError) => void
onProgress?: (progress: number) => void
}
export type CameraError = "NO_WEBCAM_FOUND" | "NO_WEBCAM_ACCESS";

export enum Purpose {
SYNC = "sync",
Expand All @@ -48,3 +34,17 @@ export enum URType {
COSMOS_SIGNATURE = "cosmos-signature",
EVM_SIGNATURE = "evm-signature",
}

export interface ScannerProps {
purpose?: Purpose;
urTypes?: string[];
handleScan: (ur: { type: string; cbor: string }) => void;
handleError: (error: string) => void;
options?: {
width?: number | string;
height?: number | string;
blur?: boolean;
};
videoLoaded?: (canPlay: boolean, error?: CameraError) => void;
onProgress?: (progress: number) => void;
}
14 changes: 14 additions & 0 deletions packages/animated-qr-base/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "node_modules/*"],
"compilerOptions": {
"rootDir": "./src",
"baseUrl": "./",
"outDir": "./dist",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"paths": {
"*": ["src/*", "node_modules/*"]
}
},
}
28 changes: 28 additions & 0 deletions packages/animated-qr-lit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@keystonehq/animated-qr-lit",
"version": "0.0.1",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"homepage": "https://github.com/KeystoneHQ/keystone-airgaped-base#readme",
"scripts": {
"dev": "pnpm run cleanup && tsc -w",
"build": "pnpm run cleanup && tsc",
"cleanup": "rimraf ./dist",
"test": "jest --passWithNoTests"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"rimraf": "^5.0.7"
},
"dependencies": {
"@keystonehq/animated-qr-base": "^0.0.1",
"@ngraveio/bc-ur": "^1.1.13",
"@zxing/browser": "^0.1.1",
"@zxing/library": "^0.19.1",
"lit": "^3.1.4",
"qrcode": "^1.5.3"
}
}
82 changes: 82 additions & 0 deletions packages/animated-qr-lit/src/AnimatedQRCode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { LitElement, html, css } from "lit";
import { property } from "lit/decorators/property.js";
import { customElement } from "lit/decorators/custom-element.js";
import QRCode from "qrcode";
import { UR, UREncoder } from "@ngraveio/bc-ur";

const MAX_FRAGMENT_LENGTH = 400;
const DEFAULT_INTERVAL = 100;
const DEFAULT_QR_SIZE = 180;

@customElement("animated-qrcode-lit")
export class AnimatedQRCodeLite extends LitElement {
private timer: NodeJS.Timeout;
private urEncoder: UREncoder;

@property({ type: String })
cbor: string;

@property({ type: String })
type: string;

@property({ type: Number })
capacity: number = MAX_FRAGMENT_LENGTH;

@property({ type: Number })
interval: number = DEFAULT_INTERVAL;

@property({ type: Number })
size: number = DEFAULT_QR_SIZE;

static styles = css`
:host {
display: block;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
}
`;

generateQRCode() {
if (this.timer) {
clearInterval(this.timer);
}
this.urEncoder = new UREncoder(
new UR(Buffer.from(this.cbor, "hex"), this.type),
this.capacity
);
this.timer = setInterval(() => {
this.updateQrcode(this.urEncoder.nextPart().toUpperCase());
}, this.interval);
}

async updateQrcode(data: string) {
const img = this.shadowRoot.querySelector("img");
img.src = await QRCode.toDataURL(data, {
margin: 0,
});
}

connectedCallback() {
super.connectedCallback();
this.generateQRCode();
}

disconnectedCallback(): void {
super.disconnectedCallback();
clearInterval(this.timer);
}

updated(nextProps) {
this.generateQRCode();
if (nextProps.has("size")) {
this.style.width = `${this.size}px`;
this.style.height = `${this.size}px`;
}
}

render() {
return html`<img style="width: ${this.size - 10}px; height: auto;" />`;
}
}
68 changes: 68 additions & 0 deletions packages/animated-qr-lit/src/AnimatedQRScanner/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { BarcodeFormat, DecodeHintType } from "@zxing/library";
import { BrowserQRCodeReader, IScannerControls } from "@zxing/browser";
import { ScannerProps, getAnimatedScan } from "@keystonehq/animated-qr-base";

const codeReader = () => {
const hint = new Map();
hint.set(DecodeHintType.POSSIBLE_FORMATS, [BarcodeFormat.QR_CODE]);
return new BrowserQRCodeReader(hint, {
delayBetweenScanAttempts: 50,
delayBetweenScanSuccess: 100,
});
};

interface SetupProps extends Omit<ScannerProps, "options"> {
video: HTMLVideoElementScanPreview<IScannerControls>;
}

export const setupScanner = ({
video,
purpose,
urTypes,
handleScan,
handleError,
videoLoaded,
onProgress,
}: SetupProps) => {
const { handleScanSuccess, handleScanFailure } = getAnimatedScan({
purpose,
urTypes,
handleScan,
handleError,
onProgress,
});
const canplayListener = () => {
videoLoaded && videoLoaded(true);
};
video.addEventListener("canplay", canplayListener);
const pendingScanRequest =
video?.pendingScanRequest ?? Promise.resolve(undefined);
const scanRequest = pendingScanRequest
.then(() =>
codeReader().decodeFromVideoDevice(undefined, video, (result, error) => {
if (result) {
handleScanSuccess(result.getText());
}
if (error) {
handleScanFailure(error.message);
}
})
)
.catch((error) => {
console.error(error);
return undefined;
});

if (video) {
video.pendingScanRequest = scanRequest;
}

return () => {
video.removeEventListener("canplay", canplayListener);
scanRequest.then((controls) => {
if (controls) {
controls.stop();
}
});
};
};
2 changes: 2 additions & 0 deletions packages/animated-qr-lit/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./AnimatedQRCode";
export * from "./AnimatedQRScanner";
14 changes: 14 additions & 0 deletions packages/animated-qr-lit/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "node_modules/*", "types/**/*"],
"compilerOptions": {
"rootDir": "./src",
"baseUrl": "./",
"outDir": "./dist",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"paths": {
"*": ["src/*", "node_modules/*"]
}
},
}
13 changes: 13 additions & 0 deletions packages/animated-qr-lit/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
type Nullable<T> = T | null | undefined;

type HTMLVideoElementScanPreview<T> = HTMLVideoElement & {
pendingScanRequest?: Promise<T | undefined>;
};

interface AnimatedQRCodeProps {
cbor: string;
type: string;
size?: number;
capacity?: number;
interval?: number;
}
Loading

0 comments on commit c496da1

Please sign in to comment.