<template> <QuickNav selected="Contacts"></QuickNav> <section id="Content" class="p-6 pb-24"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> Given with {{ contact?.name }} </h1> <!-- Results List --> <table class="table-auto w-full border-t border-slate-300 text-sm sm:text-base text-center" > <thead class="bg-slate-100"> <tr class="border-b border-slate-300"> <th></th> <th class="px-1 py-2">From Them</th> <th></th> <th class="px-1 py-2">To Them</th> </tr> </thead> <tbody> <tr v-for="record in giveRecords" :key="record.id" class="border-b border-slate-300" > <td class="p-1 text-xs sm:text-sm text-left text-slate-500"> {{ new Date(record.issuedAt).toLocaleString() }} </td> <td class="p-1"> <span v-if="record.agentDid == contact.did"> <div class="font-bold"> {{ record.amount }} {{ record.unit }} <span v-if="record.amountConfirmed" title="Confirmed"> <fa icon="circle-check" class="text-green-600 fa-fw" /> </span> <button v-else @click="confirm(record)" title="Unconfirmed"> <fa icon="circle" class="text-blue-600 fa-fw" /> </button> </div> <div class="italic text-xs sm:text-sm text-slate-500"> {{ record.description }} </div> </span> </td> <td class="p-1"> <span v-if="record.agentDid == contact.did"> <fa icon="arrow-left" class="text-slate-400 fa-fw" /> </span> <span v-else> <fa icon="arrow-right" class="text-slate-400 fa-fw" /> </span> </td> <td class="p-1"> <span v-if="record.agentDid != contact.did"> <div class="font-bold"> {{ record.amount }} {{ record.unit }} <span v-if="record.amountConfirmed" title="Confirmed"> <fa icon="circle-check" class="text-green-600 fa-fw" /> </span> <button v-else @click="cannotConfirmMessage()" title="Unconfirmed" > <fa icon="circle" class="text-slate-600 fa-fw" /> </button> </div> <div class="italic text-xs sm:text-sm text-slate-500"> {{ record.description }} </div> </span> </td> </tr> </tbody> </table> <AlertMessage :alertTitle="alertTitle" :alertMessage="alertMessage" ></AlertMessage> </section> </template> <script lang="ts"> import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { accountsDB, db } from "@/db"; import { Contact } from "@/db/tables/contacts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { accessToken, SimpleSigner } from "@/libs/crypto"; import { AgreeVerifiableCredential, GiveServerRecord, GiveVerifiableCredential, SCHEMA_ORG_CONTEXT, } from "@/libs/endorserServer"; import * as didJwt from "did-jwt"; import { AxiosError } from "axios"; import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav"; @Component({ components: { AlertMessage, QuickNav } }) export default class ContactsView extends Vue { activeDid = ""; apiServer = ""; contact: Contact | null = null; giveRecords: Array<GiveServerRecord> = []; alertTitle = ""; alertMessage = ""; numAccounts = 0; async beforeCreate() { await accountsDB.open(); this.numAccounts = await accountsDB.accounts.count(); } public async getIdentity(activeDid) { await accountsDB.open(); const account = await accountsDB.accounts .where("did") .equals(activeDid) .first(); const identity = JSON.parse(account?.identity || "null"); if (!identity) { throw new Error( "Attempted to load Give records with no identity available.", ); } return identity; } public async getHeaders(identity) { const token = await accessToken(identity); const headers = { "Content-Type": "application/json", Authorization: "Bearer " + token, }; return headers; } async created() { try { await db.open(); const contactDid = this.$route.query.contactDid as string; this.contact = (await db.contacts.get(contactDid)) || null; const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; if (this.activeDid && this.contact) { this.loadGives(this.activeDid, this.contact); } } catch (err) { this.alertTitle = "Error"; this.alertMessage = err.userMessage || "There was an error retrieving the latest sweet, sweet action."; } } async loadGives(activeDid: string, contact: Contact) { try { const identity = await this.getIdentity(this.activeDid); let result = []; const url = this.apiServer + "/api/v2/report/gives?agentDid=" + encodeURIComponent(identity.did) + "&recipientDid=" + encodeURIComponent(contact.did); const headers = this.getHeaders(identity); const resp = await this.axios.get(url, { headers }); if (resp.status === 200) { result = resp.data.data; } else { console.error( "Got bad response status & data of", resp.status, resp.data, ); this.alertTitle = "Error With Server"; this.alertMessage = "Got an error retrieving your given time from the server."; } const url2 = this.apiServer + "/api/v2/report/gives?agentDid=" + encodeURIComponent(contact.did) + "&recipientDid=" + encodeURIComponent(identity.did); const headers2 = await this.getHeaders(identity); const resp2 = await this.axios.get(url2, { headers: headers2 }); if (resp2.status === 200) { result = R.concat(result, resp2.data.data); } else { console.error( "Got bad response status & data of", resp2.status, resp2.data, ); this.alertTitle = "Error With Server"; this.alertMessage = "Got an error retrieving your given time from the server."; } const sortedResult: Array<GiveServerRecord> = R.sort( (a, b) => new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(), result, ); this.giveRecords = sortedResult; } catch (error) { this.alertTitle = "Error With Server"; this.alertMessage = error as string; } } async confirm(record: GiveServerRecord) { // Make claim // I use clone here because otherwise it gets a Proxy object. // eslint-disable-next-line @typescript-eslint/no-explicit-any const origClaim: GiveVerifiableCredential = R.clone(record.fullClaim); if (record.fullClaim["@context"] == SCHEMA_ORG_CONTEXT) { delete origClaim["@context"]; } origClaim["identifier"] = record.handleId; const vcClaim: AgreeVerifiableCredential = { "@context": SCHEMA_ORG_CONTEXT, "@type": "AgreeAction", object: origClaim, }; // Make a payload for the claim const vcPayload = { vc: { "@context": ["https://www.w3.org/2018/credentials/v1"], type: ["VerifiableCredential"], credentialSubject: vcClaim, }, }; // Create a signature using private key of identity const identity = await this.getIdentity(this.activeDid); if (identity.keys[0].privateKeyHex !== null) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const privateKeyHex: string = identity.keys[0].privateKeyHex!; const signer = await SimpleSigner(privateKeyHex); const alg = undefined; // Create a JWT for the request const vcJwt: string = await didJwt.createJWT(vcPayload, { alg: alg, issuer: identity.did, signer: signer, }); // Make the xhr request payload const payload = JSON.stringify({ jwtEncoded: vcJwt }); const url = this.apiServer + "/api/v2/claim"; const token = await accessToken(identity); const headers = { "Content-Type": "application/json", Authorization: "Bearer " + token, }; try { const resp = await this.axios.post(url, payload, { headers }); if (resp.data?.success) { record.amountConfirmed = origClaim.object?.amountOfThisGood || 1; } } catch (error) { let userMessage = "There was an error. See logs for more info."; const serverError = error as AxiosError; if (serverError) { 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.alertTitle = "Error With Server"; this.alertMessage = userMessage; } } } cannotConfirmMessage() { this.alertTitle = "Not Allowed"; this.alertMessage = "Only the recipient can confirm final receipt."; } } </script> <style> /* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */ /* Tooltip container */ .tooltip { position: relative; display: inline-block; border-bottom: 1px dotted black; /* If you want dots under the hoverable text */ } /* Tooltip text */ .tooltip .tooltiptext { visibility: hidden; width: 200px; background-color: black; color: #fff; text-align: center; padding: 5px 0; border-radius: 6px; position: absolute; z-index: 1; } /* Show the tooltip text when you mouse over the tooltip container */ .tooltip:hover .tooltiptext { visibility: visible; } .tooltip:hover .tooltiptext-left { visibility: visible; } </style>