<template> <QuickNav selected="Home"></QuickNav> <!-- CONTENT --> <section id="Content" class="p-6 pb-24"> <!-- Breadcrumb --> <div id="ViewBreadcrumb" class="mb-8"> <h1 class="text-lg text-center font-light relative px-7"> <!-- Back --> <router-link :to="{ name: 'home' }" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" ><fa icon="chevron-left" class="fa-fw"></fa ></router-link> Give to Contacts </h1> </div> <!-- Quick Search --> <!-- Initial Loading Animation --> <!-- Results List --> <ul class="border-t border-slate-300"> <li class="border-b border-slate-300 py-3"> <h2 class="text-base flex gap-4 items-center"> <span class="grow italic text-slate-500" ><EntityIcon :entityId="null" :iconSize="32" class="inline-block align-middle border border-slate-300 rounded-md mr-1" ></EntityIcon> Anonymous </span> <span class="text-right"> <button type="button" @click="openDialog()" class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md" > <fa icon="gift" class="fa-fw"></fa> </button> </span> </h2> </li> <li v-for="contact in allContacts" :key="contact.did" class="border-b border-slate-300 py-3" > <h2 class="text-base flex gap-4 items-center"> <span class="grow font-semibold" ><EntityIcon :entityId="contact.did" :iconSize="32" class="inline-block align-middle border border-slate-300 rounded-md mr-1" ></EntityIcon> {{ contact.name || "(no name)" }} </span> <span class="text-right"> <button type="button" @click="openDialog(contact)" class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md" > <fa icon="gift" class="fa-fw"></fa> </button> </span> </h2> </li> </ul> <GiftedDialog ref="customDialog" @dialog-result="handleDialogResult" message="Received from" > </GiftedDialog> </section> </template> <script lang="ts"> import { Component, Vue } from "vue-facing-decorator"; import GiftedDialog from "@/components/GiftedDialog.vue"; import { db, accountsDB } from "@/db/index"; import { AccountsSchema } from "@/db/tables/accounts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { accessToken } from "@/libs/crypto"; import { createAndSubmitGive, CreateAndSubmitGiveResult, ErrorResult, GiverInputInfo, GiverOutputInfo, } from "@/libs/endorserServer"; import { Contact } from "@/db/tables/contacts"; import QuickNav from "@/components/QuickNav.vue"; import EntityIcon from "@/components/EntityIcon.vue"; import { IIdentifier } from "@veramo/core"; interface Notification { group: string; type: string; title: string; text: string; } @Component({ components: { GiftedDialog, QuickNav, EntityIcon }, }) export default class ContactGiftingView extends Vue { $notify!: (notification: Notification, timeout?: number) => void; activeDid = ""; allContacts: Array<Contact> = []; apiServer = ""; accounts: typeof AccountsSchema; numAccounts = 0; async beforeCreate() { accountsDB.open(); this.numAccounts = await accountsDB.accounts.count(); } public async getIdentity(activeDid: string) { 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: IIdentifier) { const token = await accessToken(identity); const headers = { "Content-Type": "application/json", Authorization: "Bearer " + token, }; return headers; } async created() { try { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.apiServer = settings?.apiServer || ""; this.activeDid = settings?.activeDid || ""; this.allContacts = await db.contacts.toArray(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { this.$notify( { group: "alert", type: "danger", title: "Error", text: err.message || "There was an error retrieving the latest sweet, sweet action.", }, -1, ); } } openDialog(giver: GiverInputInfo) { (this.$refs.customDialog as GiftedDialog).open(giver); } handleDialogResult(result: GiverOutputInfo) { if (result.action === "confirm") { return new Promise((resolve) => { this.recordGive( result.giver?.did, result.description, result.hours, ).then(() => { resolve(null); }); }); } else { // action was "cancel" so do nothing } } /** * * @param giverDid may be null * @param description may be an empty string * @param hours may be 0 */ public async recordGive( giverDid?: string, description?: string, hours?: number, ) { if (!this.activeDid) { this.$notify( { group: "alert", type: "danger", title: "Error", text: "You must select an identity before you can record a give.", }, -1, ); return; } if (!description && !hours) { this.$notify( { group: "alert", type: "danger", title: "Error", text: "You must enter a description or some number of hours.", }, -1, ); return; } try { const identity = await this.getIdentity(this.activeDid); const result = await createAndSubmitGive( this.axios, this.apiServer, identity, giverDid, this.activeDid, description, hours, ); if (this.isGiveCreationError(result)) { const errorMessage = this.getGiveCreationErrorMessage(result); console.log("Error with give result:", result); this.$notify( { group: "alert", type: "danger", title: "Error", text: errorMessage || "There was an error recording the give.", }, -1, ); } else { this.$notify( { group: "alert", type: "success", title: "Success", text: "That gift was recorded.", }, -1, ); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { console.log("Error with give caught:", error); const message = error.userMessage || error.response?.data?.error?.message || "There was an error recording the Give."; this.$notify( { group: "alert", type: "danger", title: "Error", text: message, }, -1, ); } } // Helper functions for readability isGiveCreationError(result: CreateAndSubmitGiveResult) { return result.type == "error"; } getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) { return (result as ErrorResult).error?.userMessage; } } </script>