34 changed files with 1794 additions and 557 deletions
			
			
		| @ -0,0 +1,633 @@ | |||||
|  | <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 Is Offered</h1> | ||||
|  | 
 | ||||
|  |     <h1 class="text-xl font-bold text-center mb-4"> | ||||
|  |       <span> | ||||
|  |         Offer 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 is offered" | ||||
|  |       v-model="itemDescription" | ||||
|  |       data-testId="itemDescription" | ||||
|  |     /> | ||||
|  |     <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" | ||||
|  |         data-testId="inputOfferAmount" | ||||
|  |       /> | ||||
|  |       <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="flex flex-row mt-2"> | ||||
|  |       <span | ||||
|  |         class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2" | ||||
|  |       > | ||||
|  |         Conditions | ||||
|  |       </span> | ||||
|  |       <textarea | ||||
|  |         class="w-full border border-slate-400 px-3 py-2 rounded-r" | ||||
|  |         placeholder="Prerequisites, other people to include, etc." | ||||
|  |         v-model="conditionDescription" | ||||
|  |       /> | ||||
|  |     </div> | ||||
|  | 
 | ||||
|  |     <div class="flex flex-row mt-2"> | ||||
|  |       <span | ||||
|  |         class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2" | ||||
|  |       > | ||||
|  |         {{ validThroughDateInput ? "" : "No" }} Expiration | ||||
|  |       </span> | ||||
|  |       <input | ||||
|  |         v-model="validThroughDateInput" | ||||
|  |         type="date" | ||||
|  |         class="w-full rounded border border-slate-400 px-3 py-2 rounded-r" | ||||
|  |       /> | ||||
|  |     </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 is offered 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 is offered 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-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> | ||||
|  |   </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 OfferDetailsView extends Vue { | ||||
|  |   $notify!: (notification: NotificationIface, timeout?: number) => void; | ||||
|  | 
 | ||||
|  |   activeDid = ""; | ||||
|  |   apiServer = ""; | ||||
|  | 
 | ||||
|  |   amountInput = "0"; | ||||
|  |   conditionDescription = ""; | ||||
|  |   itemDescription = ""; | ||||
|  |   destinationPathAfter = ""; | ||||
|  |   offeredToProject = false; | ||||
|  |   offeredToRecipient = false; | ||||
|  |   offererDid: string | undefined; | ||||
|  |   hideBackButton = false; | ||||
|  |   message = ""; | ||||
|  |   offerId = ""; | ||||
|  |   prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>; | ||||
|  |   projectId = ""; | ||||
|  |   projectName = "a project"; | ||||
|  |   recipientDid = ""; | ||||
|  |   recipientName = ""; | ||||
|  |   unitCode = "HUR"; | ||||
|  |   validThroughDateInput = ""; | ||||
|  | 
 | ||||
|  |   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?.includesObject?.amountOfThisGood; | ||||
|  |     this.amountInput = | ||||
|  |       (this.$route as Router).query["amountInput"] || | ||||
|  |       (prevAmount ? String(prevAmount) : "") || | ||||
|  |       this.amountInput; | ||||
|  |     this.unitCode = ((this.$route as Router).query["unitCode"] || | ||||
|  |       this.prevCredToEdit?.claim?.includesObject?.unitCode || | ||||
|  |       this.unitCode) as string; | ||||
|  | 
 | ||||
|  |     this.conditionDescription = | ||||
|  |       this.prevCredToEdit?.claim?.description || this.conditionDescription; | ||||
|  |     this.itemDescription = | ||||
|  |       (this.$route as Router).query["description"] || | ||||
|  |       this.prevCredToEdit?.claim?.itemOffered?.description || | ||||
|  |       this.itemDescription; | ||||
|  |     this.destinationPathAfter = (this.$route as Router).query[ | ||||
|  |       "destinationPathAfter" | ||||
|  |     ]; | ||||
|  |     this.offererDid = ((this.$route as Router).query["offererDid"] || | ||||
|  |       this.prevCredToEdit?.claim?.agent?.identifier || | ||||
|  |       this.offererDid) as string; | ||||
|  |     this.hideBackButton = | ||||
|  |       (this.$route as Router).query["hideBackButton"] === "true"; | ||||
|  |     this.message = ((this.$route as Router).query["message"] as string) || ""; | ||||
|  | 
 | ||||
|  |     // find any project ID | ||||
|  |     let project; | ||||
|  |     if ( | ||||
|  |       this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] === | ||||
|  |       "PlanAction" | ||||
|  |     ) { | ||||
|  |       project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf; | ||||
|  |     } | ||||
|  |     this.projectId = ((this.$route as Router).query["projectId"] || | ||||
|  |       project?.identifier || | ||||
|  |       this.projectId) as string; | ||||
|  |     this.projectName = ((this.$route as Router).query["projectName"] || | ||||
|  |       project?.name || | ||||
|  |       this.projectName) 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.validThroughDateInput = | ||||
|  |       this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput; | ||||
|  | 
 | ||||
|  |     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.recipientDid && !this.recipientName) { | ||||
|  |         allContacts = await db.contacts.toArray(); | ||||
|  | 
 | ||||
|  |         await accountsDB.open(); | ||||
|  |         const allAccounts = await accountsDB.accounts.toArray(); | ||||
|  |         allMyDids = allAccounts.map((acc) => acc.did); | ||||
|  |         this.recipientName = didInfo( | ||||
|  |           this.recipientDid, | ||||
|  |           this.activeDid, | ||||
|  |           allMyDids, | ||||
|  |           allContacts, | ||||
|  |         ); | ||||
|  |       } | ||||
|  |       // these should be functions but something's wrong with the syntax in the <> conditional | ||||
|  |       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 && !this.projectName) { | ||||
|  |       // 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.itemDescription && !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 offer...", | ||||
|  |         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 page 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 page 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 offererDid 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.itemDescription, | ||||
|  |           parseFloat(this.amountInput), | ||||
|  |           this.unitCode, | ||||
|  |           this.conditionDescription, | ||||
|  |           this.validThroughDateInput, | ||||
|  |           recipientDid, | ||||
|  |           projectId, | ||||
|  |         ); | ||||
|  |       } else { | ||||
|  |         result = await createAndSubmitOffer( | ||||
|  |           this.axios, | ||||
|  |           this.apiServer, | ||||
|  |           this.activeDid, | ||||
|  |           this.itemDescription, | ||||
|  |           parseFloat(this.amountInput), | ||||
|  |           this.unitCode, | ||||
|  |           this.conditionDescription, | ||||
|  |           this.validThroughDateInput, | ||||
|  |           recipientDid, | ||||
|  |           projectId, | ||||
|  |         ); | ||||
|  |       } | ||||
|  | 
 | ||||
|  |       if (result.type === "error" || this.isCreationError(result.response)) { | ||||
|  |         const errorMessage = this.getCreationErrorMessage(result); | ||||
|  |         console.error("Error with offer creation result:", result); | ||||
|  |         this.$notify( | ||||
|  |           { | ||||
|  |             group: "alert", | ||||
|  |             type: "danger", | ||||
|  |             title: "Error", | ||||
|  |             text: errorMessage || "There was an error creating the offer.", | ||||
|  |           }, | ||||
|  |           -1, | ||||
|  |         ); | ||||
|  |       } else { | ||||
|  |         this.$notify( | ||||
|  |           { | ||||
|  |             group: "alert", | ||||
|  |             type: "success", | ||||
|  |             title: "Success", | ||||
|  |             text: `That offer 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 offer recordation caught:", error); | ||||
|  |       const errorMessage = | ||||
|  |         error.userMessage || | ||||
|  |         error.response?.data?.error?.message || | ||||
|  |         "There was an error recording the offer."; | ||||
|  |       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 offerClaim = hydrateOffer( | ||||
|  |       this.prevCredToEdit?.claim as OfferVerifiableCredential, | ||||
|  |       this.activeDid, | ||||
|  |       recipientDid, | ||||
|  |       this.itemDescription, | ||||
|  |       parseFloat(this.amountInput), | ||||
|  |       this.unitCode, | ||||
|  |       this.conditionDescription, | ||||
|  |       projectId, | ||||
|  |       this.validThroughDateInput, | ||||
|  |       this.prevCredToEdit?.id as string, | ||||
|  |     ); | ||||
|  |     const claimStr = JSON.stringify(offerClaim); | ||||
|  |     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> | ||||
| @ -0,0 +1,63 @@ | |||||
|  | import { test, expect } from '@playwright/test'; | ||||
|  | import { importUser } from './testUtils'; | ||||
|  | 
 | ||||
|  | test('Record an offer', async ({ page }) => { | ||||
|  |   // Generate a random string of 3 characters, skipping the "0." at the beginning
 | ||||
|  |   const randomString = Math.random().toString(36).substring(2, 5); | ||||
|  |   // Standard title prefix
 | ||||
|  |   const description = `Offering of ${randomString}`; | ||||
|  |   const updatedDescription = `Updated ${description}`; | ||||
|  |   const randomNonZeroNumber = Math.floor(Math.random() * 998) + 1; | ||||
|  | 
 | ||||
|  |   // Create new ID for default user
 | ||||
|  |   await importUser(page); | ||||
|  | 
 | ||||
|  |   // Select a project
 | ||||
|  |   await page.goto('./discover'); | ||||
|  |   await page.locator('ul#listDiscoverResults li:nth-child(1)').click(); | ||||
|  | 
 | ||||
|  |   // Record an offer
 | ||||
|  |   await page.getByTestId('offerButton').click(); | ||||
|  |   await page.getByTestId('inputDescription').fill(description); | ||||
|  |   await page.getByTestId('inputOfferAmount').fill(randomNonZeroNumber.toString()); | ||||
|  |   await page.getByRole('button', { name: 'Sign & Send' }).click(); | ||||
|  |   await expect(page.getByText('That offer was recorded.')).toBeVisible(); | ||||
|  | 
 | ||||
|  |   // go to the offer and check the values
 | ||||
|  |   await page.goto('./projects'); | ||||
|  |   await page.locator('li').filter({ hasText: description }).locator('a').first().click(); | ||||
|  |   await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible(); | ||||
|  |   await expect(page.getByText(description, { exact: true })).toBeVisible(); | ||||
|  |   const serverPagePromise = page.waitForEvent('popup'); | ||||
|  |   await page.getByRole('link', { name: 'View on the Public Server' }).click(); | ||||
|  |   const serverPage = await serverPagePromise; | ||||
|  |   await serverPage.getByText(description); | ||||
|  |   await serverPage.getByText('did:none:HIDDEN'); | ||||
|  | 
 | ||||
|  |   // Now update that offer
 | ||||
|  | 
 | ||||
|  |   // find the edit page and check the old values again
 | ||||
|  |   await page.goto('./projects'); | ||||
|  |   await page.locator('li').filter({ hasText: description }).locator('a').first().click(); | ||||
|  |   await page.getByTestId('editClaimButton').click(); | ||||
|  |   await page.locator('heading', { hasText: 'What is offered' }).isVisible(); | ||||
|  |   const itemDesc = await page.getByTestId('itemDescription'); | ||||
|  |   await expect(itemDesc).toHaveValue(description); | ||||
|  |   const amount = await page.getByTestId('inputOfferAmount'); | ||||
|  |   await expect(amount).toHaveValue(randomNonZeroNumber.toString()); | ||||
|  |   // update the values
 | ||||
|  |   await itemDesc.fill(updatedDescription); | ||||
|  |   await amount.fill(String(randomNonZeroNumber + 1)); | ||||
|  |   await page.getByRole('button', { name: 'Sign & Send' }).click(); | ||||
|  | 
 | ||||
|  |   // go to the offer claim again and check the updated values
 | ||||
|  |   await page.goto('./projects'); | ||||
|  |   await page.locator('li').filter({ hasText: description }).locator('a').first().click(); | ||||
|  |   const newItemDesc = await page.getByTestId('description'); | ||||
|  |   await expect(newItemDesc).toHaveText(updatedDescription); | ||||
|  | 
 | ||||
|  |   // go to edit page
 | ||||
|  |   await page.getByTestId('editClaimButton').click(); | ||||
|  |   const newAmount = await page.getByTestId('inputOfferAmount'); | ||||
|  |   await expect(newAmount).toHaveValue((randomNonZeroNumber + 1).toString()); | ||||
|  | }); | ||||
					Loading…
					
					
				
		Reference in new issue