From bd148e88a3edf216a1cb5579ceda3b046040c4bd Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 27 Apr 2024 20:33:10 -0600 Subject: [PATCH] for scan on QR code screen, import and keep on that screen --- src/libs/endorserServer.ts | 47 +++++++++ src/libs/util.ts | 2 +- src/views/AccountViewView.vue | 13 ++- src/views/ContactQRScanShowView.vue | 155 ++++++++++++++++++++++------ src/views/ContactsView.vue | 140 ++++++++----------------- 5 files changed, 223 insertions(+), 134 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index c450389..3d06d35 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -6,6 +6,8 @@ import { IIdentifier } from "@veramo/core"; import { Contact } from "@/db/tables/contacts"; import { accessToken, SimpleSigner } from "@/libs/crypto"; +import { NonsensitiveDexie } from "@/db/index"; +import { getIdentity } from "@/libs/util"; export const SCHEMA_ORG_CONTEXT = "https://schema.org"; // the object in RegisterAction claims @@ -706,6 +708,12 @@ export async function createAndSubmitClaim( } } +/** + * An AcceptAction is when someone accepts some contract or pledge. + * + * @param claim has properties '@context' & '@type' + * @return true if the claim is a schema.org AcceptAction + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export const isAccept = (claim: Record) => { return ( @@ -902,3 +910,42 @@ export const bvcMeetingJoinClaim = (did: string, startTime: string) => { }, }; }; + +export async function setVisibilityUtil( + activeDid: string, + apiServer: string, + axios: Axios, + db: NonsensitiveDexie, + contact: Contact, + visibility: boolean, +) { + if (!activeDid) { + return { error: "Cannot set visibility without an identifier." }; + } + const url = + apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe"); + const identity = await getIdentity(activeDid); + const headers = await getHeaders(identity); + const payload = JSON.stringify({ did: contact.did }); + + try { + const resp = await axios.post(url, payload, { headers }); + if (resp.status === 200) { + contact.seesMe = visibility; + db.contacts.update(contact.did, { seesMe: visibility }); + return { success: true }; + } else { + console.error( + "Got some bad server response when setting visibility: ", + resp.status, + resp, + ); + const message = + resp.data.error?.message || "Got some error setting visibility."; + return { error: message }; + } + } catch (err) { + console.error("Got some error when setting visibility:", err); + return { error: "Check connectivity and try again." }; + } +} diff --git a/src/libs/util.ts b/src/libs/util.ts index 1082317..0bd8016 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -201,7 +201,7 @@ export const getIdentity = async (activeDid: string): Promise => { if (!identity) { throw new Error( - `Attempted to load Offer records for DID ${activeDid} but no identifier was found`, + `Attempted to load identity ${activeDid} but no identifier was found`, ); } return identity; diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 7b39a70..416150a 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -55,7 +55,7 @@

Create An Identifier @@ -71,10 +71,13 @@ - + Set Your Name @@ -163,7 +166,7 @@

Share Your Info @@ -1168,7 +1171,7 @@ export default class AccountViewView extends Vue { this.$notify( { group: "alert", - type: "warning", + type: "danger", title: "Update Error", text: "Unable to update your settings. Check claim limits again.", }, diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 17cf141..a335ad5 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -79,16 +79,25 @@ 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 { 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 } 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({ @@ -106,29 +115,12 @@ export default class ContactQRScanShow extends Vue { 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 || ""; + 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(); @@ -172,25 +164,109 @@ export default class ContactQRScanShow extends Vue { } } + 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 - onScanDetect(content: any) { + async onScanDetect(content: any) { const url = content[0]?.rawValue; if (url) { + let newContact: Contact; try { - localStorage.setItem("contactEndorserUrl", url); - this.$router.push({ name: "contacts" }); + 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, + }; + 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: "warning", - title: "Invalid Contact QR Code", - text: "The QR code isn't in the right format.", + type: "danger", + title: "Contact Error", + text: "Could not save contact info. Check if it already exists.", }, 5000, ); @@ -199,7 +275,7 @@ export default class ContactQRScanShow extends Vue { this.$notify( { group: "alert", - type: "warning", + type: "danger", title: "Invalid Contact QR Code", text: "No QR code detected with contact information.", }, @@ -208,13 +284,29 @@ export default class ContactQRScanShow extends Vue { } } + 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: "warning", + type: "danger", title: "Invalid Scan", text: "The scan was invalid.", }, @@ -223,6 +315,7 @@ export default class ContactQRScanShow extends Vue { } onCopyToClipboard() { + //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing useClipboard() .copy(this.qrValue) .then(() => { diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 7cbc880..7e257ab 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -325,6 +325,7 @@ import { isDid, RegisterVerifiableCredential, SERVICE_ID, + setVisibilityUtil, } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import QuickNav from "@/components/QuickNav.vue"; @@ -344,7 +345,6 @@ export default class ContactsView extends Vue { activeDid = ""; apiServer = ""; contacts: Array = []; - contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || ""; contactInput = ""; contactEdit: Contact | null = null; contactNewName = ""; @@ -388,12 +388,18 @@ export default class ContactsView extends Vue { this.contacts = baseContacts.sort((a, b) => (a.name || "").localeCompare(b.name || ""), ); + } - if (this.contactEndorserUrl) { - await this.addContactFromScan(this.contactEndorserUrl); - localStorage.removeItem("contactEndorserUrl"); - this.contactEndorserUrl = ""; - } + danger(message: string, title: string = "Error", timeout = 5000) { + this.$notify( + { + group: "alert", + type: "danger", + title: title, + text: message, + }, + timeout, + ); } public async getIdentity(activeDid: string): Promise { @@ -528,22 +534,14 @@ export default class ContactsView extends Vue { title: "Load Error", text: "Got an error loading your gives.", }, - -1, + 5000, ); } } async onClickNewContact(): Promise { if (!this.contactInput) { - this.$notify( - { - group: "alert", - type: "warning", - title: "No Contact", - text: "There was no contact info to add.", - }, - 3000, - ); + this.danger("There was no contact info to add.", "No Contact"); return; } @@ -573,15 +571,7 @@ export default class ContactsView extends Vue { 3000, // keeping it up so that the "visibility" message is seen ); } catch (e) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Contacts Maybe Added", - text: "An error occurred. Some contacts may have been added.", - }, - -1, - ); + this.danger("An error occurred. Some contacts may have been added."); } // .orderBy("name") wouldn't retrieve any entries with a blank name @@ -697,30 +687,13 @@ export default class ContactsView extends Vue { async addContact(newContact: Contact) { if (!newContact.did) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Incomplete Contact", - text: "Cannot add a contact without a DID.", - }, - 5000, - ); + this.danger("Cannot add a contact without a DID.", "Incomplete Contact"); return; } if (!isDid(newContact.did)) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Invalid DID", - text: "The DID is not valid. It must begin with 'did:'", - }, - 5000, - ); + this.danger("The DID must begin with 'did:'", "Invalid DID"); return; } - newContact.seesMe = true; // since we will immediately set that on the server return db.contacts .add(newContact) .then(() => { @@ -737,6 +710,7 @@ export default class ContactsView extends Vue { } else { addedMessage = "They were added."; } + this.contactInput = ""; if (this.isRegistered) { this.$notify( { @@ -771,15 +745,7 @@ export default class ContactsView extends Vue { message += " Check that the contact doesn't conflict with any you already have."; } - this.$notify( - { - group: "alert", - type: "danger", - title: "Contact Not Added", - text: message, - }, - -1, - ); + this.danger(message, "Contact Not Added", -1); }); } @@ -962,63 +928,42 @@ export default class ContactsView extends Vue { visibility: boolean, showSuccessAlert: boolean, ) { - const url = - this.apiServer + - "/api/report/" + - (visibility ? "canSeeMe" : "cannotSeeMe"); - const identity = await this.getIdentity(this.activeDid); - const headers = await this.getHeaders(identity); - const payload = JSON.stringify({ did: contact.did }); - - try { - const resp = await this.axios.post(url, payload, { headers }); - if (resp.status === 200) { - if (showSuccessAlert) { - this.$notify( - { - group: "alert", - type: "success", - title: "Visibility Set", - text: - this.nameForDid(this.contacts, contact.did) + - " can " + - (visibility ? "" : "not ") + - "see your activity.", - }, - 3000, - ); - } - contact.seesMe = visibility; - db.contacts.update(contact.did, { seesMe: visibility }); - } else { - console.error( - "Got some bad server response when setting visibility: ", - resp.status, - resp, - ); - const message = - resp.data.error?.message || "Got some error setting visibility."; + const result = await setVisibilityUtil( + this.activeDid, + this.apiServer, + this.axios, + db, + contact, + visibility, + ); + if (result.success) { + if (showSuccessAlert) { this.$notify( { group: "alert", - type: "danger", - title: "Error Setting Visibility", - text: message, + type: "success", + title: "Visibility Set", + text: + (contact.name || "That user") + + " can " + + (visibility ? "" : "not ") + + "see your activity.", }, - 5000, + 3000, ); } - } catch (err) { - console.error("Got some error when setting visibility:", err); + } else if (result.error) { this.$notify( { group: "alert", type: "danger", title: "Error Setting Visibility", - text: "Check connectivity and try again.", + text: result.error as string, }, 5000, ); + } else { + console.error("Got strange result from setting visibility:", result); } } @@ -1087,7 +1032,8 @@ export default class ContactsView extends Vue { private nameForContact(contact?: Contact, capitalize?: boolean): string { return ( - (contact?.name as string) || (capitalize ? "T" : "t") + "his unnamed user" + (contact?.name as string) || + (capitalize ? "This" : "this") + " unnamed user" ); }