You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							235 lines
						
					
					
						
							7.0 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							235 lines
						
					
					
						
							7.0 KiB
						
					
					
				
								<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>
							 | 
						|
								
							 |