<template> <div v-if="visible" class="dialog-overlay"> <div class="dialog"> <h1 class="text-xl font-bold text-center mb-4"> {{ customTitle }} </h1> <input type="text" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" :placeholder="prompt || 'What was given?'" 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 id="inputGivenAmount" 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="mt-4 flex justify-center"> <span> <router-link :to="{ name: 'gifted-details', query: { amountInput, description, giverDid: giver?.did, giverName: giver?.name, offerId, projectId, recipientDid: receiver?.did, recipientName: receiver?.name, unitCode, }, }" class="text-blue-500" > Photo & more options ... </router-link> </span> </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-lg font-bold uppercase 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-2 py-3 rounded-md" @click="confirm" > Sign & Send </button> <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> </div> </div> </template> <script lang="ts"> import { Vue, Component, Prop } from "vue-facing-decorator"; import { NotificationIface } from "@/constants/app"; import { createAndSubmitGive, didInfo } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { accountsDB, db } from "@/db/index"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { Contact } from "@/db/tables/contacts"; @Component export default class GiftedDialog extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; @Prop projectId = ""; activeDid = ""; allContacts: Array<Contact> = []; allMyDids: Array<string> = []; apiServer = ""; amountInput = "0"; callbackOnSuccess?: (amount: number) => void = () => {}; customTitle?: string; description = ""; giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent isTrade = false; offerId = ""; prompt = ""; receiver?: libsUtil.GiverReceiverInputInfo; unitCode = "HUR"; visible = false; libsUtil = libsUtil; async open( giver?: libsUtil.GiverReceiverInputInfo, receiver?: libsUtil.GiverReceiverInputInfo, offerId?: string, customTitle?: string, prompt?: string, callbackOnSuccess?: (amount: number) => void, ) { this.customTitle = customTitle; this.giver = giver; this.prompt = prompt || ""; this.receiver = receiver; // if we show "given to user" selection, default checkbox to true this.amountInput = "0"; this.callbackOnSuccess = callbackOnSuccess; this.offerId = offerId || ""; try { await db.open(); const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.apiServer = settings?.apiServer || ""; this.activeDid = settings?.activeDid || ""; this.allContacts = await db.contacts.toArray(); await accountsDB.open(); const allAccounts = await accountsDB.accounts.toArray(); this.allMyDids = allAccounts.map((acc) => acc.did); if (this.giver && !this.giver.name) { this.giver.name = didInfo( this.giver.did, this.activeDid, this.allMyDids, this.allContacts, ); } // 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, ); } this.visible = true; } close() { // close the dialog but don't change values (since it might be submitting info) this.visible = false; } 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() { this.close(); this.eraseValues(); } eraseValues() { this.description = ""; this.giver = undefined; this.amountInput = "0"; this.prompt = ""; this.unitCode = "HUR"; } async confirm() { if (!this.activeDid) { this.$notify( { group: "alert", type: "danger", title: "Error", text: "You must select an identifier before you can record a give.", }, 3000, ); 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.close(); 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.recordGive( (this.giver?.did as string) || null, (this.receiver?.did as string) || null, this.description, parseFloat(this.amountInput), this.unitCode, ).then(() => { this.eraseValues(); }); } /** * * @param giverDid may be null * @param recipientDid may be null * @param description may be an empty string * @param amount may be 0 * @param unitCode may be omitted, defaults to "HUR" */ async recordGive( giverDid: string | null, recipientDid: string | null, description: string, amount: number, unitCode: string = "HUR", ) { try { const result = await createAndSubmitGive( this.axios, this.apiServer, this.activeDid, giverDid as string, recipientDid as string, description, amount, unitCode, this.projectId, this.offerId, this.isTrade, ); if ( result.type === "error" || this.isGiveCreationError(result.response) ) { const errorMessage = this.getGiveCreationErrorMessage(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.`, }, 7000, ); if (this.callbackOnSuccess) { this.callbackOnSuccess(amount); } } // 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, ); } } // 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 isGiveCreationError(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 getGiveCreationErrorMessage(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> <style> .dialog-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; padding: 1.5rem; } .dialog { background-color: white; padding: 1rem; border-radius: 0.5rem; width: 100%; max-width: 500px; } </style>