<template> <QuickNav /> <TopMessage /> <!-- CONTENT --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <!-- Back --> <div v-if="!hideBackButton" 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="cancelBack()" > <fa icon="chevron-left" class="fa-fw"></fa> </h1> </div> <!-- Heading --> <h1 class="text-4xl text-center font-light px-4 mb-4">What Was Offered</h1> <h1 class="text-xl font-bold text-center mb-4"> <span>From {{ giverName }}</span> <span> to {{ offeredToProject ? projectName : offeredToRecipient ? recipientName : "someone unidentified" }}</span > </h1> <textarea class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" placeholder="What was offered" v-model="description" /> <div class="flex flex-row justify-center"> <span class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" @click="changeUnitCode()" > {{ libsUtil.UNIT_SHORT[unitCode] || unitCode }} </span> <div class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" @click="amountInput === '0' ? null : decrement()" > <fa icon="chevron-left" /> </div> <input type="number" class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" v-model="amountInput" /> <div class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" @click="increment()" > <fa icon="chevron-right" /> </div> </div> <div class="h-7 mt-4 flex"> <input v-if="projectId && !offeredToRecipient" type="checkbox" class="h-6 w-6 mr-2" v-model="offeredToProject" /> <fa v-else icon="square" class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded" @click="notifyUserOfProject()" /> <label class="text-sm mt-1"> {{ projectId ? "This was given to " + projectName : "No project was chosen" }} </label> </div> <div class="h-7 mt-4 flex"> <input v-if="recipientDid && !offeredToProject" type="checkbox" class="h-6 w-6 mr-2" v-model="offeredToRecipient" /> <fa v-else icon="square" class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded" @click="notifyUserOfRecipient()" /> <label class="text-sm mt-1"> {{ recipientDid ? "This was given to " + recipientName : "No recipient was chosen." }} </label> </div> <div class="mt-4 flex"> <router-link :to="{ name: 'claim-add-raw', query: { claim: constructOfferParam(), }, }" class="text-blue-500" > Edit & Submit Raw </router-link> </div> <p class="text-center mb-2 mt-6 italic"> Sign & Send to publish to the world <fa icon="circle-info" class="pl-2 text-blue-500 cursor-pointer" @click="explainData()" /> </p> <div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <button class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" @click="cancel" > Cancel </button> </div> </section> </template> <script lang="ts"> import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import QuickNav from "@/components/QuickNav.vue"; import TopMessage from "@/components/TopMessage.vue"; import { NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { createAndSubmitOffer, didInfo, editAndSubmitOffer, GenericCredWrapper, getPlanFromCache, hydrateOffer, OfferVerifiableCredential, } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { Contact } from "@/db/tables/contacts"; @Component({ components: { QuickNav, TopMessage, }, }) export default class OfferDetails extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; activeDid = ""; apiServer = ""; amountInput = "0"; description = ""; destinationPathAfter = ""; offeredToProject = false; offeredToRecipient = false; giverDid: string | undefined; giverName = ""; hideBackButton = false; isTrade = false; message = ""; offerId = ""; prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>; projectId = ""; projectName = "a project"; recipientDid = ""; recipientName = ""; unitCode = "HUR"; validThroughDate = null; libsUtil = libsUtil; async mounted() { try { this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"] ? (JSON.parse( (this.$route as Router).query["prevCredToEdit"], ) as GenericCredWrapper<OfferVerifiableCredential>) : undefined; } catch (error) { this.$notify( { group: "alert", type: "danger", title: "Retrieval Error", text: "The previous record isn't available for editing. If you submit, you'll create a new record.", }, 6000, ); } const prevAmount = this.prevCredToEdit?.claim?.object?.amountOfThisGood; this.amountInput = (this.$route as Router).query["amountInput"] || (prevAmount ? String(prevAmount) : "") || this.amountInput; this.description = (this.$route as Router).query["description"] || this.prevCredToEdit?.claim?.description || this.description; this.destinationPathAfter = (this.$route as Router).query[ "destinationPathAfter" ]; this.giverDid = ((this.$route as Router).query["giverDid"] || this.prevCredToEdit?.claim?.agent?.identifier || this.giverDid) as string; this.giverName = ((this.$route as Router).query["giverName"] as string) || ""; this.hideBackButton = (this.$route as Router).query["hideBackButton"] === "true"; this.message = ((this.$route as Router).query["message"] as string) || ""; // find any offer ID const fulfills = this.prevCredToEdit?.claim?.fulfills; const fulfillsArray = Array.isArray(fulfills) ? fulfills : fulfills ? [fulfills] : []; const offer = fulfillsArray.find((rec) => rec["@type"] === "Offer"); this.offerId = ((this.$route as Router).query["offerId"] || offer?.identifier || this.offerId) as string; // find any project ID const project = fulfillsArray.find((rec) => rec["@type"] === "PlanAction"); this.projectId = ((this.$route as Router).query["projectId"] || project?.identifier || this.projectId) as string; this.recipientDid = ((this.$route as Router).query["recipientDid"] || this.prevCredToEdit?.claim?.recipient?.identifier) as string; this.recipientName = ((this.$route as Router).query["recipientName"] as string) || ""; this.unitCode = ((this.$route as Router).query["unitCode"] || this.prevCredToEdit?.claim?.object?.unitCode || this.unitCode) as string; // this is an endpoint for sharing project info to highlight something given // https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target if ((this.$route as Router).query["shareTitle"]) { this.description = ((this.$route as Router).query["shareTitle"] as string) + (this.description ? "\n" + this.description : ""); } if ((this.$route as Router).query["shareText"]) { this.description = (this.description ? this.description + "\n" : "") + ((this.$route as Router).query["shareText"] as string); } if ((this.$route as Router).query["shareUrl"]) { this.imageUrl = (this.$route as Router).query["shareUrl"] as string; } try { await db.open(); const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.apiServer = settings?.apiServer || ""; this.activeDid = settings?.activeDid || ""; let allContacts: Contact[] = []; let allMyDids: string[] = []; if ( (this.giverDid && !this.giverName) || (this.recipientDid && !this.recipientName) ) { allContacts = await db.contacts.toArray(); await accountsDB.open(); const allAccounts = await accountsDB.accounts.toArray(); allMyDids = allAccounts.map((acc) => acc.did); if (this.giverDid && !this.giverName) { this.giverName = didInfo( this.giverDid, this.activeDid, allMyDids, allContacts, ); } if (this.recipientDid && !this.recipientName) { this.recipientName = didInfo( this.recipientDid, this.activeDid, allMyDids, allContacts, ); } } this.offeredToProject = !!this.projectId; this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { console.error("Error retrieving settings from database:", err); this.$notify( { group: "alert", type: "danger", title: "Error", text: err.message || "There was an error retrieving your settings.", }, -1, ); } if (this.projectId) { // console.log("Getting project name from cache", this.projectId); const project = await getPlanFromCache( this.projectId, this.axios, this.apiServer, this.activeDid, ); this.projectName = project?.name ? "the project: " + project.name : "a project"; } } changeUnitCode() { const units = Object.keys(this.libsUtil.UNIT_SHORT); const index = units.indexOf(this.unitCode); this.unitCode = units[(index + 1) % units.length]; } increment() { this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`; } decrement() { this.amountInput = `${Math.max( 0, (parseFloat(this.amountInput) || 1) - 1, )}`; } cancel() { if (this.destinationPathAfter) { (this.$router as Router).push({ path: this.destinationPathAfter }); } else { (this.$router as Router).back(); } } cancelBack() { (this.$router as Router).back(); } async confirm() { if (!this.activeDid) { this.$notify( { group: "alert", type: "danger", title: "Error", text: "You must select an identifier before you can record a offer.", }, 2000, ); return; } if (parseFloat(this.amountInput) < 0) { this.$notify( { group: "alert", type: "danger", text: "You may not send a negative number.", title: "", }, 2000, ); return; } if (!this.description && !parseFloat(this.amountInput)) { this.$notify( { group: "alert", type: "danger", title: "Error", text: `You must enter a description or some number of ${ this.libsUtil.UNIT_LONG[this.unitCode] }.`, }, 2000, ); return; } this.$notify( { group: "alert", type: "toast", text: "Recording the give...", title: "", }, 1000, ); // this is asynchronous, but we don't need to wait for it to complete await this.recordOffer(); } notifyUserOfProject() { if (!this.projectId) { this.$notify( { group: "alert", type: "warning", title: "Error", text: "To assign to a project, you must open this dialog through a project.", }, 3000, ); } else { // must be because offeredToRecipient is true this.$notify( { group: "alert", type: "warning", title: "Error", text: "You cannot assign both to a project and to a recipient.", }, 3000, ); } } notifyUserOfRecipient() { if (!this.recipientDid) { this.$notify( { group: "alert", type: "warning", title: "Error", text: "To assign to a recipient, you must open this dialog from a contact.", }, 3000, ); } else { // must be because offeredToProject is true this.$notify( { group: "alert", type: "warning", title: "Error", text: "You cannot assign both to a recipient and to a project.", }, 3000, ); } } /** * * @param giverDid may be null * @param description may be an empty string * @param amountInput may be 0 * @param unitCode may be omitted, defaults to "HUR" */ public async recordOffer() { try { const recipientDid = this.offeredToRecipient ? this.recipientDid : undefined; const projectId = this.offeredToProject ? this.projectId : undefined; let result; if (this.prevCredToEdit) { // don't create from a blank one in case some properties were set from a different interface result = await editAndSubmitOffer( this.axios, this.apiServer, this.prevCredToEdit, this.activeDid, this.description, parseFloat(this.amountInput), this.unitCode, this.validThroughDate, recipientDid, projectId, ); } else { result = await createAndSubmitOffer( this.axios, this.apiServer, this.activeDid, this.description, parseFloat(this.amountInput), this.unitCode, this.validThroughDate, recipientDid, projectId, ); } if (result.type === "error" || this.isCreationError(result.response)) { const errorMessage = this.getCreationErrorMessage(result); console.error("Error with give creation result:", result); this.$notify( { group: "alert", type: "danger", title: "Error", text: errorMessage || "There was an error creating the give.", }, -1, ); } else { this.$notify( { group: "alert", type: "success", title: "Success", text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`, }, 5000, ); localStorage.removeItem("imageUrl"); if (this.destinationPathAfter) { (this.$router as Router).push({ path: this.destinationPathAfter }); } else { (this.$router as Router).back(); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { console.error("Error with give recordation caught:", error); const errorMessage = error.userMessage || error.response?.data?.error?.message || "There was an error recording the give."; this.$notify( { group: "alert", type: "danger", title: "Error", text: errorMessage, }, -1, ); } } constructOfferParam() { const recipientDid = this.offeredToRecipient ? this.recipientDid : undefined; const projectId = this.offeredToProject ? this.projectId : undefined; const giveClaim = hydrateOffer( this.prevCredToEdit?.claim as OfferVerifiableCredential, this.activeDid, recipientDid, this.description, parseFloat(this.amountInput), this.unitCode, "", projectId, this.validThroughDate, this.prevCredToEdit?.id as string, ); const claimStr = JSON.stringify(giveClaim); return claimStr; } // Helper functions for readability /** * @param result response "data" from the server * @returns true if the result indicates an error */ // eslint-disable-next-line @typescript-eslint/no-explicit-any isCreationError(result: any) { return result.status !== 201 || result.data?.error; } /** * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data") * @returns best guess at an error message */ // eslint-disable-next-line @typescript-eslint/no-explicit-any getCreationErrorMessage(result: any) { return ( result.error?.userMessage || result.error?.error || result.response?.data?.error?.message ); } explainData() { this.$notify( { group: "alert", type: "success", title: "Data Sharing", text: libsUtil.PRIVACY_MESSAGE, }, -1, ); } } </script>