You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							412 lines
						
					
					
						
							11 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							412 lines
						
					
					
						
							11 KiB
						
					
					
				
								<template>
							 | 
						|
								  <div v-if="visible" class="dialog-overlay">
							 | 
						|
								    <div class="dialog">
							 | 
						|
								      <h1 class="text-xl font-bold text-center mb-4">
							 | 
						|
								        {{ customTitle }}
							 | 
						|
								      </h1>
							 | 
						|
								      <input
							 | 
						|
								        type="text"
							 | 
						|
								        class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
							 | 
						|
								        placeholder="What was given"
							 | 
						|
								        v-model="description"
							 | 
						|
								      />
							 | 
						|
								      <div class="flex flex-row justify-center">
							 | 
						|
								        <span
							 | 
						|
								          class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
							 | 
						|
								          @click="changeUnitCode()"
							 | 
						|
								        >
							 | 
						|
								          {{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
							 | 
						|
								        </span>
							 | 
						|
								        <div
							 | 
						|
								          class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
							 | 
						|
								          @click="amountInput === '0' ? null : decrement()"
							 | 
						|
								        >
							 | 
						|
								          <fa icon="chevron-left" />
							 | 
						|
								        </div>
							 | 
						|
								        <input
							 | 
						|
								          type="number"
							 | 
						|
								          class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
							 | 
						|
								          v-model="amountInput"
							 | 
						|
								        />
							 | 
						|
								        <div
							 | 
						|
								          class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
							 | 
						|
								          @click="increment()"
							 | 
						|
								        >
							 | 
						|
								          <fa icon="chevron-right" />
							 | 
						|
								        </div>
							 | 
						|
								      </div>
							 | 
						|
								      <div class="mt-4 flex justify-center">
							 | 
						|
								        <span>
							 | 
						|
								          <router-link
							 | 
						|
								            :to="{
							 | 
						|
								              name: 'gifted-details',
							 | 
						|
								              query: {
							 | 
						|
								                amountInput,
							 | 
						|
								                description,
							 | 
						|
								                giverDid: giver?.did,
							 | 
						|
								                giverName: giver?.name,
							 | 
						|
								                offerId,
							 | 
						|
								                projectId,
							 | 
						|
								                recipientDid: receiver?.did,
							 | 
						|
								                recipientName: receiver?.name,
							 | 
						|
								                unitCode,
							 | 
						|
								              },
							 | 
						|
								            }"
							 | 
						|
								            class="text-blue-500"
							 | 
						|
								          >
							 | 
						|
								            Photo & Details ...
							 | 
						|
								          </router-link>
							 | 
						|
								        </span>
							 | 
						|
								      </div>
							 | 
						|
								      <p class="text-center mb-2 mt-6 italic">
							 | 
						|
								        Sign & Send to publish to the world
							 | 
						|
								        <fa
							 | 
						|
								          icon="circle-info"
							 | 
						|
								          class="pl-2 text-blue-500 cursor-pointer"
							 | 
						|
								          @click="explainData()"
							 | 
						|
								        />
							 | 
						|
								      </p>
							 | 
						|
								      <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
							 | 
						|
								        <button
							 | 
						|
								          class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
							 | 
						|
								          @click="confirm"
							 | 
						|
								        >
							 | 
						|
								          Sign & Send
							 | 
						|
								        </button>
							 | 
						|
								        <button
							 | 
						|
								          class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
							 | 
						|
								          @click="cancel"
							 | 
						|
								        >
							 | 
						|
								          Cancel
							 | 
						|
								        </button>
							 | 
						|
								      </div>
							 | 
						|
								    </div>
							 | 
						|
								  </div>
							 | 
						|
								</template>
							 | 
						|
								
							 | 
						|
								<script lang="ts">
							 | 
						|
								import { Vue, Component, Prop } from "vue-facing-decorator";
							 | 
						|
								
							 | 
						|
								import { NotificationIface } from "@/constants/app";
							 | 
						|
								import {
							 | 
						|
								  createAndSubmitGive,
							 | 
						|
								  didInfo,
							 | 
						|
								  GiverReceiverInputInfo,
							 | 
						|
								} from "@/libs/endorserServer";
							 | 
						|
								import * as libsUtil from "@/libs/util";
							 | 
						|
								import { accountsDB, db } from "@/db/index";
							 | 
						|
								import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
							 | 
						|
								import { Contact } from "@/db/tables/contacts";
							 | 
						|
								
							 | 
						|
								@Component
							 | 
						|
								export default class GiftedDialog extends Vue {
							 | 
						|
								  $notify!: (notification: NotificationIface, timeout?: number) => void;
							 | 
						|
								
							 | 
						|
								  @Prop projectId = "";
							 | 
						|
								
							 | 
						|
								  activeDid = "";
							 | 
						|
								  allContacts: Array<Contact> = [];
							 | 
						|
								  allMyDids: Array<string> = [];
							 | 
						|
								  apiServer = "";
							 | 
						|
								
							 | 
						|
								  amountInput = "0";
							 | 
						|
								  callbackOnSuccess?: (amount: number) => void = () => {};
							 | 
						|
								  customTitle?: string;
							 | 
						|
								  description = "";
							 | 
						|
								  giver?: GiverReceiverInputInfo; // undefined means no identified giver agent
							 | 
						|
								  isTrade = false;
							 | 
						|
								  offerId = "";
							 | 
						|
								  receiver?: GiverReceiverInputInfo;
							 | 
						|
								  unitCode = "HUR";
							 | 
						|
								  visible = false;
							 | 
						|
								
							 | 
						|
								  libsUtil = libsUtil;
							 | 
						|
								
							 | 
						|
								  async open(
							 | 
						|
								    giver?: GiverReceiverInputInfo,
							 | 
						|
								    receiver?: GiverReceiverInputInfo,
							 | 
						|
								    offerId?: string,
							 | 
						|
								    customTitle?: string,
							 | 
						|
								    callbackOnSuccess?: (amount: number) => void,
							 | 
						|
								  ) {
							 | 
						|
								    this.customTitle = customTitle;
							 | 
						|
								    this.description = "";
							 | 
						|
								    this.giver = giver;
							 | 
						|
								    this.receiver = receiver;
							 | 
						|
								    // if we show "given to user" selection, default checkbox to true
							 | 
						|
								    this.amountInput = "0";
							 | 
						|
								    this.callbackOnSuccess = callbackOnSuccess;
							 | 
						|
								    this.offerId = offerId || "";
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								      await db.open();
							 | 
						|
								      const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
							 | 
						|
								      this.apiServer = settings?.apiServer || "";
							 | 
						|
								      this.activeDid = settings?.activeDid || "";
							 | 
						|
								
							 | 
						|
								      this.allContacts = await db.contacts.toArray();
							 | 
						|
								
							 | 
						|
								      await accountsDB.open();
							 | 
						|
								      const allAccounts = await accountsDB.accounts.toArray();
							 | 
						|
								      this.allMyDids = allAccounts.map((acc) => acc.did);
							 | 
						|
								
							 | 
						|
								      if (this.giver && !this.giver.name) {
							 | 
						|
								        this.giver.name = didInfo(
							 | 
						|
								          this.giver.did,
							 | 
						|
								          this.activeDid,
							 | 
						|
								          this.allMyDids,
							 | 
						|
								          this.allContacts,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								      // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								    } catch (err: any) {
							 | 
						|
								      console.error("Error retrieving settings from database:", err);
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: err.message || "There was an error retrieving your settings.",
							 | 
						|
								        },
							 | 
						|
								        -1,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    this.visible = true;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  close() {
							 | 
						|
								    // close the dialog but don't change values (since it might be submitting info)
							 | 
						|
								    this.visible = false;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  changeUnitCode() {
							 | 
						|
								    const units = Object.keys(this.libsUtil.UNIT_SHORT);
							 | 
						|
								    const index = units.indexOf(this.unitCode);
							 | 
						|
								    this.unitCode = units[(index + 1) % units.length];
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  increment() {
							 | 
						|
								    this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  decrement() {
							 | 
						|
								    this.amountInput = `${Math.max(
							 | 
						|
								      0,
							 | 
						|
								      (parseFloat(this.amountInput) || 1) - 1,
							 | 
						|
								    )}`;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  cancel() {
							 | 
						|
								    this.close();
							 | 
						|
								    this.eraseValues();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  eraseValues() {
							 | 
						|
								    this.description = "";
							 | 
						|
								    this.giver = undefined;
							 | 
						|
								    this.amountInput = "0";
							 | 
						|
								    this.unitCode = "HUR";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async confirm() {
							 | 
						|
								    if (!this.activeDid) {
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: "You must select an identifier before you can record a give.",
							 | 
						|
								        },
							 | 
						|
								        3000,
							 | 
						|
								      );
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								    if (parseFloat(this.amountInput) < 0) {
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          text: "You may not send a negative number.",
							 | 
						|
								          title: "",
							 | 
						|
								        },
							 | 
						|
								        2000,
							 | 
						|
								      );
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								    if (!this.description && !parseFloat(this.amountInput)) {
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: `You must enter a description or some number of ${
							 | 
						|
								            this.libsUtil.UNIT_LONG[this.unitCode]
							 | 
						|
								          }.`,
							 | 
						|
								        },
							 | 
						|
								        2000,
							 | 
						|
								      );
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    this.close();
							 | 
						|
								    this.$notify(
							 | 
						|
								      {
							 | 
						|
								        group: "alert",
							 | 
						|
								        type: "toast",
							 | 
						|
								        text: "Recording the give...",
							 | 
						|
								        title: "",
							 | 
						|
								      },
							 | 
						|
								      1000,
							 | 
						|
								    );
							 | 
						|
								    // this is asynchronous, but we don't need to wait for it to complete
							 | 
						|
								    await this.recordGive(
							 | 
						|
								      (this.giver?.did as string) || null,
							 | 
						|
								      (this.receiver?.did as string) || null,
							 | 
						|
								      this.description,
							 | 
						|
								      parseFloat(this.amountInput),
							 | 
						|
								      this.unitCode,
							 | 
						|
								    ).then(() => {
							 | 
						|
								      this.eraseValues();
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   *
							 | 
						|
								   * @param giverDid may be null
							 | 
						|
								   * @param recipientDid may be null
							 | 
						|
								   * @param description may be an empty string
							 | 
						|
								   * @param amount may be 0
							 | 
						|
								   * @param unitCode may be omitted, defaults to "HUR"
							 | 
						|
								   */
							 | 
						|
								  async recordGive(
							 | 
						|
								    giverDid: string | null,
							 | 
						|
								    recipientDid: string | null,
							 | 
						|
								    description: string,
							 | 
						|
								    amount: number,
							 | 
						|
								    unitCode: string = "HUR",
							 | 
						|
								  ) {
							 | 
						|
								    try {
							 | 
						|
								      const identity = await libsUtil.getIdentity(this.activeDid);
							 | 
						|
								      const result = await createAndSubmitGive(
							 | 
						|
								        this.axios,
							 | 
						|
								        this.apiServer,
							 | 
						|
								        identity,
							 | 
						|
								        giverDid,
							 | 
						|
								        this.receiver?.did as string,
							 | 
						|
								        description,
							 | 
						|
								        amount,
							 | 
						|
								        unitCode,
							 | 
						|
								        this.projectId,
							 | 
						|
								        this.offerId,
							 | 
						|
								        this.isTrade,
							 | 
						|
								      );
							 | 
						|
								
							 | 
						|
								      if (
							 | 
						|
								        result.type === "error" ||
							 | 
						|
								        this.isGiveCreationError(result.response)
							 | 
						|
								      ) {
							 | 
						|
								        const errorMessage = this.getGiveCreationErrorMessage(result);
							 | 
						|
								        console.error("Error with give creation result:", result);
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "danger",
							 | 
						|
								            title: "Error",
							 | 
						|
								            text: errorMessage || "There was an error creating the give.",
							 | 
						|
								          },
							 | 
						|
								          -1,
							 | 
						|
								        );
							 | 
						|
								      } else {
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "success",
							 | 
						|
								            title: "Success",
							 | 
						|
								            text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
							 | 
						|
								          },
							 | 
						|
								          7000,
							 | 
						|
								        );
							 | 
						|
								        if (this.callbackOnSuccess) {
							 | 
						|
								          this.callbackOnSuccess(amount);
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								      // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								    } catch (error: any) {
							 | 
						|
								      console.error("Error with give recordation caught:", error);
							 | 
						|
								      const errorMessage =
							 | 
						|
								        error.userMessage ||
							 | 
						|
								        error.response?.data?.error?.message ||
							 | 
						|
								        "There was an error recording the give.";
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: errorMessage,
							 | 
						|
								        },
							 | 
						|
								        -1,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Helper functions for readability
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * @param result response "data" from the server
							 | 
						|
								   * @returns true if the result indicates an error
							 | 
						|
								   */
							 | 
						|
								  // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								  isGiveCreationError(result: any) {
							 | 
						|
								    return result.status !== 201 || result.data?.error;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
							 | 
						|
								   * @returns best guess at an error message
							 | 
						|
								   */
							 | 
						|
								  // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								  getGiveCreationErrorMessage(result: any) {
							 | 
						|
								    return (
							 | 
						|
								      result.error?.userMessage ||
							 | 
						|
								      result.error?.error ||
							 | 
						|
								      result.response?.data?.error?.message
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  explainData() {
							 | 
						|
								    this.$notify(
							 | 
						|
								      {
							 | 
						|
								        group: "alert",
							 | 
						|
								        type: "success",
							 | 
						|
								        title: "Data Sharing",
							 | 
						|
								        text: libsUtil.PRIVACY_MESSAGE,
							 | 
						|
								      },
							 | 
						|
								      -1,
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</script>
							 | 
						|
								
							 | 
						|
								<style>
							 | 
						|
								.dialog-overlay {
							 | 
						|
								  position: fixed;
							 | 
						|
								  top: 0;
							 | 
						|
								  left: 0;
							 | 
						|
								  right: 0;
							 | 
						|
								  bottom: 0;
							 | 
						|
								  background-color: rgba(0, 0, 0, 0.5);
							 | 
						|
								  display: flex;
							 | 
						|
								  justify-content: center;
							 | 
						|
								  align-items: center;
							 | 
						|
								  padding: 1.5rem;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								.dialog {
							 | 
						|
								  background-color: white;
							 | 
						|
								  padding: 1rem;
							 | 
						|
								  border-radius: 0.5rem;
							 | 
						|
								  width: 100%;
							 | 
						|
								  max-width: 500px;
							 | 
						|
								}
							 | 
						|
								</style>
							 | 
						|
								
							 |