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.
 
 
 
 
 
 

411 lines
13 KiB

<template>
<QuickNav selected="Contacts" />
<section class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Header -->
<div class="mb-8">
<router-link
:to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</router-link>
<h1 class="text-4xl text-center font-light pt-4">
Transferred with {{ contact?.name }}
</h1>
</div>
<!-- Info Messages -->
<div class="text-center text-sm text-slate-600 mb-6 space-y-1">
<p>(Only 50 most recent)</p>
<p>
(This does not include claims by them if they're not visible to you.)
</p>
</div>
<!-- Transfer History Table -->
<table
class="table-auto w-full border-t border-slate-300 text-sm sm:text-base text-center"
>
<thead class="bg-slate-100">
<tr class="border-b border-slate-300">
<th class="px-1 py-2">Date</th>
<th class="px-1 py-2">From Them</th>
<th></th>
<th class="px-1 py-2">To Them</th>
</tr>
</thead>
<tbody>
<tr
v-for="record in giveRecords"
:key="record.jwtId"
class="border-b border-slate-300"
>
<!-- Date -->
<td class="p-1 text-xs sm:text-sm text-left text-slate-500">
{{ new Date(record.issuedAt).toLocaleString() }}
</td>
<!-- From Them -->
<td class="p-1">
<div v-if="record.agentDid === contact?.did">
<div class="font-bold">
{{ displayAmount(record.unit, record.amount) }}
<font-awesome
v-if="record.amountConfirmed"
icon="circle-check"
class="text-green-600 fa-fw"
title="Confirmed"
/>
<button v-else title="Unconfirmed" @click="confirm(record)">
<font-awesome icon="circle" class="text-blue-600 fa-fw" />
</button>
</div>
<div class="italic text-xs sm:text-sm text-slate-500">
{{ record.description }}
</div>
</div>
</td>
<!-- Direction Arrow -->
<td class="p-1">
<font-awesome
:icon="
record.agentDid === contact?.did ? 'arrow-left' : 'arrow-right'
"
class="text-slate-400 fa-fw"
/>
</td>
<!-- To Them -->
<td class="p-1">
<div v-if="record.agentDid !== contact?.did">
<div class="font-bold">
{{ displayAmount(record.unit, record.amount) }}
<font-awesome
v-if="record.amountConfirmed"
icon="circle-check"
class="text-green-600 fa-fw"
title="Confirmed"
/>
<button
v-else
title="Unconfirmed"
@click="cannotConfirmMessage()"
>
<font-awesome icon="circle" class="text-slate-600 fa-fw" />
</button>
</div>
<div class="italic text-xs sm:text-sm text-slate-500">
{{ record.description }}
</div>
</div>
</td>
</tr>
</tbody>
</table>
</section>
</template>
<script lang="ts">
import { AxiosError, AxiosRequestHeaders } from "axios";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import {
NOTIFY_SETTINGS_RETRIEVAL_ERROR,
NOTIFY_SERVER_RETRIEVAL_ERROR,
NOTIFY_CONFIRMATION_RESTRICTION,
} from "../constants/notifications";
import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { GiveSummaryRecord, GiveActionClaim } from "../interfaces";
import { AgreeActionClaim } from "../interfaces/claims";
import {
createEndorserJwtVcFromClaim,
displayAmount,
getHeaders,
SCHEMA_ORG_CONTEXT,
} from "../libs/endorserServer";
import { retrieveAccountCount } from "../libs/util";
/**
* Contact Amounts View Component
* @author Matthew Raymer
*
* This component displays the transfer history between the current user and a specific contact.
* It shows both incoming and outgoing transfers with confirmation status and allows users
* to confirm unconfirmed transfers.
*
* Features:
* - Displays transfer history in a table format
* - Shows confirmation status for each transfer
* - Allows confirmation of unconfirmed transfers
* - Handles server communication for transfer data
* - Provides error handling and user feedback
*
* Workflow:
* 1. Component loads with contact DID from route query
* 2. Fetches contact information from database via PlatformServiceMixin
* 3. Loads transfer history from server API
* 4. Displays transfers in chronological order
* 5. Allows user to confirm unconfirmed transfers
*
* Transfer Types:
* - From Them: Transfers received from the contact
* - To Them: Transfers sent to the contact
*
* Confirmation Status:
* - Confirmed: Transfer has been acknowledged by recipient
* - Unconfirmed: Transfer pending confirmation
* - Cannot Confirm: User is not the recipient of the transfer
*/
@Component({
components: { QuickNav },
mixins: [PlatformServiceMixin],
})
export default class ContactAmountssView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
/** Notification helpers */
notify!: ReturnType<typeof createNotifyHelpers>;
/** Active user DID */
activeDid = "";
/** API server URL */
apiServer = "";
/** Current contact data */
contact: Contact | null = null;
/** Array of transfer records */
giveRecords: Array<GiveSummaryRecord> = [];
/** Number of user accounts */
numAccounts = 0;
/** Display amount utility function */
displayAmount = displayAmount;
/**
* Component lifecycle hook that initializes account count
*/
async beforeCreate() {
this.numAccounts = await retrieveAccountCount();
}
/**
* Component lifecycle hook that initializes the contact amounts view
*
* Workflow:
* 1. Extracts contact DID from route query parameters
* 2. Queries database for contact information via PlatformServiceMixin
* 3. Retrieves user settings for active DID and API server
* 4. Loads transfer history if both active DID and contact are available
* 5. Handles errors with appropriate user notifications
*
* @throws Will not throw but notifies on errors
* @emits Notification on database or settings errors
*/
async created() {
this.notify = createNotifyHelpers(this.$notify);
try {
const contactDid = this.$route.query["contactDid"] as string;
const contact = await this.$getContact(contactDid);
this.contact = contact;
const settings = await this.$getSettings(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
await this.$logError("Error retrieving settings or gives.");
this.notify.error(
err.userMessage || NOTIFY_SETTINGS_RETRIEVAL_ERROR.message,
TIMEOUTS.LONG,
);
}
}
/**
* Loads transfer history from the server API
*
* Fetches both incoming and outgoing transfers between the active user and contact,
* then sorts them by date and updates the component state.
*
* @param activeDid The active user's DID
* @param contact The contact to load transfers with
* @throws Will not throw but notifies on server errors
* @emits Notification on server communication errors
*/
async loadGives(activeDid: string, contact: Contact) {
try {
let result: Array<GiveSummaryRecord> = [];
const url =
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(this.activeDid) +
"&recipientDid=" +
encodeURIComponent(contact.did);
const headers = await getHeaders(activeDid);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
result = resp.data.data;
} else {
await this.$logError(
`Got bad response status & data of ${resp.status} ${JSON.stringify(resp.data)}`,
);
this.notify.error(NOTIFY_SERVER_RETRIEVAL_ERROR.message, TIMEOUTS.LONG);
}
const url2 =
this.apiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(contact.did) +
"&recipientDid=" +
encodeURIComponent(this.activeDid);
const headers2 = await getHeaders(activeDid);
const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) {
result = R.concat(result, resp2.data.data);
} else {
await this.$logError(
`Got bad response status & data of ${resp2.status} ${JSON.stringify(resp2.data)}`,
);
this.notify.error(NOTIFY_SERVER_RETRIEVAL_ERROR.message, TIMEOUTS.LONG);
}
const sortedResult: Array<GiveSummaryRecord> = R.sort(
(a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result,
);
this.giveRecords = sortedResult;
} catch (error) {
this.notify.error(error as string, TIMEOUTS.LONG);
}
}
/**
* Confirms an unconfirmed transfer by creating and submitting a confirmation claim
*
* Creates an AgreeAction claim for the transfer and submits it to the server
* to confirm receipt of the transfer.
*
* @param record The transfer record to confirm
* @throws Will not throw but notifies on server errors
* @emits Notification on confirmation success or failure
*/
async confirm(record: GiveSummaryRecord) {
// Make claim
// I use clone here because otherwise it gets a Proxy object.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const origClaim: GiveActionClaim = R.clone(record.fullClaim);
if (record.fullClaim["@context"] == SCHEMA_ORG_CONTEXT) {
delete origClaim["@context"];
}
origClaim["identifier"] = record.handleId;
const vcClaim: AgreeActionClaim = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "AgreeAction",
object: origClaim,
};
const vcJwt: string = await createEndorserJwtVcFromClaim(
this.activeDid,
vcClaim,
);
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = this.apiServer + "/api/v2/claim";
const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
try {
const resp = await this.axios.post(url, payload, { headers });
if (resp.data?.success) {
record.amountConfirmed =
(origClaim.object?.amountOfThisGood as number) || 1;
}
} catch (error) {
let userMessage = "There was an error.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.notify.error(userMessage, TIMEOUTS.LONG);
}
}
/**
* Shows notification that user cannot confirm a transfer they didn't receive
*
* Only the recipient of a transfer can confirm its receipt.
* This method notifies users when they try to confirm a transfer
* that was sent to someone else.
*
* @emits Notification explaining confirmation restrictions
*/
cannotConfirmMessage() {
this.notify.error(
NOTIFY_CONFIRMATION_RESTRICTION.message,
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>