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