Skip to content

Commit

Permalink
feat: qrcode reader (#171)
Browse files Browse the repository at this point in the history
# Motivation

Read QR code within a dapp.

# Changes

- new dependency `jsQR`
- new QR code reader components, services and workes
- docs route `qr-code` renamed to `qr-code-generator`
- docs for QR reader
- added `inner-end` slot to input in order to display a QR code button within
- few lint rules corrected
  • Loading branch information
peterpeterparker authored Mar 16, 2023
1 parent 350349e commit 3feb763
Show file tree
Hide file tree
Showing 30 changed files with 795 additions and 39 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ node_modules

.dfx

test-results
test-results

samples
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ node_modules
.env.*
!.env.example

dist

# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
Expand Down
4 changes: 2 additions & 2 deletions e2e/qr-code.e2e.ts → e2e/qr-code-generator.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { expect, test } from "@playwright/test";

const testUrl = "/components/qr-code";
const testUrl = "/components/qr-code-generator";

test("QR code page has expected h1", async ({ page }) => {
await page.goto(testUrl);
await expect(page.locator("h1")).toHaveText("QR Code");
await expect(page.locator("h1")).toHaveText("QR Code Generator");
});

test("QR code renders https://nns.ic0.app", async ({ page }) => {
Expand Down
70 changes: 70 additions & 0 deletions e2e/qr-code-reader.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { chromium, expect, test, type Page } from "@playwright/test";

const testUrl = "/components/qr-code-reader";

test("QR code page has expected h1", async ({ page }) => {
await page.goto(testUrl);
await expect(page.locator("h1")).toHaveText("QR Code Reader");
});

const testQRCode = async (page: Page) => {
await page.goto(testUrl);

const showcase = page.getByTestId("showcase");
await showcase.scrollIntoViewIfNeeded();

const modal = page.getByTestId("qr-code-modal");
await modal.click();

// Wait for video
await page.waitForTimeout(2000);

await expect(page).toHaveScreenshot();
};

test("Read QR throws error because user block camera", async () => {
const browser = await chromium.launch();

const context = await browser.newContext({
permissions: [],
});

const page = await context.newPage();

await testQRCode(page);
});

test("Read QR throws error because camera stream fails", async () => {
const browser = await chromium.launch({
args: [
"--use-fake-ui-for-media-stream",
"--use-fake-device-for-media-stream",
"--use-file-for-fake-video-capture=./empty/stream/error", // leads to error: Requested device not found
],
});

const context = await browser.newContext({
permissions: ["camera"],
});

const page = await context.newPage();

await testQRCode(page);
});

test("Read QR code value with camera", async () => {
const browser = await chromium.launch({
args: [
"--use-fake-ui-for-media-stream",
"--use-fake-device-for-media-stream",
"--use-file-for-fake-video-capture=./samples/qrcode.y4m",
],
});

const context = await browser.newContext({
permissions: ["camera"],
});

const page = await context.newPage();
await testQRCode(page);
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"lint": "prettier --check --plugin-search-dir=. . && eslint .",
"format": "prettier --write --plugin-search-dir=. .",
"test": "TZ=UTC jest",
"e2e": "playwright test",
"download:samples": "./scripts/download-samples.sh",
"e2e": "npm run download:samples && playwright test",
"i18n": "node scripts/i18n.types.mjs"
},
"exports": {
Expand Down Expand Up @@ -62,6 +63,7 @@
},
"dependencies": {
"dompurify": "^3.0.1",
"jsqr": "^1.4.0",
"qr-creator": "^1.0.0"
},
"keywords": [
Expand Down
15 changes: 15 additions & 0 deletions scripts/download-samples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash

DIR="./samples"

if [ ! -d $DIR ]; then
mkdir $DIR
fi

FILE="$DIR/qrcode.y4m"

if test -f "$FILE"; then
echo "$FILE exists"
else
wget --load-cookies /tmp/cookies.txt "https://drive.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://drive.google.com/uc?export=download&id=1wpsNoXTX3_y6Wrh7NE0SVwIZufK0-xeu' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=1wpsNoXTX3_y6Wrh7NE0SVwIZufK0-xeu" -O $FILE && rm -rf /tmp/cookies.txt
fi
10 changes: 8 additions & 2 deletions src/docs/constants/docs.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,17 @@ export const COMPONENT_ROUTES: ComponentRoute[] = [
},

{
path: "/components/qr-code",
title: "QR Code",
path: "/components/qr-code-generator",
title: "QR Code Generator",
description: "Generates a QR Code.",
},

{
path: "/components/qr-code-reader",
title: "QR Code Reader",
description: "Read a QR Code with the camera.",
},

{
path: "/components/segment",
title: "Segment",
Expand Down
2 changes: 2 additions & 0 deletions src/lib/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export { default as Popover } from "./components/Popover.svelte";
export { default as ProgressBar } from "./components/ProgressBar.svelte";
export { default as ProgressSteps } from "./components/ProgressSteps.svelte";
export { default as QRCode } from "./components/QRCode.svelte";
export { default as QRCodeReader } from "./components/QRCodeReader.svelte";
export { default as QRCodeReaderModal } from "./components/QRCodeReaderModal.svelte";
export { default as Segment } from "./components/Segment.svelte";
export { default as SegmentButton } from "./components/SegmentButton.svelte";
export { default as SkeletonText } from "./components/SkeletonText.svelte";
Expand Down
67 changes: 47 additions & 20 deletions src/lib/components/Input.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@
inputType !== "number" && inputType !== "icp"
? autocomplete ?? "off"
: undefined;
let displayInnerEnd: boolean;
$: displayInnerEnd = $$slots["inner-end"] !== undefined;
</script>

<div class="input-block" class:disabled>
Expand All @@ -147,26 +150,34 @@
<slot name="end" />
</div>
{/if}
<input
bind:this={inputElement}
data-tid={testId}
type={inputType === "icp" ? "text" : inputType}
{required}
{spellcheck}
{name}
id={name}
{step}
{disabled}
value={inputType === "icp" ? icpValue : value}
minlength={minLength}
{placeholder}
{max}
{autocomplete}
on:blur
on:focus
on:input={handleInput}
on:keydown={handleKeyDown}
/>
<div class="input-field">
<input
bind:this={inputElement}
data-tid={testId}
type={inputType === "icp" ? "text" : inputType}
{required}
{spellcheck}
{name}
id={name}
{step}
{disabled}
value={inputType === "icp" ? icpValue : value}
minlength={minLength}
{placeholder}
{max}
{autocomplete}
on:blur
on:focus
on:input={handleInput}
on:keydown={handleKeyDown}
class:inner-end={displayInnerEnd}
/>
{#if displayInnerEnd}
<div class="inner-end-slot">
<slot name="inner-end" />
</div>
{/if}
</div>
</div>

<style lang="scss">
Expand Down Expand Up @@ -224,4 +235,20 @@
input[disabled] {
cursor: text;
}
.input-field {
position: relative;
}
.inner-end {
padding-right: var(--input-padding-inner-end, 64px);
}
.inner-end-slot {
position: absolute;
top: 50%;
right: 0;
transform: translate(0, -50%);
padding: var(--padding) var(--padding-2x);
}
</style>
Loading

0 comments on commit 3feb763

Please sign in to comment.