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.
		
		
		
		
		
			
		
			
				
					
					
						
							389 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							389 lines
						
					
					
						
							12 KiB
						
					
					
				
								<template>
							 | 
						|
								  <QuickNav />
							 | 
						|
								  <TopMessage />
							 | 
						|
								
							 | 
						|
								  <!-- CONTENT -->
							 | 
						|
								  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
							 | 
						|
								    <!-- Back -->
							 | 
						|
								    <div 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="$router.back()"
							 | 
						|
								      >
							 | 
						|
								        <fa icon="chevron-left" class="fa-fw"></fa>
							 | 
						|
								      </h1>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <!-- Heading -->
							 | 
						|
								    <h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
							 | 
						|
								      End of BVC Saturday Meeting
							 | 
						|
								    </h1>
							 | 
						|
								
							 | 
						|
								    <div>
							 | 
						|
								      <h2 class="text-2xl m-2">Confirm</h2>
							 | 
						|
								      <div v-if="loadingConfirms" class="flex justify-center">
							 | 
						|
								        <fa icon="spinner" class="animate-spin" />
							 | 
						|
								      </div>
							 | 
						|
								      <div v-else-if="claimsToConfirm.length === 0">
							 | 
						|
								        There are no claims yet today for you to confirm.
							 | 
						|
								      </div>
							 | 
						|
								      <ul class="border-t border-slate-300 m-2">
							 | 
						|
								        <li
							 | 
						|
								          class="border-b border-slate-300 py-2"
							 | 
						|
								          v-for="record in claimsToConfirm"
							 | 
						|
								          :key="record.id"
							 | 
						|
								        >
							 | 
						|
								          <div class="grid grid-cols-12">
							 | 
						|
								            <span class="col-span-11 justify-self-start">
							 | 
						|
								              <span>
							 | 
						|
								                <input
							 | 
						|
								                  type="checkbox"
							 | 
						|
								                  :checked="claimsToConfirmSelected.includes(record.id)"
							 | 
						|
								                  @click="
							 | 
						|
								                    claimsToConfirmSelected.includes(record.id)
							 | 
						|
								                      ? claimsToConfirmSelected.splice(
							 | 
						|
								                          claimsToConfirmSelected.indexOf(record.id),
							 | 
						|
								                          1,
							 | 
						|
								                        )
							 | 
						|
								                      : claimsToConfirmSelected.push(record.id)
							 | 
						|
								                  "
							 | 
						|
								                  class="mr-2 h-6 w-6"
							 | 
						|
								                />
							 | 
						|
								              </span>
							 | 
						|
								              {{
							 | 
						|
								                claimSpecialDescription(
							 | 
						|
								                  record,
							 | 
						|
								                  activeDid,
							 | 
						|
								                  allMyDids,
							 | 
						|
								                  allContacts,
							 | 
						|
								                )
							 | 
						|
								              }}
							 | 
						|
								              <a @click="onClickLoadClaim(record.id)">
							 | 
						|
								                <fa
							 | 
						|
								                  icon="file-lines"
							 | 
						|
								                  class="pl-2 text-blue-500 cursor-pointer"
							 | 
						|
								                />
							 | 
						|
								              </a>
							 | 
						|
								            </span>
							 | 
						|
								          </div>
							 | 
						|
								        </li>
							 | 
						|
								      </ul>
							 | 
						|
								    </div>
							 | 
						|
								    <div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
							 | 
						|
								      <span>
							 | 
						|
								        {{
							 | 
						|
								          claimCountWithHidden === 1
							 | 
						|
								            ? "There is 1 other claim with hidden details,"
							 | 
						|
								            : `There are ${claimCountWithHidden} other claims with hidden details,`
							 | 
						|
								        }}
							 | 
						|
								        so if you expected but do not see details from someone then ask them to
							 | 
						|
								        check that their activity is visible to you on their Contacts
							 | 
						|
								        <fa icon="users" class="text-slate-500" />
							 | 
						|
								        page.
							 | 
						|
								      </span>
							 | 
						|
								    </div>
							 | 
						|
								    <div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
							 | 
						|
								      <span>
							 | 
						|
								        {{
							 | 
						|
								          claimCountByUser === 1
							 | 
						|
								            ? "There is 1 other claim by you"
							 | 
						|
								            : `There are ${claimCountByUser} other claims by you`
							 | 
						|
								        }}
							 | 
						|
								        which you don't need to confirm.
							 | 
						|
								      </span>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <div>
							 | 
						|
								      <h2 class="text-2xl m-2">Anything else?</h2>
							 | 
						|
								      <div class="m-2 flex">
							 | 
						|
								        <input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
							 | 
						|
								        <span class="pb-2 pl-2 pr-2">Someone else gave</span>
							 | 
						|
								        <span v-if="someoneGave">
							 | 
						|
								          <input
							 | 
						|
								            type="text"
							 | 
						|
								            v-model="description"
							 | 
						|
								            size="20"
							 | 
						|
								            class="border border-slate-400 h-6 px-2"
							 | 
						|
								          />
							 | 
						|
								          <br />
							 | 
						|
								          (Everyone likes personalized messages! 😁)
							 | 
						|
								        </span>
							 | 
						|
								        <!-- This is to match input height to avoid shifting when hiding & showing. -->
							 | 
						|
								        <span v-else class="h-6">...</span>
							 | 
						|
								      </div>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <div
							 | 
						|
								      v-if="claimsToConfirmSelected.length || (someoneGave && description)"
							 | 
						|
								      class="flex justify-center mt-4"
							 | 
						|
								    >
							 | 
						|
								      <button
							 | 
						|
								        @click="record()"
							 | 
						|
								        class="block text-center text-md font-bold 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 w-56"
							 | 
						|
								      >
							 | 
						|
								        Sign & Send
							 | 
						|
								      </button>
							 | 
						|
								    </div>
							 | 
						|
								    <div v-else class="flex justify-center mt-4">
							 | 
						|
								      <button
							 | 
						|
								        class="block text-center text-md font-bold 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-2 py-3 rounded-md w-56"
							 | 
						|
								      >
							 | 
						|
								        Choose What To Confirm
							 | 
						|
								      </button>
							 | 
						|
								    </div>
							 | 
						|
								  </section>
							 | 
						|
								</template>
							 | 
						|
								
							 | 
						|
								<script lang="ts">
							 | 
						|
								import axios from "axios";
							 | 
						|
								import { DateTime } from "luxon";
							 | 
						|
								import * as R from "ramda";
							 | 
						|
								import { IIdentifier } from "@veramo/core";
							 | 
						|
								import { Component, Vue } from "vue-facing-decorator";
							 | 
						|
								
							 | 
						|
								import QuickNav from "@/components/QuickNav.vue";
							 | 
						|
								import TopMessage from "@/components/TopMessage.vue";
							 | 
						|
								import { NotificationIface } from "@/constants/app";
							 | 
						|
								import { accountsDB, db } from "@/db/index";
							 | 
						|
								import { Account } from "@/db/tables/accounts";
							 | 
						|
								import { Contact } from "@/db/tables/contacts";
							 | 
						|
								import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
							 | 
						|
								import { accessToken } from "@/libs/crypto";
							 | 
						|
								import {
							 | 
						|
								  BVC_MEETUPS_PROJECT_CLAIM_ID,
							 | 
						|
								  claimSpecialDescription,
							 | 
						|
								  containsHiddenDid,
							 | 
						|
								  createAndSubmitConfirmation,
							 | 
						|
								  createAndSubmitGive,
							 | 
						|
								  ErrorResult,
							 | 
						|
								  GenericCredWrapper,
							 | 
						|
								  GenericVerifiableCredential,
							 | 
						|
								} from "@/libs/endorserServer";
							 | 
						|
								import * as libsUtil from "@/libs/util";
							 | 
						|
								
							 | 
						|
								@Component({
							 | 
						|
								  methods: { claimSpecialDescription },
							 | 
						|
								  components: {
							 | 
						|
								    QuickNav,
							 | 
						|
								    TopMessage,
							 | 
						|
								  },
							 | 
						|
								})
							 | 
						|
								export default class QuickActionBvcBeginView extends Vue {
							 | 
						|
								  $notify!: (notification: NotificationIface, timeout?: number) => void;
							 | 
						|
								
							 | 
						|
								  activeDid = "";
							 | 
						|
								  allContacts: Array<Contact> = [];
							 | 
						|
								  allMyDids: Array<string> = [];
							 | 
						|
								  apiServer = "";
							 | 
						|
								  claimCountByUser = 0;
							 | 
						|
								  claimCountWithHidden = 0;
							 | 
						|
								  claimsToConfirm: GenericCredWrapper[] = [];
							 | 
						|
								  claimsToConfirmSelected: string[] = [];
							 | 
						|
								  description = "breakfast";
							 | 
						|
								  loadingConfirms = true;
							 | 
						|
								  someoneGave = false;
							 | 
						|
								
							 | 
						|
								  async created() {
							 | 
						|
								    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();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async mounted() {
							 | 
						|
								    this.loadingConfirms = true;
							 | 
						|
								    let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
							 | 
						|
								    if (currentOrPreviousSat.weekday < 6) {
							 | 
						|
								      // it's not Saturday or Sunday,
							 | 
						|
								      // so move back one week before setting to the Saturday
							 | 
						|
								      currentOrPreviousSat = currentOrPreviousSat.minus({ week: 1 });
							 | 
						|
								    }
							 | 
						|
								    const eventStartDateObj = currentOrPreviousSat
							 | 
						|
								      .set({ weekday: 6 })
							 | 
						|
								      .set({ hour: 9 })
							 | 
						|
								      .startOf("hour");
							 | 
						|
								
							 | 
						|
								    // Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
							 | 
						|
								    const todayOrPreviousStartDate =
							 | 
						|
								      eventStartDateObj.toISO({
							 | 
						|
								        suppressMilliseconds: true,
							 | 
						|
								      }) || "";
							 | 
						|
								
							 | 
						|
								    await accountsDB.open();
							 | 
						|
								    const allAccounts = await accountsDB.accounts.toArray();
							 | 
						|
								    this.allMyDids = allAccounts.map((acc) => acc.did);
							 | 
						|
								    const account: Account | undefined = await accountsDB.accounts
							 | 
						|
								      .where("did")
							 | 
						|
								      .equals(this.activeDid)
							 | 
						|
								      .first();
							 | 
						|
								    const identity: IIdentifier = JSON.parse(
							 | 
						|
								      (account?.identity as string) || "null",
							 | 
						|
								    );
							 | 
						|
								    const headers = {
							 | 
						|
								      Authorization: "Bearer " + (await accessToken(identity)),
							 | 
						|
								    };
							 | 
						|
								    try {
							 | 
						|
								      const response = await fetch(
							 | 
						|
								        this.apiServer +
							 | 
						|
								          "/api/claim/?" +
							 | 
						|
								          "issuedAt_greaterThanOrEqualTo=" +
							 | 
						|
								          encodeURIComponent(todayOrPreviousStartDate) +
							 | 
						|
								          "&excludeConfirmations=true",
							 | 
						|
								        { headers },
							 | 
						|
								      );
							 | 
						|
								
							 | 
						|
								      if (!response.ok) {
							 | 
						|
								        console.error("Bad response", response);
							 | 
						|
								        throw new Error("Bad response when retrieving claims.");
							 | 
						|
								      }
							 | 
						|
								      await response.json().then((data) => {
							 | 
						|
								        const dataByOthers = R.reject(
							 | 
						|
								          (claim: GenericCredWrapper) => claim.issuer === this.activeDid,
							 | 
						|
								          data,
							 | 
						|
								        );
							 | 
						|
								        const dataByOthersWithoutHidden = R.reject(
							 | 
						|
								          containsHiddenDid,
							 | 
						|
								          dataByOthers,
							 | 
						|
								        );
							 | 
						|
								        this.claimsToConfirm = dataByOthersWithoutHidden;
							 | 
						|
								        this.claimCountByUser = data.length - dataByOthers.length;
							 | 
						|
								        this.claimCountWithHidden =
							 | 
						|
								          dataByOthers.length - dataByOthersWithoutHidden.length;
							 | 
						|
								      });
							 | 
						|
								    } catch (error) {
							 | 
						|
								      console.error("Error:", error);
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: "There was an error retrieving today's claims to confirm.",
							 | 
						|
								        },
							 | 
						|
								        -1,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								    this.loadingConfirms = false;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  onClickLoadClaim(jwtId: string) {
							 | 
						|
								    const route = {
							 | 
						|
								      path: "/claim/" + encodeURIComponent(jwtId),
							 | 
						|
								    };
							 | 
						|
								    this.$router.push(route);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async record() {
							 | 
						|
								    try {
							 | 
						|
								      const identity = await libsUtil.getIdentity(this.activeDid);
							 | 
						|
								
							 | 
						|
								      // in parallel, make a confirmation for each selected claim and send them all to the server
							 | 
						|
								      const confirmResults = await Promise.allSettled(
							 | 
						|
								        this.claimsToConfirmSelected.map(async (jwtId) => {
							 | 
						|
								          const record = this.claimsToConfirm.find(
							 | 
						|
								            (claim) => claim.id === jwtId,
							 | 
						|
								          );
							 | 
						|
								          if (!record) {
							 | 
						|
								            return { type: "error", error: "Record not found." };
							 | 
						|
								          }
							 | 
						|
								          const identity = await libsUtil.getIdentity(this.activeDid);
							 | 
						|
								          return createAndSubmitConfirmation(
							 | 
						|
								            identity,
							 | 
						|
								            record.claim as GenericVerifiableCredential,
							 | 
						|
								            record.id,
							 | 
						|
								            record.handleId,
							 | 
						|
								            this.apiServer,
							 | 
						|
								            axios,
							 | 
						|
								          );
							 | 
						|
								        }),
							 | 
						|
								      );
							 | 
						|
								      // check for any rejected confirmations
							 | 
						|
								      const confirmsSucceeded = confirmResults.filter(
							 | 
						|
								        (result) =>
							 | 
						|
								          result.status === "fulfilled" && result.value.type === "success",
							 | 
						|
								      );
							 | 
						|
								      if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
							 | 
						|
								        console.error("Error sending confirmations:", confirmResults);
							 | 
						|
								        const howMany = confirmsSucceeded.length === 0 ? "all" : "some";
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "danger",
							 | 
						|
								            title: "Error",
							 | 
						|
								            text: `There was an error sending ${howMany} of the confirmations.`,
							 | 
						|
								          },
							 | 
						|
								          -1,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      // now send the give for the description
							 | 
						|
								      let giveSucceeded = false;
							 | 
						|
								      if (this.someoneGave) {
							 | 
						|
								        const giveResult = await createAndSubmitGive(
							 | 
						|
								          axios,
							 | 
						|
								          this.apiServer,
							 | 
						|
								          identity,
							 | 
						|
								          undefined,
							 | 
						|
								          this.activeDid,
							 | 
						|
								          this.description,
							 | 
						|
								          undefined,
							 | 
						|
								          undefined,
							 | 
						|
								          BVC_MEETUPS_PROJECT_CLAIM_ID,
							 | 
						|
								        );
							 | 
						|
								        giveSucceeded = giveResult.type === "success";
							 | 
						|
								        if (!giveSucceeded) {
							 | 
						|
								          console.error("Error sending give:", giveResult);
							 | 
						|
								          this.$notify(
							 | 
						|
								            {
							 | 
						|
								              group: "alert",
							 | 
						|
								              type: "danger",
							 | 
						|
								              title: "Error",
							 | 
						|
								              text:
							 | 
						|
								                (giveResult as ErrorResult)?.error?.userMessage ||
							 | 
						|
								                "There was an error sending that give.",
							 | 
						|
								            },
							 | 
						|
								            -1,
							 | 
						|
								          );
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      if (confirmsSucceeded.length > 0 || giveSucceeded) {
							 | 
						|
								        const confirms =
							 | 
						|
								          confirmsSucceeded.length === 1 ? "confirmation" : "confirmations";
							 | 
						|
								        const actions =
							 | 
						|
								          confirmsSucceeded.length > 0 && giveSucceeded
							 | 
						|
								            ? `Your ${confirms} and that give have been recorded.`
							 | 
						|
								            : giveSucceeded
							 | 
						|
								              ? "That give has been recorded."
							 | 
						|
								              : "Your " +
							 | 
						|
								                confirms +
							 | 
						|
								                " " +
							 | 
						|
								                (confirmsSucceeded.length === 1 ? "has" : "have") +
							 | 
						|
								                " been recorded.";
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "success",
							 | 
						|
								            title: "Success",
							 | 
						|
								            text: actions,
							 | 
						|
								          },
							 | 
						|
								          -1,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								    } catch (error: any) {
							 | 
						|
								      console.error("Error sending claims.", error);
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: error.userMessage || "There was an error sending claims.",
							 | 
						|
								        },
							 | 
						|
								        -1,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</script>
							 | 
						|
								
							 |