Skip to content

Commit

Permalink
Merge pull request #20 from cre8/19-automatic-recognition-of-qr-code
Browse files Browse the repository at this point in the history
fix: scan qr code in a document
  • Loading branch information
cre8 authored May 5, 2024
2 parents 84d330e + edc902e commit b28679e
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 29 deletions.
2 changes: 2 additions & 0 deletions apps/holder/projects/browser-extension/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { environment } from '../environments/environment';
import { ApiModule, Configuration } from '../../../shared/api/kms';
import { AuthServiceInterface } from '../../../shared/settings/settings.component';
import { AuthService } from './auth/auth.service';
import { HashLocationStrategy, LocationStrategy } from '@angular/common';

// eslint-disable-next-line @typescript-eslint/no-namespace
export declare namespace globalThis {
Expand All @@ -29,6 +30,7 @@ function getConfiguration() {
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
{ provide: LocationStrategy, useClass: HashLocationStrategy },
provideAnimations(),
provideOAuthClient(),
importProvidersFrom(ApiModule, HttpClientModule),
Expand Down
4 changes: 4 additions & 0 deletions apps/holder/projects/browser-extension/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const routes: Routes = [
path: 'scan',
component: ScannerComponent,
},
{
path: 'scan/:id',
component: ScannerComponent,
},
{
path: 'credentials',
component: CredentialsListComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { firstValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
import { IssuanceRequestComponent } from '../../../../shared/oid4vc/issuance-request/issuance-request.component';
import { VerifyRequestComponent } from '../../../../shared/oid4vc/verify-request/verify-request.component';
import { ActivatedRoute } from '@angular/router';

@Component({
selector: 'app-scanner',
Expand All @@ -38,10 +39,18 @@ export class ScannerComponent implements OnInit {

constructor(
public scanner: ScannerService,
private httpClient: HttpClient
private httpClient: HttpClient,
private route: ActivatedRoute
) {}

ngOnInit(): void {
const id = this.route.snapshot.paramMap.get('id');
if (id) {
this.url = id;
this.action = id.startsWith('openid-credential-offer://')
? 'issue'
: 'verify';
}
this.scanner.results = [];
this.urlField = new FormControl('');
this.urlField.valueChanges.subscribe((value) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,19 @@ export class ScannerService {
private zone: NgZone,
private oid4vciApiService: Oid4vciApiService,
private oid4vpApiService: Oid4vcpApiService
) {}
) {
// this.listenToEvents();
}

listenToEvents() {
console.log('listen');
chrome.runtime.onMessage.addListener((request) => {
console.log(request);
if (request.action === 'addQRCode') {
this.parse(request.data);
}
});
}

async parse(url: string) {
if (url.startsWith('openid-credential-offer')) {
Expand Down
27 changes: 24 additions & 3 deletions apps/holder/projects/browser-extension/src/background.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
let qrCodes: string[] = [];
let scanningStatus: string;
let walletenWindow: chrome.windows.Window | null = null;

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
if (request.action === 'fetchImage') {
fetch(request.url)
.then((response) => response.blob())
Expand All @@ -23,19 +24,39 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
qrCodes.push(request.data);
chrome.action.setBadgeText({
text: qrCodes.length.toString(),
tabId: sender.tab!.id,
tabId: (sender.tab as chrome.tabs.Tab).id,
});
}
if (request.action === 'getQRCodes') {
sendResponse(qrCodes);
}

if (request.action === 'open') {
const id = request.data.url;
const url = chrome.runtime.getURL(
`index.html#/scan/${encodeURIComponent(id)}`
);
if (walletenWindow !== null) {
console.log('removing window');
chrome.windows.remove(
(walletenWindow as chrome.windows.Window).id as number
);
}
walletenWindow = await chrome.windows.create({
url,
type: 'panel',
width: 400,
height: 700,
focused: true,
});
}

if (request.action === 'reset') {
// reset qrCodes and scanningStatus
qrCodes = [];
chrome.action.setBadgeText({
text: qrCodes.length.toString(),
tabId: sender.tab!.id,
tabId: (sender.tab as chrome.tabs.Tab).id,
});
}

Expand Down
89 changes: 67 additions & 22 deletions apps/holder/projects/browser-extension/src/content-script.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import jsQR from 'jsqr';

let codes: string[] = [];
const scannedQrCodeImages = new Map();

/**
* Gets the image data from the given url and sends it to the callback.
* @param {HTMLImageElement} elemet
*/
function scanImage(element: HTMLImageElement) {
if (!element.src) return;
if (element.src.startsWith('data:')) {
readImage(element.src, element);
} else {
//TODO: this is not implemented
chrome.runtime.sendMessage(
{ action: 'fetchImage', url: element.src },
function (response) {
readImage(response.imageData, element);
}
(response) => readImage(response.imageData, element)
);
}
}
Expand All @@ -29,27 +27,27 @@ function readImage(
htmlElement: HTMLImageElement | HTMLCanvasElement
) {
const image = new Image();
image.onload = function () {
image.onload = async () => {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
const context = canvas.getContext('2d') as CanvasRenderingContext2D;
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const code = jsQR(imageData.data, imageData.width, imageData.height);
//check if the QR code is a valid credential offer or request
const protocols = ['openid-credential-offer', 'openid4vp'];
const protocols = ['openid-credential-offer', 'openid'];
if (code && protocols.includes(code.data.split(':')[0])) {
//TODO: in case we detected something, we could manipulate the DOM here by passing the html element
htmlElement.style.border = '5px solid green';
codes.push(code.data);
const id = await getUniqueHash(code.data);
await addScanButton(htmlElement as HTMLImageElement, id, code.data);
scannedQrCodeImages.set(id, code.data);
chrome.runtime.sendMessage({ action: 'addQRCode', data: code.data });
}
toProcess--;
if (toProcess === 0) {
chrome.runtime.sendMessage({
action: 'process',
data: { action: 'scanned', value: codes },
data: { action: 'scanned', value: scannedQrCodeImages },
});
}
};
Expand All @@ -65,21 +63,68 @@ function scanForQRCode() {
const canvases = document.querySelectorAll('canvas');
toProcess = canvases.length + images.length;
chrome.runtime.sendMessage({ action: 'process', data: 'scanning' });
// biome-ignore lint/complexity/noForEach: <explanation>
images.forEach((image) => scanImage(image));
// biome-ignore lint/complexity/noForEach: <explanation>
canvases.forEach((canvas) => {
const imageData = canvas.toDataURL();
readImage(imageData, canvas);
});
}

chrome.runtime.onMessage.addListener(function (request) {
if (request.action === 'rescanQRCode') {
codes = [];
chrome.runtime.sendMessage({ action: 'reset' });
scanForQRCode();
}
});
/**
*
* function to add a button to the scanned QR code image
* @param element
* @returns
*/
async function addScanButton(
element: HTMLImageElement,
id: string,
url: string
) {
if (scannedQrCodeImages.has(id)) return;
// Create button
const button = document.createElement('button');
button.style.position = 'absolute';
button.style.top = `${element.offsetTop + element.height}px`;
//substract with with of the button to align it to the right
button.style.left = `${element.offsetLeft + element.width - 110}px`;
button.style.zIndex = '1000';
button.style.backgroundColor = '#ff670096';
button.style.color = 'white';
button.style.border = 'none';
button.style.padding = '10px';
button.style.cursor = 'pointer';
button.style.borderRadius = '5px';
button.innerText = 'Scan QR Code';
button.onclick = () => {
chrome.runtime.sendMessage({
action: 'open',
data: { url },
});
};
element.parentElement?.appendChild(button); // Removed the redundant exclamation mark
}

/**
* Create the hash of the given data url and return a hex string.
*/
function getUniqueHash(dataUrl: string) {
return window.crypto.subtle
.digest('SHA-256', new TextEncoder().encode(dataUrl))
.then((hash) => {
let result = '';
const view = new DataView(hash);
for (let i = 0; i < view.byteLength; i += 4) {
result += `00000000${view.getUint32(i).toString(16)}`.slice(-8);
}
return result;
});
}

//TODO: maybe use an interval for this.
// Scan the page for QR codes when the page is loaded.
window.onload = scanForQRCode;
console.log('start scanning for QR Codes');
setInterval(() => {
scanForQRCode();
}, 1000);
scanForQRCode();
2 changes: 1 addition & 1 deletion apps/holder/projects/browser-extension/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>WalletExtension</title>
<base href="/" />
<!-- <base href="/" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
Expand Down
1 change: 0 additions & 1 deletion apps/issuer-demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ function getStatus(id: string) {
fetch(`${config.issuerUrl}/sessions/${id}`)
.then((res) => res.json())
.then((res) => {
console.log(res);
status.innerHTML = res.status;
});
}
Expand Down

0 comments on commit b28679e

Please sign in to comment.