<template> <QuickNav selected="Profile"></QuickNav> <!-- CONTENT --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <!-- Breadcrumb --> <div class="mb-8"> <!-- Back --> <div class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="$router.back()" > <fa icon="chevron-left" class="fa-fw"></fa> </h1> </div> <!-- Heading --> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> Your Contact Info </h1> <p v-if="!givenName" class="text-center mt-2"> <span class="text-red">Beware!</span> You aren't sharing your name, so hurry and <router-link :to="{ name: 'new-edit-account' }" class="bg-blue-500 text-white px-1.5 py-1 rounded-md" > go here to set it for them. </router-link> </p> </div> <div @click="onCopyToClipboard()" v-if="activeDid"> <!-- Play with display options: https://qr-code-styling.com/ See docs: https://www.npmjs.com/package/qr-code-generator-vue3 --> <QRCodeVue3 :value="this.qrValue" :cornersSquareOptions="{ type: 'extra-rounded' }" :dotsOptions="{ type: 'square' }" class="flex justify-center" /> <span class="flex justify-center"> Click QR to copy your contact URL to your clipboard. </span> </div> <div class="text-center" v-else> You have no identitifiers yet, so <router-link :to="{ name: 'start' }" class="bg-blue-500 text-white px-1.5 py-1 rounded-md" > create your identifier. </router-link> <br /> If you don't that first, these contacts won't see your activity. </div> <div class="text-center"> <h1 class="text-4xl text-center font-light pt-6">Scan Contact Info</h1> <qrcode-stream @detect="onScanDetect" @error="onScanError" /> <span> If you do not see a scanning camera window here, check your camera permissions. </span> </div> </section> </template> <script lang="ts"> import * as didJwt from "did-jwt"; import { sha256 } from "ethereum-cryptography/sha256.js"; import QRCodeVue3 from "qr-code-generator-vue3"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { QrcodeStream } from "vue-qrcode-reader"; import { useClipboard } from "@vueuse/core"; import { accountsDB, db } from "@/db/index"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto"; import QuickNav from "@/components/QuickNav.vue"; import { Account } from "@/db/tables/accounts"; import { CONTACT_URL_PREFIX, ENDORSER_JWT_URL_LOCATION, } from "@/libs/endorserServer"; // eslint-disable-next-line @typescript-eslint/no-var-requires const Buffer = require("buffer/").Buffer; interface Notification { group: string; type: string; title: string; text: string; } @Component({ components: { QrcodeStream, QRCodeVue3, QuickNav, }, }) export default class ContactQRScanShow extends Vue { $notify!: (notification: Notification, timeout?: number) => void; activeDid = ""; apiServer = ""; givenName = ""; qrValue = ""; public async getIdentity(activeDid: string) { await accountsDB.open(); const accounts = await accountsDB.accounts.toArray(); const account: Account | undefined = R.find( (acc) => acc.did === activeDid, accounts, ); const identity = JSON.parse(account?.identity || "null"); if (!identity) { throw new Error( "Attempted to show contact info with no identifier available.", ); } return identity; } async created() { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; this.givenName = settings?.firstName || ""; await accountsDB.open(); const accounts = await accountsDB.accounts.toArray(); const account = R.find((acc) => acc.did === this.activeDid, accounts); if (account) { const identity = await this.getIdentity(this.activeDid); const publicKeyHex = identity.keys[0].publicKeyHex; const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const newDerivPath = nextDerivationPath(account.derivationPath); const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2]; const nextPublicEncKey = Buffer.from(nextPublicHex, "hex"); const nextPublicEncKeyHash = sha256(nextPublicEncKey); const nextPublicEncKeyHashBase64 = Buffer.from(nextPublicEncKeyHash).toString("base64"); const contactInfo = { iat: Date.now(), iss: this.activeDid, own: { name: (settings?.firstName || "") + (settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3 publicEncKey, nextPublicEncKeyHash: nextPublicEncKeyHashBase64, }, }; const alg = undefined; const privateKeyHex: string = identity.keys[0].privateKeyHex; const signer = await SimpleSigner(privateKeyHex); // create a JWT for the request const vcJwt: string = await didJwt.createJWT(contactInfo, { alg: alg, issuer: identity.did, signer: signer, }); const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION; this.qrValue = viewPrefix + vcJwt; } } /** * * @param content is the result of a QR scan, an array with one item with a rawValue property */ // Unfortunately, there are not typescript definitions for the qrcode-stream component yet. // eslint-disable-next-line @typescript-eslint/no-explicit-any onScanDetect(content: any) { if (content[0]?.rawValue) { //console.log("onDetect", content[0].rawValue); localStorage.setItem("contactEndorserUrl", content[0].rawValue); this.$router.push({ name: "contacts" }); } else { this.$notify( { group: "alert", type: "warning", title: "Invalid Contact QR Code", text: "No QR code detected with contact information.", }, -1, ); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any onScanError(error: any) { console.log("Scan was invalid:", error); this.$notify( { group: "alert", type: "warning", title: "Invalid Scan", text: "The scan was invalid.", }, -1, ); } onCopyToClipboard() { useClipboard() .copy(this.qrValue) .then(() => { console.log("Contact URL:", this.qrValue); this.$notify( { group: "alert", type: "toast", title: "Copied", text: "Contact URL was copied to clipboard.", }, 2000, ); }); } } </script>