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