<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> <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 = ""; 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.log("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.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>