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.
 
 
 
 
 
 

409 lines
13 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="backButtonClasses" @click="$router.back()">
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</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">
<font-awesome icon="spinner" class="fa-spin-pulse" />
</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
v-for="record in claimsToConfirm"
:key="record.id"
class="border-b border-slate-300 py-2"
>
<div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start">
<span>
<input
type="checkbox"
:checked="claimsToConfirmSelected.includes(record.id)"
class="mr-2 h-6 w-6"
@click="
claimsToConfirmSelected.includes(record.id)
? claimsToConfirmSelected.splice(
claimsToConfirmSelected.indexOf(record.id),
1,
)
: claimsToConfirmSelected.push(record.id)
"
/>
</span>
{{
claimSpecialDescription(
record,
activeDid,
allMyDids,
allContacts,
)
}}
<a @click="onClickLoadClaim(record.id)">
<font-awesome
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>
{{ claimCountWithHiddenText }}
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
<font-awesome icon="users" class="text-slate-500" />
page.
</span>
</div>
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
<span>
{{ claimCountByUserText }}
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 v-model="someoneGave" type="checkbox" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">The group provided</span>
<span v-if="someoneGave">
<input
v-model="description"
type="text"
size="20"
class="border border-slate-400 h-6 px-2"
/>
<br />
(Everyone likes personalized messages! 😁 ... and for a pic:
<input v-model="supplyGiftDetails" type="checkbox" />)
</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="canSubmit" class="flex justify-center mt-4">
<button :class="submitButtonClasses" @click="record()">
Sign & Send
</button>
</div>
<div v-else class="flex justify-center mt-4">
<button :class="disabledButtonClasses">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 { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import {
GenericCredWrapper,
GenericVerifiableCredential,
CreateAndSubmitClaimResult,
} from "../interfaces";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription,
containsHiddenDid,
createAndSubmitConfirmation,
createAndSubmitGive,
getHeaders,
} from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
NOTIFY_ERROR_RETRIEVING_CLAIMS,
NOTIFY_SENDING_STATUS,
NOTIFY_CONFIRMATION_SEND_ERROR,
NOTIFY_ALL_CONFIRMATIONS_ERROR,
NOTIFY_GIVE_SEND_ERROR,
NOTIFY_CLAIMS_SEND_ERROR,
createConfirmationSuccessMessage,
createCombinedSuccessMessage,
} from "@/constants/notifications";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@Component({
methods: { claimSpecialDescription },
components: {
QuickNav,
TopMessage,
},
mixins: [PlatformServiceMixin],
})
export default class QuickActionBvcEndView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
// Notification helper
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
claimCountByUser = 0;
claimCountWithHidden = 0;
claimsToConfirm: GenericCredWrapper<GenericVerifiableCredential>[] = [];
claimsToConfirmSelected: string[] = [];
description = "breakfast";
loadingConfirms = true;
someoneGave = false;
supplyGiftDetails = false;
// Method used in template
claimSpecialDescription = claimSpecialDescription;
// Computed properties for template optimization
get hasSelectedClaims() {
return this.claimsToConfirmSelected.length > 0;
}
get canSubmit() {
return this.hasSelectedClaims || (this.someoneGave && this.description);
}
get claimCountWithHiddenText() {
if (this.claimCountWithHidden === 0) return "";
return this.claimCountWithHidden === 1
? "There is 1 other claim with hidden details,"
: `There are ${this.claimCountWithHidden} other claims with hidden details,`;
}
get claimCountByUserText() {
if (this.claimCountByUser === 0) return "";
return this.claimCountByUser === 1
? "There is 1 other claim by you"
: `There are ${this.claimCountByUser} other claims by you`;
}
// Template streamlining: Extract long class strings to computed properties
get backButtonClasses() {
return "text-lg text-center px-2 py-1 absolute -left-2 -top-1";
}
get submitButtonClasses() {
return "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";
}
get disabledButtonClasses() {
return "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";
}
async created() {
this.loadingConfirms = true;
// Initialize notification helper
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$settings();
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.allContacts = await this.$contacts();
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,
}) || "";
this.allMyDids = (await this.$getAllAccounts()).map(
(account) => account.did,
);
const headers = await getHeaders(this.activeDid);
try {
const response = await fetch(
this.apiServer +
"/api/claim/?" +
"issuedAt_greaterThanOrEqualTo=" +
encodeURIComponent(todayOrPreviousStartDate) +
"&excludeConfirmations=true",
{ headers },
);
if (!response.ok) {
logger.error("Bad response", response);
throw new Error("Bad response when retrieving claims.");
}
await response.json().then((data) => {
const dataByOthers = R.reject(
(claim: GenericCredWrapper<GenericVerifiableCredential>) =>
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) {
logger.error("Error:", error);
this.notify.error(NOTIFY_ERROR_RETRIEVING_CLAIMS.message, TIMEOUTS.LONG);
}
this.loadingConfirms = false;
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
(this.$router as Router).push(route);
}
async record() {
try {
if (this.claimsToConfirmSelected.length > 0) {
this.notify.toast(
NOTIFY_SENDING_STATUS.title,
NOTIFY_SENDING_STATUS.message,
TIMEOUTS.SHORT,
);
}
// in parallel, make a confirmation for each selected claim and send them all to the server
const confirmResults: PromiseSettledResult<CreateAndSubmitClaimResult>[] =
await Promise.allSettled(
this.claimsToConfirmSelected.map(async (jwtId) => {
const record = this.claimsToConfirm.find(
(claim) => claim.id === jwtId,
);
if (!record) {
return { success: false, error: "Record not found." };
}
return createAndSubmitConfirmation(
this.activeDid,
record.claim as GenericVerifiableCredential,
record.id,
record.handleId,
this.apiServer,
axios,
);
}),
);
// check for any rejected confirmations
const confirmsSucceeded = confirmResults.filter(
// 'fulfilled' is the status in a successful PromiseFulfilledResult
(result) => result.status === "fulfilled" && result.value.success,
);
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
logger.error("Error sending confirmations:", confirmResults);
const errorMessage =
confirmsSucceeded.length === 0
? NOTIFY_ALL_CONFIRMATIONS_ERROR.message
: NOTIFY_CONFIRMATION_SEND_ERROR.message;
this.notify.error(errorMessage, TIMEOUTS.LONG);
}
// now send the give for the description
let giveSucceeded = false;
if (this.someoneGave && !this.supplyGiftDetails) {
const giveResult = await createAndSubmitGive(
axios,
this.apiServer,
this.activeDid,
undefined,
this.activeDid,
this.description,
undefined,
undefined,
undefined,
undefined,
false,
undefined,
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
giveSucceeded = giveResult.success;
if (!giveSucceeded) {
logger.error("Error sending give:", giveResult);
const errorMessage =
(giveResult as CreateAndSubmitClaimResult)?.error ||
NOTIFY_GIVE_SEND_ERROR.message;
this.notify.error(errorMessage, TIMEOUTS.LONG);
}
}
if (this.someoneGave && this.supplyGiftDetails) {
// we'll give a success message for the confirmations and go to the gifted details page
if (confirmsSucceeded.length > 0) {
const actions = createConfirmationSuccessMessage(
confirmsSucceeded.length,
);
this.notify.success(actions, TIMEOUTS.STANDARD);
}
(this.$router as Router).push({
name: "gifted-details",
query: {
description: this.description,
destinationPathAfter: "/",
providerProjectId: BVC_MEETUPS_PROJECT_CLAIM_ID,
recipientDid: this.activeDid,
},
});
} else {
// just go ahead and print a message for all the activity
if (confirmsSucceeded.length > 0 || giveSucceeded) {
const actions = createCombinedSuccessMessage(
confirmsSucceeded.length,
giveSucceeded,
);
this.notify.success(actions, TIMEOUTS.STANDARD);
(this.$router as Router).push({ path: "/" });
} else {
// errors should have already shown
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
logger.error("Error sending claims.", error);
const errorMessage =
error.userMessage || NOTIFY_CLAIMS_SEND_ERROR.message;
this.notify.error(errorMessage, TIMEOUTS.LONG);
}
}
}
</script>