Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@solana/kit": "^6.0.1",
"@velora-dex/sdk": "^9.3.3",
"alchemy-sdk": "^3.6.5",
"barcode-detector": "^3.1.1",
"bech32": "^1.1.4",
"bitcoinjs-lib": "^6.1.7",
"browser-image-compression": "^2.0.2",
Expand Down
4 changes: 2 additions & 2 deletions src/frontend/src/lib/components/qr/QrCodeScanner.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import { QRCodeReader } from '@dfinity/gix-components';
import { onMount } from 'svelte';
import QrCodeReader from '$lib/components/ui/QrCodeReader.svelte';
import { ADDRESS_BOOK_QR_CODE_SCAN } from '$lib/constants/test-ids.constants';
import type { QrStatus } from '$lib/types/qr-code';

Expand Down Expand Up @@ -46,7 +46,7 @@
class="stretch qr-code-wrapper h-full w-full md:min-h-[300px]"
data-tid={ADDRESS_BOOK_QR_CODE_SCAN}
>
<QRCodeReader on:nnsCancel={onCancel} on:nnsQRCode={onQRCode} />
<QrCodeReader on:nnsCancel={onCancel} on:nnsQRCode={onQRCode} />
</div>

<style lang="scss">
Expand Down
160 changes: 160 additions & 0 deletions src/frontend/src/lib/components/ui/QrCodeReader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<script lang="ts">
import { isNullish, nonNullish } from '@dfinity/utils';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { isDesktop } from '$lib/utils/device.utils';
import { nextElementId } from '$lib/utils/html.utils';

const id = nextElementId('qrcode-reader-');

const dispatch = createEventDispatcher();

let videoElement: HTMLVideoElement | undefined;
let stream: MediaStream | undefined;
let scanInterval: NodeJS.Timeout | undefined;
let isDestroyed = false;
let isProcessingFrame = false;

onMount(async () => {
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment',
width: { ideal: 1920 },
height: { ideal: 1080 }
},
audio: false
});

if (isDestroyed) {
stopStream();

return;
}

if (nonNullish(videoElement)) {
videoElement.srcObject = stream;

await videoElement.play();

await startScanning();
}
} catch (err: unknown) {
dispatch('nnsQRCodeError', err);
}
});

const startScanning = async () => {
try {
const { BarcodeDetector } = await import('barcode-detector/ponyfill');

if (isDestroyed) {
return;
}

const detector = new BarcodeDetector({
formats: ['qr_code']
});

const scan = async () => {
if (
isProcessingFrame ||
isDestroyed ||
isNullish(videoElement) ||
videoElement.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
) {
return;
}

isProcessingFrame = true;

try {
const results = await detector.detect(videoElement);

const [qrResult] = results;

if (nonNullish(qrResult)) {
dispatch('nnsQRCode', qrResult.rawValue);
}
} catch {
// Decoding failed on this frame — expected when no QR code is visible
} finally {
isProcessingFrame = false;
}
};

scanInterval = setInterval(scan, 100);
} catch (err: unknown) {
dispatch('nnsQRCodeError', err);
}
};

const stopStream = () => {
if (nonNullish(stream)) {
stream.getTracks().forEach((track) => track.stop());

stream = undefined;
}
};

onDestroy(() => {
isDestroyed = true;

if (nonNullish(scanInterval)) {
clearInterval(scanInterval);

scanInterval = undefined;
}

stopStream();
});

// We optimistically assume that if the QR code reader is used on desktop, it has most probably only a single "user" facing camera and that we can invert the displayed video
const mirror = isDesktop();
</script>

<article {id} class="reader" class:mirror>
<video bind:this={videoElement} autoplay muted playsinline></video>

<div class="scan-overlay">
<div class="scan-region"></div>
</div>
</article>

<style lang="scss">
.reader {
position: relative;
width: 100%;
height: 100%;

border-radius: var(--border-radius);
overflow: hidden;

&.mirror {
transform: scaleX(-1);
}
}

video {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}

.scan-overlay {
position: absolute;
inset: 0;
container-type: size;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}

.scan-region {
width: min(90cqw, 90cqh);
aspect-ratio: 1;
box-shadow: 0 0 0 9999px white;
border: 2px solid rgba(var(--primary-rgb), 0.4);
}
</style>
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script lang="ts">
import { QRCodeReader } from '@dfinity/gix-components';
import Button from '$lib/components/ui/Button.svelte';
import ButtonGroup from '$lib/components/ui/ButtonGroup.svelte';
import ContentWithToolbar from '$lib/components/ui/ContentWithToolbar.svelte';
import InputText from '$lib/components/ui/InputText.svelte';
import QrCodeReader from '$lib/components/ui/QrCodeReader.svelte';
import {
TRACK_COUNT_WALLET_CONNECT,
TRACK_COUNT_WALLET_CONNECT_QR_CODE
Expand Down Expand Up @@ -71,7 +71,7 @@
<ContentWithToolbar>
<div class="qr-code rounded-lg">
{#if renderQRCodeReader}
<QRCodeReader on:nnsQRCode={onQRCodeSuccess} on:nnsQRCodeError={error} />
<QrCodeReader on:nnsQRCode={onQRCodeSuccess} on:nnsQRCodeError={error} />
{/if}

{#if !renderQRCodeReader}
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/src/lib/utils/html.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
let elementsCounters: Record<string, number> = {};

export const nextElementId = (prefix: string): string => {
elementsCounters = {
...elementsCounters,
[prefix]: (elementsCounters[prefix] ?? 0) + 1
};

return `${prefix}${elementsCounters[prefix]}`;
};
Loading