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.
1104 lines
36 KiB
1104 lines
36 KiB
<template>
|
|
<QuickNav />
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Breadcrumb -->
|
|
<div id="ViewBreadcrumb" class="mb-8">
|
|
<h1 class="text-lg text-center font-light relative px-7">
|
|
<!-- Back -->
|
|
<button
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
aria-label="Go back"
|
|
@click="$router.go(-1)"
|
|
>
|
|
<font-awesome icon="chevron-left" class="fa-fw" />
|
|
</button>
|
|
Verifiable Claim Details
|
|
</h1>
|
|
</div>
|
|
|
|
<!-- Details -->
|
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4 w-full">
|
|
<div class="block flex gap-4 overflow-hidden w-full">
|
|
<div class="w-full">
|
|
<div class="flex columns-3">
|
|
<h2 class="text-md font-bold w-full">
|
|
{{
|
|
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
|
|
}}
|
|
<button
|
|
v-if="canEditClaim"
|
|
title="Edit"
|
|
data-testId="editClaimButton"
|
|
@click="onClickEditClaim"
|
|
>
|
|
<font-awesome
|
|
icon="pen"
|
|
class="text-sm text-blue-500 ml-2 mb-1"
|
|
/>
|
|
</button>
|
|
</h2>
|
|
<div class="flex justify-center w-full">
|
|
<router-link
|
|
v-if="veriClaim.id"
|
|
:to="'/claim-cert/' + encodeURIComponent(veriClaim.id)"
|
|
class="text-blue-500 mt-2"
|
|
title="View Printable Certificate"
|
|
>
|
|
<font-awesome
|
|
icon="square"
|
|
class="text-white bg-yellow-500 p-1"
|
|
/>
|
|
</router-link>
|
|
<button
|
|
v-if="veriClaim.id"
|
|
class="text-blue-500 ml-2 mt-2"
|
|
title="Copy Printable Certificate Link"
|
|
aria-label="Copy printable certificate link"
|
|
@click="
|
|
copyToClipboard(
|
|
'A link to the certificate page',
|
|
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
|
)
|
|
"
|
|
>
|
|
<font-awesome icon="link" class="text-yellow-500 p-1" />
|
|
</button>
|
|
</div>
|
|
<!-- show link icon to copy this URL to the clipboard -->
|
|
<div class="flex justify-end w-full">
|
|
<button
|
|
title="Copy Link"
|
|
aria-label="Copy page link"
|
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
|
>
|
|
<font-awesome icon="link" class="text-slate-500" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="text-sm">
|
|
<div data-testId="description">
|
|
<font-awesome icon="message" class="fa-fw text-slate-400" />
|
|
{{ claimDescription }}
|
|
</div>
|
|
<div>
|
|
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
|
{{ didInfo(veriClaim.issuer) }}
|
|
</div>
|
|
<div>
|
|
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
|
|
Recorded
|
|
{{ formattedIssueDate }}
|
|
</div>
|
|
<div v-if="claimImage" class="flex justify-center">
|
|
<a :href="claimImage" target="_blank">
|
|
<img :src="claimImage" class="h-24 rounded-xl" />
|
|
</a>
|
|
</div>
|
|
|
|
<div v-if="veriClaim.claimType === 'PlanAction'" class="mt-4">
|
|
<router-link
|
|
:to="'/project/' + encodeURIComponent(veriClaim.handleId)"
|
|
class="text-blue-500 mt-2"
|
|
>
|
|
Go to Project page
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- Fullfills Links -->
|
|
|
|
<!-- fullfills links for a give -->
|
|
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4">
|
|
<router-link
|
|
:to="
|
|
'/project/' +
|
|
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
|
"
|
|
class="text-blue-500 mt-2"
|
|
>
|
|
Fulfills a bigger plan...
|
|
</router-link>
|
|
</div>
|
|
<!-- if there's another, it's probably fulfilling an offer, too -->
|
|
<div
|
|
v-if="
|
|
detailsForGive?.fulfillsType &&
|
|
detailsForGive?.fulfillsType !== 'PlanAction' &&
|
|
detailsForGive?.fulfillsHandleId
|
|
"
|
|
>
|
|
<!-- router-link to /claim/ only changes URL path -->
|
|
<a
|
|
class="text-blue-500 mt-4 cursor-pointer"
|
|
@click="
|
|
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
|
|
"
|
|
>
|
|
Fulfills
|
|
{{
|
|
capitalizeAndInsertSpacesBeforeCaps(
|
|
detailsForGive.fulfillsType,
|
|
)
|
|
}}...
|
|
</a>
|
|
</div>
|
|
|
|
<!-- fullfills links for an offer -->
|
|
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
|
<router-link
|
|
:to="
|
|
'/project/' +
|
|
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
|
"
|
|
class="text-blue-500 mt-4"
|
|
>
|
|
Offered to a bigger plan...
|
|
</router-link>
|
|
</div>
|
|
|
|
<!-- Providers -->
|
|
<div v-if="providersForGive?.length > 0" class="mt-4">
|
|
<span>Other assistance provided by:</span>
|
|
<ul class="ml-4">
|
|
<li
|
|
v-for="provider of providersForGive"
|
|
:key="provider.identifier"
|
|
class="list-disc ml-4"
|
|
>
|
|
<div class="flex gap-4">
|
|
<div class="grow overflow-hidden">
|
|
<a
|
|
class="text-blue-500 mt-4 cursor-pointer"
|
|
@click="handleProviderClick(provider)"
|
|
>
|
|
an activity...
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-2">
|
|
<font-awesome icon="comment" class="text-slate-400" />
|
|
{{ issuerName }} posted that.
|
|
</div>
|
|
<!--
|
|
<div>
|
|
<router-link :to="'/claim-cert/' + encodeURIComponent(veriClaim.id)">
|
|
<font-awesome icon="file-contract" class="text-slate-400" />
|
|
<span class="ml-2 text-blue-500">Printable Certificate</span>
|
|
</router-link>
|
|
</div>
|
|
-->
|
|
|
|
<div class="mt-8">
|
|
<button
|
|
v-if="libsUtil.canFulfillOffer(veriClaim, isRegistered)"
|
|
class="col-span-1 block w-fit text-center text-md 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.5 py-2 rounded-md"
|
|
@click="openFulfillGiftDialog()"
|
|
>
|
|
Affirm Delivery
|
|
<font-awesome
|
|
icon="hand-holding-heart"
|
|
class="ml-2 text-white cursor-pointer"
|
|
/>
|
|
</button>
|
|
</div>
|
|
<GiftedDialog ref="customGiveDialog" />
|
|
|
|
<div v-if="libsUtil.isGiveAction(veriClaim)">
|
|
<div class="flex columns-3">
|
|
<button
|
|
v-if="
|
|
libsUtil.isGiveRecordTheUserCanConfirm(
|
|
isRegistered,
|
|
veriClaim,
|
|
activeDid,
|
|
confirmerIdList,
|
|
)
|
|
"
|
|
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
|
|
@click="confirmConfirmClaim()"
|
|
>
|
|
Confirm
|
|
<font-awesome
|
|
icon="circle-check"
|
|
class="ml-2 text-white cursor-pointer"
|
|
/>
|
|
</button>
|
|
<h2 v-else class="font-bold uppercase text-xl mt-2">Confirmations</h2>
|
|
|
|
<span class="mt-0.5 px-4 py-2">
|
|
<router-link
|
|
v-if="libsUtil.isGiveAction(veriClaim)"
|
|
:to="'/confirm-gift/' + encodeURIComponent(veriClaim.id)"
|
|
class="col-span-1 text-blue-500"
|
|
data-testId="confirmGiftLink"
|
|
>
|
|
Details...
|
|
</router-link>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="mt-2">
|
|
{{ confirmationStatusText }}
|
|
</div>
|
|
|
|
<div v-if="totalConfirmers() > 0">
|
|
<div
|
|
v-if="
|
|
confirmerIdList.length === 0 && confsVisibleToIdList.length === 0
|
|
"
|
|
>
|
|
Nobody that you know confirmed this claim, nor do they have any
|
|
confirmers in their network.
|
|
</div>
|
|
|
|
<div
|
|
v-if="confirmerIdList.length === 0 && confsVisibleToIdList.length > 0"
|
|
>
|
|
<!-- Only show if this person has links to confirmers (below). -->
|
|
Nobody that you know has issued or confirmed this claim.
|
|
</div>
|
|
<div v-if="confirmerIdList.length > 0">
|
|
The following people have confirmed this claim.
|
|
<ul class="ml-4">
|
|
<li
|
|
v-for="confirmerId in confirmerIdList"
|
|
:key="confirmerId"
|
|
class="list-disc ml-4"
|
|
>
|
|
<div class="flex gap-4">
|
|
<div class="grow overflow-hidden">
|
|
<div class="text-sm">
|
|
{{ didInfo(confirmerId) }}
|
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
|
<router-link
|
|
:to="{
|
|
path: '/did/' + encodeURIComponent(confirmerId),
|
|
}"
|
|
class="text-blue-500"
|
|
>
|
|
<font-awesome
|
|
icon="arrow-up-right-from-square"
|
|
class="fa-fw"
|
|
/>
|
|
</router-link>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!--
|
|
Never need to show this message:
|
|
"Nobody that you know can see someone who has confirmed this claim."
|
|
|
|
If there is nobody in the confirmerIdList then we'll show the combined "nobody" message.
|
|
If there is somebody in the confirmerIdList then that's all they need to show.
|
|
-->
|
|
|
|
<!-- Now show anyone linked to confirmers. -->
|
|
<div v-if="confsVisibleToIdList.length > 0">
|
|
The following people can connect you with people who have issued or
|
|
confirmed this claim.
|
|
<ul class="ml-4">
|
|
<li
|
|
v-for="confsVisibleTo in confsVisibleToIdList"
|
|
:key="confsVisibleTo"
|
|
class="list-disc ml-4"
|
|
>
|
|
<div class="flex gap-4">
|
|
<div class="grow overflow-hidden">
|
|
<div class="text-sm">
|
|
{{ didInfo(confsVisibleTo) }}
|
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
|
|
<router-link
|
|
:to="{
|
|
path: '/did/' + encodeURIComponent(confsVisibleTo),
|
|
}"
|
|
class="text-blue-500"
|
|
>
|
|
<font-awesome
|
|
icon="arrow-up-right-from-square"
|
|
class="fa-fw"
|
|
/>
|
|
</router-link>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- explain if user cannot confirm -->
|
|
<!-- Note that these conditions are mirrored in userCanConfirm(). -->
|
|
<div v-if="confirmerIdList.includes(activeDid)">
|
|
You have confirmed this claim.
|
|
</div>
|
|
<div v-else-if="veriClaim.issuer == activeDid">
|
|
You cannot confirm this because you issued this claim, so you already
|
|
count as confirming it.
|
|
</div>
|
|
<div v-else-if="serverUtil.containsHiddenDid(veriClaim.claim)">
|
|
You cannot confirm this because it contains hidden identifiers.
|
|
</div>
|
|
</div>
|
|
|
|
<!--
|
|
Note that a similar section is found in ConfirmGiftView.vue, and kinda in HiddenDidDialog.vue
|
|
-->
|
|
<h2
|
|
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
|
@click="showVeriClaimDump = !showVeriClaimDump"
|
|
>
|
|
Details
|
|
<font-awesome v-if="showVeriClaimDump" icon="chevron-up" />
|
|
<font-awesome v-else icon="chevron-right" />
|
|
</h2>
|
|
<div v-if="showVeriClaimDump">
|
|
<div v-if="isFullyHidden" class="mb-2">
|
|
Some of the details are not visible to you; they show as "HIDDEN". They
|
|
are not visible to any of your direct contacts, either.
|
|
<span v-if="canShare">
|
|
You can ask one of your contacts to take a look and see if their
|
|
contacts can see more details:
|
|
<a class="text-blue-500" @click="onClickShareClaim()"
|
|
>click to send them this page info</a
|
|
>
|
|
and see if they can make an introduction. Someone is connected to
|
|
people closer to them; if you don't know who to ask, try the person
|
|
who registered you.
|
|
</span>
|
|
<span v-else>
|
|
You can ask one of your contacts to take a look and see if their
|
|
contacts can see more details:
|
|
<a
|
|
class="text-blue-500"
|
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
|
>click to copy this page info</a
|
|
>
|
|
and see if they can make an introduction. Someone is connected to
|
|
people closer to them; if you don't know who to ask, try the person
|
|
who registered you.
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="hasPartialVisibility">
|
|
Some of the details are not visible to you but they are visible to some
|
|
of your contacts.
|
|
<span v-if="canShare">
|
|
If you'd like an introduction,
|
|
<a class="text-blue-500" @click="onClickShareClaim()"
|
|
>click to share the information with them and ask if they'll tell
|
|
you more about the participants.</a
|
|
>
|
|
</span>
|
|
<span v-else>
|
|
If you'd like an introduction,
|
|
<a
|
|
class="text-blue-500"
|
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
|
>share this page with them and ask if they'll tell you more about
|
|
about the participants.</a
|
|
>
|
|
</span>
|
|
|
|
<div
|
|
v-for="(visibleDidPath, index) of Object.keys(veriClaimDidsVisible)"
|
|
:key="index"
|
|
class="list-disc p-4"
|
|
>
|
|
<div class="text-sm">
|
|
<font-awesome icon="minus" class="fa-fw" />
|
|
The {{ visibleDidPath }} is visible to:
|
|
</div>
|
|
<div class="ml-12 p-1">
|
|
<ul>
|
|
<li
|
|
v-for="(visDid, idx2) of veriClaimDidsVisible[visibleDidPath]"
|
|
:key="idx2"
|
|
class="list-disc"
|
|
>
|
|
<div class="text-sm mt-2">
|
|
<span>
|
|
{{ didInfo(visDid) }}
|
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
|
<router-link
|
|
:to="{
|
|
path: '/did/' + encodeURIComponent(visDid),
|
|
}"
|
|
class="text-blue-500"
|
|
>
|
|
<font-awesome
|
|
icon="arrow-up-right-from-square"
|
|
class="fa-fw"
|
|
/>
|
|
</router-link>
|
|
</span>
|
|
<span v-if="veriClaim.publicUrls?.[visDid]"
|
|
>, found at <a
|
|
:href="veriClaim.publicUrls?.[visDid]"
|
|
target="_blank"
|
|
class="text-blue-500"
|
|
>
|
|
<font-awesome icon="globe" class="fa-fw" />
|
|
{{ extractDomain(veriClaim.publicUrls[visDid] || "") }}
|
|
</a>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<span v-if="isEditedGlobalId" class="mt-2">
|
|
This record is an edited version. The latest version is shown.
|
|
</span>
|
|
<br />
|
|
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
|
<pre
|
|
v-if="showVeriClaimDump"
|
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
|
>{{ veriClaimDump }}</pre
|
|
>
|
|
|
|
<h2 class="text-xl mt-8 mb-2">Full Claim</h2>
|
|
<p class="mb-4">
|
|
The full claim includes the claim as it was originally issued, including
|
|
the signature (ie. the proof of issuance by that person).
|
|
</p>
|
|
<div v-if="!fullClaim">
|
|
<p v-if="fullClaimMessage" class="mb-4">
|
|
{{ fullClaimMessage }}
|
|
</p>
|
|
<button
|
|
v-else
|
|
class="text-blue-500 cursor-pointer"
|
|
@click="showFullClaim(veriClaim.id as string)"
|
|
>
|
|
<font-awesome icon="file-lines" class="fa-fw" />
|
|
Load Full Claim Details
|
|
</button>
|
|
</div>
|
|
<div v-else>
|
|
<pre
|
|
class="text-sm overflow-x-scroll bg-slate-100 px-4 py-3 rounded-md"
|
|
>{{ fullClaimDump }}</pre
|
|
>
|
|
</div>
|
|
|
|
<a
|
|
:href="apiServer + '/api/claim/' + veriClaim.id"
|
|
target="_blank"
|
|
class="text-blue-500 cursor-pointer"
|
|
>
|
|
<font-awesome icon="file-lines" class="fa-fw" />
|
|
<font-awesome icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
|
|
View on the Public Server
|
|
</a>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { AxiosError } from "axios";
|
|
import * as yaml from "js-yaml";
|
|
import * as R from "ramda";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
|
import { useClipboard } from "@vueuse/core";
|
|
import { GenericVerifiableCredential } from "../interfaces";
|
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import * as serverUtil from "../libs/endorserServer";
|
|
import {
|
|
GenericCredWrapper,
|
|
OfferClaim,
|
|
GiveActionClaim,
|
|
ProviderInfo,
|
|
} from "../interfaces";
|
|
import * as libsUtil from "../libs/util";
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|
|
|
@Component({
|
|
components: { GiftedDialog, QuickNav },
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
export default class ClaimView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
$router!: Router;
|
|
|
|
activeDid = "";
|
|
allMyDids: Array<string> = [];
|
|
allContacts: Array<Contact> = [];
|
|
apiServer = "";
|
|
|
|
canShare = false;
|
|
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
|
|
confsVisibleErrorMessage = "";
|
|
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
|
detailsForGive: {
|
|
fulfillsPlanHandleId?: string;
|
|
fulfillsType?: string;
|
|
fulfillsHandleId?: string;
|
|
} | null = null;
|
|
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
|
|
fullClaim = null;
|
|
fullClaimDump = "";
|
|
fullClaimMessage = "";
|
|
isEditedGlobalId = false;
|
|
isRegistered = false;
|
|
issuerName = "";
|
|
numConfsNotVisible = 0; // number of hidden DIDs in the confirmerIdList, minus the issuer if they aren't visible
|
|
providersForGive: ProviderInfo[] = [];
|
|
showIdCopy = false;
|
|
showVeriClaimDump = false;
|
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
|
veriClaimDump = "";
|
|
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
|
windowDeepLink = window.location.href; // changed in the setup for deep linking
|
|
|
|
APP_SERVER = APP_SERVER;
|
|
R = R;
|
|
yaml = yaml;
|
|
libsUtil = libsUtil;
|
|
serverUtil = serverUtil;
|
|
|
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
|
|
|
// =================================================
|
|
// COMPUTED PROPERTIES
|
|
// =================================================
|
|
|
|
/**
|
|
* Whether the current user can edit this claim
|
|
*/
|
|
get canEditClaim(): boolean {
|
|
return (
|
|
["GiveAction", "Offer", "PlanAction"].includes(
|
|
this.veriClaim.claimType as string,
|
|
) && this.veriClaim.issuer === this.activeDid
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The description to display for this claim
|
|
*/
|
|
get claimDescription(): string {
|
|
const claim = this.veriClaim.claim;
|
|
|
|
// Handle Offer claims with itemOffered
|
|
if (this.veriClaim.claimType === "Offer") {
|
|
const offerClaim = claim as OfferClaim;
|
|
return (
|
|
offerClaim.itemOffered?.description || offerClaim.description || ""
|
|
);
|
|
}
|
|
|
|
// Handle GiveAction claims
|
|
if (this.veriClaim.claimType === "GiveAction") {
|
|
const giveClaim = claim as GiveActionClaim;
|
|
return giveClaim.description || "";
|
|
}
|
|
|
|
// Fallback for other claim types
|
|
return (claim as { description?: string })?.description || "";
|
|
}
|
|
|
|
/**
|
|
* Formatted issue date for display
|
|
*/
|
|
get formattedIssueDate(): string {
|
|
return (
|
|
this.veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") || ""
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Text describing the confirmation status
|
|
*/
|
|
get confirmationStatusText(): string {
|
|
const count = this.totalConfirmers();
|
|
if (count === 0) return "Nobody has confirmed this.";
|
|
if (count === 1) return "One person has confirmed this.";
|
|
return `${count} people have confirmed this.`;
|
|
}
|
|
|
|
/**
|
|
* Whether the claim is fully hidden from the user
|
|
*/
|
|
get isFullyHidden(): boolean {
|
|
return (
|
|
serverUtil.containsHiddenDid(this.veriClaim) &&
|
|
R.isEmpty(this.veriClaimDidsVisible)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Whether the claim has some visibility through contacts
|
|
*/
|
|
get hasPartialVisibility(): boolean {
|
|
return !R.isEmpty(this.veriClaimDidsVisible);
|
|
}
|
|
|
|
/**
|
|
* The image URL for this claim if available
|
|
*/
|
|
get claimImage(): string | undefined {
|
|
const claim = this.veriClaim.claim;
|
|
|
|
// Handle different claim types that might have images
|
|
if (this.veriClaim.claimType === "Offer") {
|
|
const offerClaim = claim as OfferClaim;
|
|
return offerClaim.image;
|
|
}
|
|
|
|
if (this.veriClaim.claimType === "GiveAction") {
|
|
const giveClaim = claim as GiveActionClaim;
|
|
return giveClaim.image;
|
|
}
|
|
|
|
// Fallback for other claim types
|
|
return (claim as { image?: string })?.image;
|
|
}
|
|
|
|
resetThisValues() {
|
|
this.confirmerIdList = [];
|
|
this.confsVisibleErrorMessage = "";
|
|
this.confsVisibleToIdList = [];
|
|
this.detailsForGive = null;
|
|
this.detailsForOffer = null;
|
|
this.fullClaim = null;
|
|
this.fullClaimDump = "";
|
|
this.fullClaimMessage = "";
|
|
this.isEditedGlobalId = false;
|
|
this.numConfsNotVisible = 0;
|
|
this.providersForGive = [];
|
|
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
|
this.veriClaimDump = "";
|
|
this.veriClaimDidsVisible = {};
|
|
}
|
|
|
|
// =================================================
|
|
// UTILITY METHODS
|
|
// =================================================
|
|
|
|
/**
|
|
* Handle provider click navigation
|
|
*/
|
|
handleProviderClick(provider: ProviderInfo): void {
|
|
if (provider.identifier.startsWith("did:")) {
|
|
this.$router.push("/did/" + encodeURIComponent(provider.identifier));
|
|
} else {
|
|
this.showDifferentClaimPage(provider.identifier);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract domain from URL for display
|
|
*/
|
|
extractDomain(url: string): string {
|
|
return url.substring(url.indexOf("//") + 2);
|
|
}
|
|
|
|
async created() {
|
|
const settings = await this.$settings();
|
|
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.allContacts = await this.$contacts();
|
|
|
|
this.isRegistered = settings.isRegistered || false;
|
|
|
|
try {
|
|
this.allMyDids = await libsUtil.retrieveAccountDids();
|
|
} catch (error) {
|
|
await this.$logAndConsole(
|
|
"Error retrieving all account DIDs on home page:" + error,
|
|
true,
|
|
);
|
|
this.notify.error(
|
|
"See the Help page for problems with your personal data.",
|
|
);
|
|
}
|
|
|
|
const claimId = this.$route.params.id as string;
|
|
if (claimId) {
|
|
await this.loadClaim(claimId, this.activeDid);
|
|
} else {
|
|
this.notify.error("No claim ID was provided.");
|
|
}
|
|
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
|
|
|
|
this.canShare = !!navigator.share;
|
|
|
|
this.notify = createNotifyHelpers(this.$notify);
|
|
}
|
|
|
|
// insert a space before any capital letters except the initial letter
|
|
// (and capitalize initial letter, just in case)
|
|
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
|
|
if (!text) return "";
|
|
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
|
}
|
|
|
|
totalConfirmers() {
|
|
return (
|
|
this.numConfsNotVisible +
|
|
this.confirmerIdList.length +
|
|
this.confsVisibleToIdList.length
|
|
);
|
|
}
|
|
|
|
// Isn't there a better way to make this available to the template?
|
|
didInfo(did: string): string {
|
|
return serverUtil.didInfo(
|
|
did,
|
|
this.activeDid,
|
|
this.allMyDids,
|
|
this.allContacts,
|
|
);
|
|
}
|
|
|
|
async loadClaim(claimId: string, userDid: string) {
|
|
const urlPath = libsUtil.isGlobalUri(claimId)
|
|
? "/api/claim/byHandle/"
|
|
: "/api/claim/";
|
|
const url = this.apiServer + urlPath + encodeURIComponent(claimId);
|
|
const headers = await serverUtil.getHeaders(userDid);
|
|
|
|
try {
|
|
const resp = await this.axios.get(url, { headers });
|
|
if (resp.status === 200) {
|
|
this.veriClaim = resp.data;
|
|
this.issuerName = this.didInfo(this.veriClaim.issuer);
|
|
this.veriClaimDump = yaml.dump(this.veriClaim);
|
|
this.veriClaimDidsVisible = libsUtil.findAllVisibleToDids(
|
|
this.veriClaim,
|
|
true,
|
|
);
|
|
} else {
|
|
// actually, axios typically throws an error so we never get here
|
|
await this.$logError("Error getting claim: " + JSON.stringify(resp));
|
|
this.notify.error("There was a problem retrieving that claim.");
|
|
return;
|
|
}
|
|
|
|
this.isEditedGlobalId = !this.veriClaim.handleId.endsWith(claimId);
|
|
|
|
// retrieve more details on Give, Offer, or Plan
|
|
if (this.veriClaim.claimType === "GiveAction") {
|
|
const giveUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/gives?handleId=" +
|
|
encodeURIComponent(this.veriClaim.handleId as string);
|
|
const giveHeaders = await serverUtil.getHeaders(userDid);
|
|
const giveResp = await this.axios.get(giveUrl, {
|
|
headers: giveHeaders,
|
|
});
|
|
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
|
|
this.detailsForGive = giveResp.data.data[0];
|
|
} else {
|
|
await this.$logError(
|
|
"Error getting detailed give info: " + JSON.stringify(giveResp),
|
|
);
|
|
}
|
|
|
|
// look for providers
|
|
const providerUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/providersToGive?handleId=" +
|
|
encodeURIComponent(this.veriClaim.handleId as string);
|
|
const providerHeaders = await serverUtil.getHeaders(userDid);
|
|
const providerResp = await this.axios.get(providerUrl, {
|
|
headers: providerHeaders,
|
|
});
|
|
// should be at least an empty array
|
|
if (
|
|
providerResp.status === 200 &&
|
|
Array.isArray(providerResp.data.data)
|
|
) {
|
|
this.providersForGive = providerResp.data.data;
|
|
} else {
|
|
await this.$logError(
|
|
"Error getting give providers: " + JSON.stringify(giveResp),
|
|
);
|
|
this.notify.warning("Got error retrieving linked provider data.");
|
|
}
|
|
} else if (this.veriClaim.claimType === "Offer") {
|
|
const offerUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/offers?handleId=" +
|
|
encodeURIComponent(this.veriClaim.handleId as string);
|
|
const offerHeaders = await serverUtil.getHeaders(userDid);
|
|
const offerResp = await this.axios.get(offerUrl, {
|
|
headers: offerHeaders,
|
|
});
|
|
if (offerResp.status === 200) {
|
|
this.detailsForOffer = offerResp.data.data[0];
|
|
} else {
|
|
await this.$logError(
|
|
"Error getting detailed offer info: " + JSON.stringify(offerResp),
|
|
);
|
|
this.notify.warning("Got error retrieving linked offer data.");
|
|
}
|
|
}
|
|
|
|
// retrieve the list of confirmers
|
|
const confirmerInfo = await libsUtil.retrieveConfirmerIdList(
|
|
this.apiServer,
|
|
claimId,
|
|
this.veriClaim.issuer,
|
|
userDid,
|
|
);
|
|
if (confirmerInfo) {
|
|
this.confirmerIdList = confirmerInfo.confirmerIdList;
|
|
this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList;
|
|
this.numConfsNotVisible = confirmerInfo.numConfsNotVisible;
|
|
} else {
|
|
this.confsVisibleErrorMessage =
|
|
"Had problems retrieving confirmations.";
|
|
}
|
|
} catch (error: unknown) {
|
|
const serverError = error as AxiosError;
|
|
await this.$logError(
|
|
"Error retrieving claim: " + JSON.stringify(serverError),
|
|
);
|
|
this.notify.error(
|
|
"Something went wrong retrieving claim data.",
|
|
TIMEOUTS.STANDARD,
|
|
);
|
|
}
|
|
}
|
|
|
|
async showFullClaim(claimId: string) {
|
|
const url =
|
|
this.apiServer + "/api/claim/full/" + encodeURIComponent(claimId);
|
|
const headers = await serverUtil.getHeaders(this.activeDid);
|
|
|
|
try {
|
|
const resp = await this.axios.get(url, { headers });
|
|
if (resp.status === 200) {
|
|
this.fullClaim = resp.data;
|
|
this.fullClaimDump = yaml.dump(this.fullClaim);
|
|
} else {
|
|
// actually, axios typically throws an error so we never get here
|
|
await this.$logError(
|
|
"Error getting full claim: " + JSON.stringify(resp),
|
|
);
|
|
this.notify.error("There was a problem getting that claim.");
|
|
}
|
|
} catch (error: unknown) {
|
|
await this.$logError(
|
|
"Error retrieving full claim: " + JSON.stringify(error),
|
|
);
|
|
const serverError = error as AxiosError;
|
|
if (serverError.response?.status === 403) {
|
|
let issuerPhrase = "";
|
|
const issuerContact = serverUtil.contactForDid(
|
|
this.veriClaim.issuer,
|
|
this.allContacts,
|
|
);
|
|
if (issuerContact?.name) {
|
|
issuerPhrase +=
|
|
"Ask " +
|
|
issuerContact.name +
|
|
" to show you the full claim details.";
|
|
}
|
|
if (
|
|
this.confirmerIdList.length > 0 ||
|
|
this.confsVisibleToIdList.length > 0
|
|
) {
|
|
if (issuerContact?.name) {
|
|
issuerPhrase +=
|
|
"You could also ask someone in the Confirmations section to make an introduction.";
|
|
} else {
|
|
issuerPhrase +=
|
|
"Ask someone in the Confirmations section to make an introduction.";
|
|
}
|
|
}
|
|
this.fullClaimMessage =
|
|
"You are not authorized to view the full contents of this claim." +
|
|
issuerPhrase +
|
|
" You might ask someone in your network -- like the person who registered you --" +
|
|
" if they can find out more and make an introduction: " +
|
|
" send them this page and see if they can make a connection for you.";
|
|
} else {
|
|
this.notify.error(
|
|
"Something went wrong retrieving that claim.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
confirmConfirmClaim() {
|
|
this.notify.confirm(
|
|
"Do you personally confirm that this is true?",
|
|
async () => {
|
|
await this.confirmClaim();
|
|
},
|
|
);
|
|
}
|
|
|
|
// similar code is found in ProjectViewView
|
|
async confirmClaim() {
|
|
// similar logic is found in endorser-mobile
|
|
const goodClaim = serverUtil.removeSchemaContext(
|
|
serverUtil.removeVisibleToDids(
|
|
serverUtil.addLastClaimOrHandleAsIdIfMissing(
|
|
this.veriClaim.claim,
|
|
this.veriClaim.id,
|
|
this.veriClaim.handleId,
|
|
),
|
|
),
|
|
);
|
|
const confirmationClaim: GenericVerifiableCredential = {
|
|
"@context": "https://schema.org",
|
|
"@type": "AgreeAction",
|
|
object: goodClaim,
|
|
};
|
|
const result = await serverUtil.createAndSubmitClaim(
|
|
confirmationClaim,
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
);
|
|
if (result.success) {
|
|
this.notify.confirmationSubmitted();
|
|
} else {
|
|
await this.$logError(
|
|
"Got error submitting the confirmation: " + JSON.stringify(result),
|
|
);
|
|
this.notify.error("There was a problem submitting the confirmation.");
|
|
}
|
|
}
|
|
|
|
showDifferentClaimPage(claimId: string) {
|
|
const route = {
|
|
path: "/claim/" + encodeURIComponent(claimId),
|
|
};
|
|
(this.$router as Router).push(route).then(async () => {
|
|
this.resetThisValues();
|
|
await this.loadClaim(claimId, this.activeDid);
|
|
});
|
|
}
|
|
|
|
openFulfillGiftDialog() {
|
|
const giver: libsUtil.GiverReceiverInputInfo = {
|
|
did: libsUtil.offerGiverDid(
|
|
this.veriClaim as GenericCredWrapper<OfferClaim>,
|
|
),
|
|
};
|
|
(this.$refs.customGiveDialog as GiftedDialog).open(
|
|
giver,
|
|
undefined,
|
|
this.veriClaim.handleId,
|
|
"Offer fulfilled by " + (giver?.name || "someone not named"),
|
|
);
|
|
}
|
|
|
|
copyToClipboard(name: string, text: string) {
|
|
useClipboard()
|
|
.copy(text)
|
|
.then(() => {
|
|
this.notify.copied(name || "That");
|
|
});
|
|
}
|
|
|
|
onClickShareClaim() {
|
|
this.copyToClipboard("A link to this page", this.windowDeepLink);
|
|
window.navigator.share({
|
|
title: "Help Connect Me",
|
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
|
url: this.windowDeepLink,
|
|
});
|
|
}
|
|
|
|
async onClickEditClaim() {
|
|
if (this.veriClaim.claimType === "GiveAction") {
|
|
const route = {
|
|
name: "gifted-details",
|
|
query: {
|
|
prevCredToEdit: JSON.stringify(this.veriClaim),
|
|
destinationPathAfter:
|
|
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
|
|
},
|
|
};
|
|
(this.$router as Router).push(route);
|
|
} else if (this.veriClaim.claimType === "Offer") {
|
|
const route = {
|
|
name: "offer-details",
|
|
query: {
|
|
prevCredToEdit: JSON.stringify(this.veriClaim),
|
|
destinationPathAfter:
|
|
"/claim/" + encodeURIComponent(this.veriClaim.handleId),
|
|
},
|
|
};
|
|
(this.$router as Router).push(route);
|
|
} else if (this.veriClaim.claimType === "PlanAction") {
|
|
const route = {
|
|
name: "new-edit-project",
|
|
query: { projectId: this.veriClaim.handleId },
|
|
};
|
|
(this.$router as Router).push(route);
|
|
} else {
|
|
await this.$logError(
|
|
"Unrecognized claim type for edit: " + this.veriClaim.claimType,
|
|
);
|
|
this.notify.error(
|
|
"This is an unrecognized claim type.",
|
|
TIMEOUTS.STANDARD,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
/*
|
|
Tooltip, generated on "title" attributes on "fa" icons
|
|
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
|
*/
|
|
/* Tooltip container */
|
|
.tooltip {
|
|
position: relative;
|
|
display: inline-block;
|
|
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
|
|
}
|
|
|
|
/* Tooltip text */
|
|
.tooltip .tooltiptext {
|
|
visibility: hidden;
|
|
width: 200px;
|
|
background-color: black;
|
|
color: #fff;
|
|
text-align: center;
|
|
padding: 5px 0;
|
|
border-radius: 6px;
|
|
|
|
position: absolute;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Show the tooltip text when you mouse over the tooltip container */
|
|
.tooltip:hover .tooltiptext {
|
|
visibility: visible;
|
|
}
|
|
.tooltip:hover .tooltiptext-left {
|
|
visibility: visible;
|
|
}
|
|
</style>
|
|
|