<template> <QuickNav /> <TopMessage /> <!-- CONTENT --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <!-- Breadcrumb --> <div id="ViewBreadcrumb"> <h1 class="text-lg text-center font-light relative px-7"> <!-- Back --> <button @click="$router.go(-1)" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" > <fa icon="chevron-left" class="fa-fw"></fa> </button> Project Idea </h1> <h2 class="text-xl font-semibold"> {{ name }} <button v-if="activeDid === issuer || activeDid === agentDid" @click="onEditClick()" title="Edit" data-testId="editClaimButton" > <fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" /> </button> </h2> </div> <!-- Project Details --> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4"> <div> <div class="pb-4 flex gap-4"> <div class="pt-1"> <ProjectIcon :entityId="projectId" :iconSize="64" :imageUrl="imageUrl" :linkToFull="true" class="block border border-slate-300 rounded-md max-h-16 max-w-16" /> </div> <div class="overflow-hidden"> <div class="text-sm mb-3"> <div class="truncate"> <fa icon="user" class="fa-fw text-slate-400"></fa> {{ serverUtil.didInfo(issuer, activeDid, allMyDids, allContacts) }} <span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)"> <button @click=" libsUtil.doCopyTwoSecRedo( issuer, () => (showDidCopy = !showDidCopy), ) " class="ml-2 mr-2" > <fa icon="copy" class="text-slate-400 fa-fw"></fa> </button> <span v-show="showDidCopy">Copied DID</span> </span> </div> <div v-if="startTime"> <fa icon="calendar" class="fa-fw text-slate-400"></fa> {{ startTime }} </div> <div v-if="latitude || longitude"> <fa icon="location-dot" class="fa-fw text-slate-400"></fa> <a :href="getOpenStreetMapUrl()" target="_blank" class="underline" >Map View <fa icon="arrow-up-right-from-square" class="fa-fw" /> </a> </div> <div v-if="url"> <fa icon="globe" class="fa-fw text-slate-400"></fa> <a :href="addScheme(url)" target="_blank" class="underline"> {{ domainForWebsite(this.url) }} <fa icon="arrow-up-right-from-square" class="fa-fw" /> </a> </div> </div> </div> </div> <div class="text-sm text-slate-500"> <div v-if="!expanded"> {{ truncatedDesc }} <a v-if="description.length >= truncateLength" @click="expandText" class="uppercase text-xs font-semibold text-slate-700" >... Read More</a > </div> <div v-else> {{ description }} <a @click="collapseText" class="uppercase text-xs font-semibold text-slate-700" >- Read Less</a > </div> </div> <a @click="onClickLoadClaim(projectId)" class="cursor-pointer"> <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" /> </a> </div> </div> <div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4 mt-4"> <div> <div v-if="fulfillersToThis.length > 0" class="bg-slate-100 px-4 py-3 rounded-md" > <h3 class="text-sm uppercase font-semibold mt-3"> Projects That Contribute To This </h3> <!-- centering because long, wrapped project names didn't left align with blank or "text-left" --> <div class="text-center"> <div v-for="plan in fulfillersToThis" :key="plan.handleId"> <button @click="onClickLoadProject(plan.handleId)" class="text-blue-500" > {{ plan.name }} </button> </div> <div v-if="fulfillersToHitLimit" class="text-center"> <button @click="loadPlanFulfillersTo()">Load More</button> </div> </div> </div> </div> <div> <div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md"> <h3 class="text-sm uppercase font-semibold mb-3"> Projects Getting Contributions From This </h3> <!-- centering because long, wrapped project names didn't left align with blank or "text-left" --> <div class="text-center"> <button @click="onClickLoadProject(fulfilledByThis.handleId)" class="text-blue-500" > {{ fulfilledByThis.name }} </button> </div> </div> </div> </div> <div v-if="activeDid && isRegistered"> <div class="text-center"> <p class="mt-2 mt-4 text-center">Record a contribution from:</p> </div> <ul class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2" > <li @click="openGiftDialogToProject({ name: 'you', did: activeDid })"> <fa icon="hand" class="fa-fw text-blue-500 text-5xl cursor-pointer" /> <h3 class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer" > You </h3> </li> <li @click="openGiftDialogToProject()"> <img src="../assets/blank-square.svg" class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer" /> <h3 class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer" > Unnamed/Unknown </h3> </li> <li v-for="contact in allContacts.slice(0, 5)" :key="contact.did" @click="openGiftDialogToProject(contact)" > <EntityIcon :contact="contact" :iconSize="64" class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer" /> <h3 class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer" > {{ contact.name || "(no name)" }} </h3> </li> <li> <span v-if="allContacts.length >= 5" @click="onClickAllContactsGifting()" class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer" > ... or someone else... </span> </li> </ul> <GiftedDialog ref="giveDialogToThis" :toProjectId="this.projectId" /> </div> <!-- Offers & Gifts to & from this --> <div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4"> <!-- First, offers on the left--> <div class="bg-slate-100 px-4 py-3 rounded-md"> <div v-if="activeDid && isRegistered"> <div class="text-center"> <button data-testId="offerButton" @click="openOfferDialog()" class="block w-full 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-1 rounded-md" > Offer to this (maybe with conditions)... </button> </div> </div> <OfferDialog ref="customOfferDialog" :projectId="this.projectId" :projectName="this.name" /> <h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3> <div v-if="offersToThis.length === 0"> (None yet. Wanna <span @click="openOfferDialog()" class="cursor-pointer text-blue-500" >offer something... especially if others join you</span >?) </div> <ul v-else class="text-sm border-t border-slate-300"> <li v-for="offer in offersToThis" :key="offer.id" class="py-1.5 border-b border-slate-300" > <div class="flex justify-between gap-4"> <span> <fa icon="user" class="fa-fw text-slate-400"></fa> {{ serverUtil.didInfo( offer.offeredByDid, activeDid, allMyDids, allContacts, ) }} </span> <span v-if="offer.amount" class="whitespace-nowrap"> <fa :icon="libsUtil.iconForUnitCode(offer.unit)" class="fa-fw text-slate-400" />{{ offer.amount }} </span> </div> <div v-if="offer.objectDescription" class="text-slate-500"> <fa icon="comment" class="fa-fw text-slate-400" /> {{ offer.objectDescription }} </div> <div class="flex justify-between"> <a @click="onClickLoadClaim(offer.jwtId as string)" class="cursor-pointer" > <fa icon="file-lines" class="pl-2 pt-1 text-blue-500" /> </a> <a v-if="checkIsFulfillable(offer)" @click="onClickFulfillGiveToOffer(offer)" > <fa icon="hand-holding-heart" class="text-blue-500 cursor-pointer" /> </a> </div> </li> </ul> <div v-if="offersHitLimit" class="text-center text-blue-500"> <button @click="loadOffers()">Load More</button> </div> </div> <!-- Now, gives TO this project in the middle --> <!-- (similar to "FROM" gift display below) --> <div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-to"> <div v-if="activeDid && isRegistered"> <div class="text-center"> <button @click="openGiftDialogToProject()" class="block w-full 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-1rounded-md" > Given To This... </button> </div> </div> <h3 class="text-lg font-bold mb-3 mt-4">Given To This Idea</h3> <div v-if="givesToThis.length === 0"> (None yet. If you've seen something, say something by clicking a contact above.) </div> <ul v-else class="text-sm border-t border-slate-300"> <li v-for="give in givesToThis" :key="give.id" class="py-1.5 border-b border-slate-300" > <div class="flex justify-between gap-4"> <span> <fa icon="user" class="fa-fw text-slate-400" /> {{ serverUtil.didInfo( give.agentDid, activeDid, allMyDids, allContacts, ) }} </span> <span v-if="give.amount" class="whitespace-nowrap"> <fa :icon="libsUtil.iconForUnitCode(give.unit)" class="fa-fw text-slate-400" />{{ give.amount }} </span> </div> <div class="text-slate-500"> <fa icon="calendar" class="fa-fw text-slate-400" /> {{ give.issuedAt?.substring(0, 10) }} </div> <div v-if="give.description" class="text-slate-500"> <fa icon="comment" class="fa-fw text-slate-400" /> {{ give.description }} </div> <div class="flex justify-between"> <a @click="onClickLoadClaim(give.jwtId)"> <fa icon="file-lines" class="text-blue-500 cursor-pointer" /> </a> <a v-if=" checkIsConfirmable(give) && !recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId) " @click="deepCheckConfirmable(give)" > <fa icon="circle-check" class="text-blue-500 cursor-pointer" /> </a> <a v-else-if="checkingConfirmationForJwtId === give.jwtId"> <fa icon="spinner" class="fa-spin-pulse" /> </a> <a v-else @click="shallowNotifyWhyCannotConfirm(give)"> <fa icon="circle-check" class="text-slate-500 cursor-pointer" /> </a> </div> <div v-if="give.fullClaim.image" class="flex justify-center"> <a :href="give.fullClaim.image" target="_blank"> <img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" /> </a> </div> </li> </ul> <div v-if="givesHitLimit" class="text-center text-blue-500"> <button @click="loadGives()">Load More</button> </div> </div> <!-- Finally, gives FROM this project on the right --> <!-- (similar to "TO" gift display above) --> <div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-from"> <div v-if="activeDid && isRegistered"> <div class="text-center"> <button @click="openGiftDialogFromProject()" class="block w-full 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-1 rounded-md" > Given By This... </button> </div> </div> <GiftedDialog ref="giveDialogFromThis" :fromProjectId="this.projectId" /> <h3 class="text-lg font-bold mb-3 mt-4"> Benefitted From This Project </h3> <div v-if="givesProvidedByThis.length === 0">(None yet.)</div> <ul v-else class="text-sm border-t border-slate-300"> <li v-for="give in givesProvidedByThis" :key="give.id" class="py-1.5 border-b border-slate-300" > <div class="flex justify-between gap-4"> <span> {{ serverUtil.didInfo( give.recipientDid, activeDid, allMyDids, allContacts, ) }} </span> <span v-if="give.amount" class="whitespace-nowrap"> <fa :icon="libsUtil.iconForUnitCode(give.unit)" class="fa-fw text-slate-400" />{{ give.amount }} </span> </div> <div class="text-slate-500"> <fa icon="calendar" class="fa-fw text-slate-400" /> {{ give.issuedAt?.substring(0, 10) }} </div> <div v-if="give.description" class="text-slate-500"> <fa icon="comment" class="fa-fw text-slate-400" /> {{ give.description }} </div> <div class="flex justify-between"> <a @click="onClickLoadClaim(give.jwtId)"> <fa icon="file-lines" class="text-blue-500 cursor-pointer" /> </a> <a v-if=" checkIsConfirmable(give) && !recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId) " @click="deepCheckConfirmable(give)" > <fa icon="circle-check" class="text-blue-500 cursor-pointer" /> </a> <a v-else-if="checkingConfirmationForJwtId === give.jwtId"> <fa icon="spinner" class="fa-spin-pulse" /> </a> <a v-else @click="shallowNotifyWhyCannotConfirm(give)"> <fa icon="circle-check" class="text-slate-500 cursor-pointer" /> </a> </div> <div v-if="give.fullClaim.image" class="flex justify-center"> <a :href="give.fullClaim.image" target="_blank"> <img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" /> </a> </div> </li> </ul> <div v-if="givesProvidedByHitLimit" class="text-center"> <button @click="loadGivesProvidedBy()">Load More</button> </div> </div> </div> </section> </template> <script lang="ts"> import { AxiosError } from "axios"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import GiftedDialog from "../components/GiftedDialog.vue"; import OfferDialog from "../components/OfferDialog.vue"; import TopMessage from "../components/TopMessage.vue"; import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.vue"; import ProjectIcon from "../components/ProjectIcon.vue"; import { NotificationIface } from "../constants/app"; import { db, logConsoleAndDb, retrieveSettingsForActiveAccount, } from "../db/index"; import { Contact } from "../db/tables/contacts"; import * as libsUtil from "../libs/util"; import { GenericCredWrapper, GiveSummaryRecord, GiveVerifiableCredential, OfferSummaryRecord, OfferVerifiableCredential, PlanSummaryRecord, } from "../libs/endorserServer"; import * as serverUtil from "../libs/endorserServer"; import { retrieveAccountDids } from "../libs/util"; @Component({ components: { EntityIcon, GiftedDialog, OfferDialog, ProjectIcon, QuickNav, TopMessage, }, }) export default class ProjectViewView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; activeDid = ""; agentDid = ""; allMyDids: Array<string> = []; allContacts: Array<Contact> = []; apiServer = ""; checkingConfirmationForJwtId = ""; description = ""; expanded = false; fulfilledByThis: PlanSummaryRecord | null = null; fulfillersToThis: Array<PlanSummaryRecord> = []; fulfillersToHitLimit = false; givesToThis: Array<GiveSummaryRecord> = []; givesHitLimit = false; givesProvidedByThis: Array<GiveSummaryRecord> = []; givesProvidedByHitLimit = false; imageUrl = ""; isRegistered = false; issuer = ""; latitude = 0; longitude = 0; name = ""; offersToThis: Array<OfferSummaryRecord> = []; offersHitLimit = false; projectId = ""; // handle ID recentlyCheckedAndUnconfirmableJwts: string[] = []; showDidCopy = false; startTime = ""; truncatedDesc = ""; truncateLength = 40; url = ""; libsUtil = libsUtil; serverUtil = serverUtil; async created() { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.apiServer = settings.apiServer || ""; this.allContacts = await db.contacts.toArray(); this.isRegistered = !!settings.isRegistered; try { this.allMyDids = await retrieveAccountDids(); } catch (error) { // continue because we want to see claims, even anonymously logConsoleAndDb( "Error retrieving all account DIDs on home page:" + error, true, ); this.$notify( { group: "alert", type: "danger", title: "Error Loading Profile", text: "See the Help page to fix problems with your personal data.", }, -1, ); } const pathParam = window.location.pathname.substring("/project/".length); if (pathParam) { this.projectId = decodeURIComponent(pathParam); } this.loadProject(this.projectId, this.activeDid); } onEditClick() { const route = { name: "new-edit-project", query: { projectId: this.projectId }, }; (this.$router as Router).push(route); } // Isn't there a better way to make this available to the template? expandText() { this.expanded = true; } collapseText() { this.expanded = false; } async loadProject(projectId: string, userDid: string) { this.projectId = projectId; const url = this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId); const headers = await serverUtil.getHeaders(userDid); try { const resp = await this.axios.get(url, { headers }); if (resp.status === 200) { const startTime = resp.data.claim?.startTime; if (startTime != null) { const startDateTime = new Date(startTime); this.startTime = startDateTime.toLocaleDateString() + " " + startDateTime.toLocaleTimeString(); } this.agentDid = resp.data.claim?.agent?.identifier; this.imageUrl = resp.data.claim?.image; this.issuer = resp.data.issuer; this.name = resp.data.claim?.name || "(no name)"; this.description = resp.data.claim?.description || "(no description)"; this.truncatedDesc = this.description.slice(0, this.truncateLength); this.latitude = resp.data.claim?.location?.geo?.latitude || 0; this.longitude = resp.data.claim?.location?.geo?.longitude || 0; this.url = resp.data.claim?.url || ""; } else { // actually, axios throws an error on 404 so we probably never get here console.error("Error getting project:", resp); this.$notify( { group: "alert", type: "danger", title: "Error", text: "There was a problem getting that project.", }, 5000, ); } } catch (error: unknown) { console.error("Error retrieving project:", error); this.$notify( { group: "alert", type: "danger", title: "Error", text: "Something went wrong retrieving that project.", }, 5000, ); } this.givesToThis = []; this.loadGives(); this.givesProvidedByThis = []; this.loadGivesProvidedBy(); this.offersToThis = []; this.loadOffers(); this.fulfillersToThis = []; this.loadPlanFulfillersTo(); this.fulfilledByThis = null; this.loadPlanFulfilledBy(); } async loadGives() { const givesUrl = this.apiServer + "/api/v2/report/givesToPlans?planIds=" + encodeURIComponent(JSON.stringify([this.projectId])); let postfix = ""; if (this.givesToThis.length > 0) { postfix = "&beforeId=" + this.givesToThis[this.givesToThis.length - 1].jwtId; } const givesInUrl = givesUrl + postfix; const headers = await serverUtil.getHeaders(this.activeDid); try { const resp = await this.axios.get(givesInUrl, { headers }); if (resp.status === 200 && resp.data.data) { this.givesToThis = this.givesToThis.concat(resp.data.data); this.givesHitLimit = resp.data.hitLimit; } else { this.$notify( { group: "alert", type: "danger", title: "Error", text: "Failed to retrieve more gives to this project.", }, 5000, ); } } catch (error: unknown) { const serverError = error as AxiosError; this.$notify( { group: "alert", type: "danger", title: "Error", text: "Something went wrong retrieving more gives to this project.", }, 5000, ); console.error( "Something went wrong retrieving more gives to this project:", serverError.message, ); } } async loadGivesProvidedBy() { const providedByUrl = this.apiServer + "/api/v2/report/givesProvidedBy?providerId=" + encodeURIComponent(this.projectId); let postfix = ""; if (this.givesProvidedByThis.length > 0) { postfix = "&beforeId=" + this.givesProvidedByThis[this.givesProvidedByThis.length - 1].jwtId; } const providedByFullUrl = providedByUrl + postfix; const headers = await serverUtil.getHeaders(this.activeDid); try { const resp = await this.axios.get(providedByFullUrl, { headers }); if (resp.status === 200) { this.givesProvidedByThis = this.givesProvidedByThis.concat( resp.data.data, ); this.givesProvidedByHitLimit = resp.data.hitLimit; } else { this.$notify( { group: "alert", type: "danger", title: "Error", text: "Failed to retrieve gives that were provided by this project.", }, 5000, ); } } catch (error: unknown) { const serverError = error as AxiosError; this.$notify( { group: "alert", type: "danger", title: "Error", text: "Something went wrong retrieving gives that were provided by this project.", }, 5000, ); console.error( "Something went wrong retrieving gives that were provided by this project:", serverError.message, ); } } async loadOffers() { const offersUrl = this.apiServer + "/api/v2/report/offersToPlans?planIds=" + encodeURIComponent(JSON.stringify([this.projectId])); let postfix = ""; if (this.offersToThis.length > 0) { postfix = "&beforeId=" + this.offersToThis[this.offersToThis.length - 1].jwtId; } const offersInUrl = offersUrl + postfix; const headers = await serverUtil.getHeaders(this.activeDid); try { const resp = await this.axios.get(offersInUrl, { headers }); if (resp.status === 200 && resp.data.data) { this.offersToThis = this.offersToThis.concat(resp.data.data); this.offersHitLimit = resp.data.hitLimit; } else { this.$notify( { group: "alert", type: "danger", title: "Error", text: "Failed to retrieve more offers to this project.", }, 5000, ); } } catch (error: unknown) { const serverError = error as AxiosError; this.$notify( { group: "alert", type: "danger", title: "Error", text: "Something went wrong retrieving more offers to this project.", }, 5000, ); console.error( "Something went wrong retrieving more offers to this project:", serverError.message, ); } } async loadPlanFulfillersTo() { const fulfillsUrl = this.apiServer + "/api/v2/report/planFulfillersToPlan?planHandleId=" + encodeURIComponent(this.projectId); let postfix = ""; if (this.fulfillersToThis.length > 0) { postfix = "&beforeId=" + this.fulfillersToThis[this.fulfillersToThis.length - 1].jwtId; } const fulfillsInUrl = fulfillsUrl + postfix; const headers = await serverUtil.getHeaders(this.activeDid); try { const resp = await this.axios.get(fulfillsInUrl, { headers }); if (resp.status === 200) { this.fulfillersToThis = this.fulfillersToThis.concat(resp.data.data); this.fulfillersToHitLimit = resp.data.hitLimit; } else { this.$notify( { group: "alert", type: "danger", title: "Error", text: "Failed to retrieve more plans that fullfill this project.", }, 5000, ); } } catch (error: unknown) { const serverError = error as AxiosError; this.$notify( { group: "alert", type: "danger", title: "Error", text: "Something went wrong retrieving more plans that fulfull this project.", }, 5000, ); console.error( "Something went wrong retrieving more plans that fulfill this project:", serverError.message, ); } } async loadPlanFulfilledBy() { const fulfilledByUrl = this.apiServer + "/api/v2/report/planFulfilledByPlan?planHandleId=" + encodeURIComponent(this.projectId); const headers = await serverUtil.getHeaders(this.activeDid); try { const resp = await this.axios.get(fulfilledByUrl, { headers }); if (resp.status === 200) { this.fulfilledByThis = resp.data.data; } else { this.$notify( { group: "alert", type: "danger", title: "Error", text: "Failed to retrieve plans fulfilled by this project.", }, 5000, ); } } catch (error: unknown) { const serverError = error as AxiosError; this.$notify( { group: "alert", type: "danger", title: "Error", text: "Something went wrong retrieving plans fulfilled by this project.", }, 5000, ); console.error( "Error retrieving plans fulfilled by this project:", serverError.message, ); } } /** * Handle clicking on a project entry found in the list * @param id of the project **/ async onClickLoadProject(projectId: string) { const route = { path: "/project/" + encodeURIComponent(projectId), }; (this.$router as Router).push(route); this.loadProject(projectId, this.activeDid); } getOpenStreetMapUrl() { // Google URL is https://maps.google.com/?q=LAT,LONG return ( "https://www.openstreetmap.org/?mlat=" + this.latitude + "&mlon=" + this.longitude + "#map=15/" + this.latitude + "/" + this.longitude ); } openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) { (this.$refs.giveDialogToThis as GiftedDialog).open( contact, undefined, undefined, (contact?.name || "Someone not named") + ` gave to this project`, ); } openGiftDialogFromProject() { (this.$refs.giveDialogFromThis as GiftedDialog).open( undefined, { did: this.activeDid, name: "You" }, undefined, `This project gave to you`, ); } openOfferDialog() { (this.$refs.customOfferDialog as OfferDialog).open(); } onClickAllContactsGifting() { const route = { name: "contact-gift", query: { projectId: this.projectId, }, }; (this.$router as Router).push(route); } onClickLoadClaim(jwtId: string) { const route = { path: "/claim/" + encodeURIComponent(jwtId), }; (this.$router as Router).push(route); } checkIsFulfillable(offer: OfferSummaryRecord) { const offerRecord: GenericCredWrapper<OfferVerifiableCredential> = { ...serverUtil.BLANK_GENERIC_SERVER_RECORD, claim: offer.fullClaim, claimType: "Offer", issuer: offer.offeredByDid, }; return libsUtil.canFulfillOffer(offerRecord); } onClickFulfillGiveToOffer(offer: OfferSummaryRecord) { const offerRecord: GenericCredWrapper<OfferVerifiableCredential> = { ...serverUtil.BLANK_GENERIC_SERVER_RECORD, claim: offer.fullClaim, issuer: offer.offeredByDid, }; const giver: libsUtil.GiverReceiverInputInfo = { did: libsUtil.offerGiverDid(offerRecord), }; (this.$refs.giveDialogToThis as GiftedDialog).open( giver, undefined, offer.handleId, "Given by " + (giver?.name || "someone not named"), ); } // return an HTTPS URL if it's not a global URL addScheme(url: string) { if (!libsUtil.isGlobalUri(url)) { return "https://" + url; } return url; } // return just the domain for display, if possible domainForWebsite(url: string) { try { const hostname = new URL(url).hostname; if (!hostname) { // happens for non-http URLs return url; } else if (url.endsWith(hostname)) { // it's just the domain return hostname; } else { // there's more, but don't bother displaying the whole thing return hostname + "..."; } } catch (error: unknown) { // must not be a valid URL return url; } } /** * @param confirmerIdList optional list of DIDs who confirmed; if missing, doesn't do a full server check */ checkIsConfirmable(give: GiveSummaryRecord, confirmerIdList?: string[]) { const giveDetails: GenericCredWrapper<GiveVerifiableCredential> = { ...serverUtil.BLANK_GENERIC_SERVER_RECORD, claim: give.fullClaim, claimType: "GiveAction", issuer: give.issuerDid, }; return libsUtil.isGiveRecordTheUserCanConfirm( this.isRegistered, giveDetails, this.activeDid, confirmerIdList, ); } shallowNotifyWhyCannotConfirm(give: GiveSummaryRecord) { const confirmerIds = this.recentlyCheckedAndUnconfirmableJwts.includes( give.jwtId, ) ? [this.activeDid] : []; libsUtil.notifyWhyCannotConfirm( this.$notify, this.isRegistered, "GiveAction", give, this.activeDid, confirmerIds, ); } async deepCheckConfirmable(give: GiveSummaryRecord) { this.checkingConfirmationForJwtId = give.jwtId; const confirmerInfo: libsUtil.ConfirmerData | undefined = await libsUtil.retrieveConfirmerIdList( this.apiServer, give.jwtId, give.issuerDid, this.activeDid, ); if ( this.checkIsConfirmable(give, confirmerInfo?.confirmerIdList as string[]) ) { this.confirmConfirmClaim(give); } else { this.recentlyCheckedAndUnconfirmableJwts = [ ...this.recentlyCheckedAndUnconfirmableJwts, give.jwtId, ]; libsUtil.notifyWhyCannotConfirm( this.$notify, this.isRegistered, "GiveAction", give, this.activeDid, confirmerInfo?.confirmerIdList as string[], ); } this.checkingConfirmationForJwtId = ""; } confirmConfirmClaim(give: GiveSummaryRecord) { this.$notify( { group: "modal", type: "confirm", title: "Confirm", text: "Do you personally confirm that this is true?", onYes: async () => { await this.confirmClaim(give); }, }, -1, ); } // similar code is found in ClaimView async confirmClaim(give: GiveSummaryRecord) { // similar logic is found in endorser-mobile const goodClaim = serverUtil.removeSchemaContext( serverUtil.removeVisibleToDids( serverUtil.addLastClaimOrHandleAsIdIfMissing( give.fullClaim, give.jwtId, give.handleId, ), ), ); const confirmationClaim: serverUtil.GenericVerifiableCredential = { "@context": "https://schema.org", "@type": "AgreeAction", object: goodClaim, }; const result = await serverUtil.createAndSubmitClaim( confirmationClaim, this.activeDid, this.apiServer, this.axios, ); if (result.type === "success") { this.$notify( { group: "alert", type: "success", title: "Success", text: "Confirmation submitted.", }, 5000, ); this.recentlyCheckedAndUnconfirmableJwts = [ ...this.recentlyCheckedAndUnconfirmableJwts, give.jwtId, ]; } else { console.error("Got error submitting the confirmation:", result); const message = (result.error?.error as string) || "There was a problem submitting the confirmation."; this.$notify( { group: "alert", type: "danger", title: "Error", text: message, }, 5000, ); } } } </script>