<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="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4" > <span class="text-red">Beware!</span> You aren't sharing your name, so quickly <router-link :to="{ name: 'new-edit-account' }" class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md" > click here to set it for them. </router-link> </p> </div> <div @click="onCopyToClipboard()" v-if="activeDid" class="text-center"> <!-- 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> Click that QR to copy your contact URL to your clipboard. </span> <div>Not scanning? Show it in pieces.</div> </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 QuickNav from "@/components/QuickNav.vue"; import { NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { Account } from "@/db/tables/accounts"; import { Contact } from "@/db/tables/contacts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { deriveAddress, getContactPayloadFromJwtUrl, nextDerivationPath, SimpleSigner, } from "@/libs/crypto"; import { CONTACT_URL_PREFIX, ENDORSER_JWT_URL_LOCATION, isDid, setVisibilityUtil, } from "@/libs/endorserServer"; import { Buffer } from "buffer/"; @Component({ components: { QrcodeStream, QRCodeVue3, QuickNav, }, }) export default class ContactQRScanShow extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; activeDid = ""; apiServer = ""; givenName = ""; qrValue = ""; async created() { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = (settings?.activeDid as string) || ""; this.apiServer = (settings?.apiServer as string) || ""; this.givenName = (settings?.firstName as string) || ""; 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, profileImageUrl: settings?.profileImageUrl, registered: settings?.isRegistered, }, }; 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; } } danger(message: string, title: string = "Error", timeout = 5000) { this.$notify( { group: "alert", type: "danger", title: title, text: message, }, timeout, ); } 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 as string) || "null"); if (!identity) { throw new Error( "Attempted to show contact info with no identifier available.", ); } return identity; } /** * * @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 async onScanDetect(content: any) { const url = content[0]?.rawValue; if (url) { let newContact: Contact; try { const payload = getContactPayloadFromJwtUrl(url); if (!payload) { this.$notify( { group: "alert", type: "danger", title: "No Contact Info", text: "The contact info could not be parsed.", }, 3000, ); return; } newContact = { did: payload.iss as string, name: payload.own.name, nextPubKeyHashB64: payload.own.nextPublicEncKeyHash, profileImageUrl: payload.own.profileImageUrl, publicKeyBase64: payload.own.publicEncKey, registered: payload.own.registered, }; if (!newContact.did) { this.danger("There is no DID.", "Incomplete Contact"); return; } if (!isDid(newContact.did)) { this.danger("The DID must begin with 'did:'", "Invalid DID"); return; } } catch (e) { console.error("Error parsing QR info:", e); this.danger("Could not parse the QR info.", "Read Error"); return; } try { await db.open(); await db.contacts.add(newContact); let addedMessage; if (this.activeDid) { await this.setVisibility(newContact, true); addedMessage = "They were added, and your activity is visible to them."; } else { addedMessage = "They were added."; } this.$notify( { group: "alert", type: "success", title: "Contact Added", text: addedMessage, }, 3000, ); } catch (e) { console.error("Error saving contact info:", e); this.$notify( { group: "alert", type: "danger", title: "Contact Error", text: "Could not save contact info. Check if it already exists.", }, 5000, ); } } else { this.$notify( { group: "alert", type: "danger", title: "Invalid Contact QR Code", text: "No QR code detected with contact information.", }, 5000, ); } } async setVisibility(contact: Contact, visibility: boolean) { const result = await setVisibilityUtil( this.activeDid, this.apiServer, this.axios, db, contact, visibility, ); if (result.error) { this.danger(result.error as string, "Error Setting Visibility"); } else if (!result.success) { console.error("Got strange result from setting visibility:", result); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any onScanError(error: any) { console.error("Scan was invalid:", error); this.$notify( { group: "alert", type: "danger", title: "Invalid Scan", text: "The scan was invalid.", }, 5000, ); } onCopyToClipboard() { //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing 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>