|
|
|
<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>
|