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.
		
		
		
		
		
			
		
			
				
					
					
						
							468 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							468 lines
						
					
					
						
							14 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="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
							 | 
						|
								        <br />
							 | 
						|
								        <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="onCopyUrlToClipboard()"
							 | 
						|
								      v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
							 | 
						|
								      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 this or QR code to copy your contact URL to your clipboard.
							 | 
						|
								      </span>
							 | 
						|
								    </div>
							 | 
						|
								    <div v-else-if="activeDid" class="text-center">
							 | 
						|
								      <!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
							 | 
						|
								      <span @click="onCopyDidToClipboard()" class="text-blue-500">
							 | 
						|
								        Click here to copy your DID to your clipboard.
							 | 
						|
								      </span>
							 | 
						|
								      <span>
							 | 
						|
								        Then give it to them so they can paste it in their list of People.
							 | 
						|
								      </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 { AxiosError } from "axios";
							 | 
						|
								import { Buffer } from "buffer/";
							 | 
						|
								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 { Contact } from "@/db/tables/contacts";
							 | 
						|
								import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
							 | 
						|
								import {
							 | 
						|
								  deriveAddress,
							 | 
						|
								  getContactPayloadFromJwtUrl,
							 | 
						|
								  nextDerivationPath,
							 | 
						|
								} from "@/libs/crypto";
							 | 
						|
								import {
							 | 
						|
								  CONTACT_URL_PREFIX,
							 | 
						|
								  createEndorserJwtForDid,
							 | 
						|
								  ENDORSER_JWT_URL_LOCATION,
							 | 
						|
								  isDid,
							 | 
						|
								  register,
							 | 
						|
								  setVisibilityUtil,
							 | 
						|
								} from "@/libs/endorserServer";
							 | 
						|
								import { ETHR_DID_PREFIX } from "@/libs/crypto/vc";
							 | 
						|
								
							 | 
						|
								@Component({
							 | 
						|
								  components: {
							 | 
						|
								    QrcodeStream,
							 | 
						|
								    QRCodeVue3,
							 | 
						|
								    QuickNav,
							 | 
						|
								  },
							 | 
						|
								})
							 | 
						|
								export default class ContactQRScanShow extends Vue {
							 | 
						|
								  $notify!: (notification: NotificationIface, timeout?: number) => void;
							 | 
						|
								
							 | 
						|
								  activeDid = "";
							 | 
						|
								  apiServer = "";
							 | 
						|
								  givenName = "";
							 | 
						|
								  hideRegisterPromptOnNewContact = false;
							 | 
						|
								  isRegistered = false;
							 | 
						|
								  qrValue = "";
							 | 
						|
								
							 | 
						|
								  ETHR_DID_PREFIX = ETHR_DID_PREFIX;
							 | 
						|
								
							 | 
						|
								  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) || "";
							 | 
						|
								    this.hideRegisterPromptOnNewContact =
							 | 
						|
								      !!settings?.hideRegisterPromptOnNewContact;
							 | 
						|
								    this.isRegistered = !!settings?.isRegistered;
							 | 
						|
								
							 | 
						|
								    await accountsDB.open();
							 | 
						|
								    const accounts = await accountsDB.accounts.toArray();
							 | 
						|
								    const account = R.find((acc) => acc.did === this.activeDid, accounts);
							 | 
						|
								    if (account) {
							 | 
						|
								      const publicKeyHex = account.publicKeyHex;
							 | 
						|
								      const publicEncKey = Buffer.from(publicKeyHex, "hex").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,
							 | 
						|
								          profileImageUrl: settings?.profileImageUrl,
							 | 
						|
								          registered: settings?.isRegistered,
							 | 
						|
								        },
							 | 
						|
								      };
							 | 
						|
								
							 | 
						|
								      if (account?.mnemonic && account?.derivationPath) {
							 | 
						|
								        const newDerivPath = nextDerivationPath(
							 | 
						|
								          account.derivationPath as string,
							 | 
						|
								        );
							 | 
						|
								        const nextPublicHex = deriveAddress(
							 | 
						|
								          account.mnemonic as string,
							 | 
						|
								          newDerivPath,
							 | 
						|
								        )[2];
							 | 
						|
								        const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
							 | 
						|
								        const nextPublicEncKeyHash = sha256(nextPublicEncKey);
							 | 
						|
								        const nextPublicEncKeyHashBase64 =
							 | 
						|
								          Buffer.from(nextPublicEncKeyHash).toString("base64");
							 | 
						|
								        contactInfo.own.nextPublicEncKeyHash = nextPublicEncKeyHashBase64;
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      const vcJwt = await createEndorserJwtForDid(this.activeDid, contactInfo);
							 | 
						|
								
							 | 
						|
								      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,
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   *
							 | 
						|
								   * @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);
							 | 
						|
								          newContact.seesMe = true; // didn't work inside setVisibility
							 | 
						|
								          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,
							 | 
						|
								        );
							 | 
						|
								
							 | 
						|
								        if (this.isRegistered) {
							 | 
						|
								          if (!this.hideRegisterPromptOnNewContact && !newContact.registered) {
							 | 
						|
								            setTimeout(() => {
							 | 
						|
								              this.$notify(
							 | 
						|
								                {
							 | 
						|
								                  group: "modal",
							 | 
						|
								                  type: "confirm",
							 | 
						|
								                  title: "Register",
							 | 
						|
								                  text: "Do you want to register them?",
							 | 
						|
								                  onCancel: async (stopAsking: boolean) => {
							 | 
						|
								                    if (stopAsking) {
							 | 
						|
								                      db.settings.update(MASTER_SETTINGS_KEY, {
							 | 
						|
								                        hideRegisterPromptOnNewContact: stopAsking,
							 | 
						|
								                      });
							 | 
						|
								                      this.hideRegisterPromptOnNewContact = stopAsking;
							 | 
						|
								                    }
							 | 
						|
								                  },
							 | 
						|
								                  onNo: async (stopAsking: boolean) => {
							 | 
						|
								                    if (stopAsking) {
							 | 
						|
								                      db.settings.update(MASTER_SETTINGS_KEY, {
							 | 
						|
								                        hideRegisterPromptOnNewContact: stopAsking,
							 | 
						|
								                      });
							 | 
						|
								                      this.hideRegisterPromptOnNewContact = stopAsking;
							 | 
						|
								                    }
							 | 
						|
								                  },
							 | 
						|
								                  onYes: async () => {
							 | 
						|
								                    await this.register(newContact);
							 | 
						|
								                  },
							 | 
						|
								                  promptToStopAsking: true,
							 | 
						|
								                },
							 | 
						|
								                -1,
							 | 
						|
								              );
							 | 
						|
								            }, 500);
							 | 
						|
								          }
							 | 
						|
								        }
							 | 
						|
								      } 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);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async register(contact: Contact) {
							 | 
						|
								    this.$notify(
							 | 
						|
								      {
							 | 
						|
								        group: "alert",
							 | 
						|
								        type: "toast",
							 | 
						|
								        text: "",
							 | 
						|
								        title: "Registration submitted...",
							 | 
						|
								      },
							 | 
						|
								      1000,
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								      const regResult = await register(
							 | 
						|
								        this.activeDid,
							 | 
						|
								        this.apiServer,
							 | 
						|
								        this.axios,
							 | 
						|
								        contact,
							 | 
						|
								      );
							 | 
						|
								      if (regResult.success) {
							 | 
						|
								        contact.registered = true;
							 | 
						|
								        db.contacts.update(contact.did, { registered: true });
							 | 
						|
								
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "success",
							 | 
						|
								            title: "Registration Success",
							 | 
						|
								            text:
							 | 
						|
								              (contact.name || "That unnamed person") + " has been registered.",
							 | 
						|
								          },
							 | 
						|
								          5000,
							 | 
						|
								        );
							 | 
						|
								      } else {
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "danger",
							 | 
						|
								            title: "Registration Error",
							 | 
						|
								            text:
							 | 
						|
								              (regResult.error as string) ||
							 | 
						|
								              "Something went wrong during registration.",
							 | 
						|
								          },
							 | 
						|
								          5000,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								    } catch (error) {
							 | 
						|
								      console.error("Error when registering:", error);
							 | 
						|
								      let userMessage = "There was an error. See logs for more info.";
							 | 
						|
								      const serverError = error as AxiosError;
							 | 
						|
								      if (serverError) {
							 | 
						|
								        if (serverError.response?.data?.error?.message) {
							 | 
						|
								          userMessage = serverError.response.data.error.message;
							 | 
						|
								        } else if (serverError.message) {
							 | 
						|
								          userMessage = serverError.message; // Info for the user
							 | 
						|
								        } else {
							 | 
						|
								          userMessage = JSON.stringify(serverError.toJSON());
							 | 
						|
								        }
							 | 
						|
								      } else {
							 | 
						|
								        userMessage = error as string;
							 | 
						|
								      }
							 | 
						|
								      // Now set that error for the user to see.
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Registration Error",
							 | 
						|
								          text: userMessage,
							 | 
						|
								        },
							 | 
						|
								        5000,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // 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,
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  onCopyUrlToClipboard() {
							 | 
						|
								    //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,
							 | 
						|
								        );
							 | 
						|
								      });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  onCopyDidToClipboard() {
							 | 
						|
								    //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
							 | 
						|
								    useClipboard()
							 | 
						|
								      .copy(this.activeDid)
							 | 
						|
								      .then(() => {
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "info",
							 | 
						|
								            title: "Copied",
							 | 
						|
								            text: "Your DID was copied to the clipboard. Have them paste it on their 'People' screen to add you.",
							 | 
						|
								          },
							 | 
						|
								          10000,
							 | 
						|
								        );
							 | 
						|
								      });
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</script>
							 | 
						|
								
							 |