diff --git a/package-lock.json b/package-lock.json index 2ab5803db..fba206706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@pvermeer/dexie-encrypted-addon": "^3.0.0", "@tweenjs/tween.js": "^21.0.0", "@types/js-yaml": "^4.0.9", + "@types/luxon": "^3.4.2", "@veramo/core": "^5.4.1", "@veramo/credential-w3c": "^5.4.1", "@veramo/data-store": "^5.4.1", @@ -41,7 +42,7 @@ "js-generate-password": "^0.1.9", "js-yaml": "^4.1.0", "localstorage-slim": "^2.5.0", - "luxon": "^3.4.3", + "luxon": "^3.4.4", "merkletreejs": "^0.3.11", "moment": "^2.29.4", "notiwind": "^2.0.2", @@ -9170,6 +9171,11 @@ "@types/geojson": "*" } }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" + }, "node_modules/@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", @@ -20251,9 +20257,9 @@ } }, "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } diff --git a/package.json b/package.json index cfab56ec9..ee91c0687 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@pvermeer/dexie-encrypted-addon": "^3.0.0", "@tweenjs/tween.js": "^21.0.0", "@types/js-yaml": "^4.0.9", + "@types/luxon": "^3.4.2", "@veramo/core": "^5.4.1", "@veramo/credential-w3c": "^5.4.1", "@veramo/data-store": "^5.4.1", @@ -41,7 +42,7 @@ "js-generate-password": "^0.1.9", "js-yaml": "^4.1.0", "localstorage-slim": "^2.5.0", - "luxon": "^3.4.3", + "luxon": "^3.4.4", "merkletreejs": "^0.3.11", "moment": "^2.29.4", "notiwind": "^2.0.2", diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts index 4b3ad1e5d..f95b38399 100644 --- a/src/db/tables/settings.ts +++ b/src/db/tables/settings.ts @@ -31,6 +31,7 @@ export type Settings = { }>; showContactGivesInline?: boolean; // Display contact inline or not + showShortcutBvc?: boolean; // Show shortcut for BVC actions vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push warnIfProdServer?: boolean; // Warn if using a production server warnIfTestServer?: boolean; // Warn if using a testing server diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 55756e6c8..be4865de7 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -50,9 +50,9 @@ export interface GenericVerifiableCredential { export interface GenericServerRecord extends GenericVerifiableCredential { handleId?: string; - id?: string; - issuedAt?: string; - issuer?: string; + id: string; + issuedAt: string; + issuer: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any claim: Record; claimType?: string; @@ -61,6 +61,9 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = { "@context": SCHEMA_ORG_CONTEXT, "@type": "", claim: {}, + id: "", + issuedAt: "", + issuer: "", }; export interface GiveServerRecord { @@ -579,6 +582,182 @@ export async function createAndSubmitClaim( } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isAccept = (claim: Record) => { + return ( + claim && + claim["@context"] === SCHEMA_ORG_CONTEXT && + claim["@type"] === "AcceptAction" + ); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isOffer = (claim: Record) => { + return ( + claim && + claim["@context"] === SCHEMA_ORG_CONTEXT && + claim["@type"] === "Offer" + ); +}; + +export function currencyShortWordForCode(unitCode: string, single: boolean) { + return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; +} + +export function displayAmount(code: string, amt: number) { + return "" + amt + " " + currencyShortWordForCode(code, amt === 1); +} + +// insert a space before any capital letters except the initial letter +// (and capitalize initial letter, just in case) +export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => { + return !text + ? "" + : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); +}; + +/** + return readable summary of claim, or something generic + + similar code is also contained in endorser-mobile + **/ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const claimSummary = (claim: Record) => { + if (!claim) { + // to differentiate from "something" above + return "something"; + } + if (claim.claim) { + // probably a Verified Credential + // eslint-disable-next-line @typescript-eslint/no-explicit-any + claim = claim.claim as Record; + } + if (Array.isArray(claim)) { + if (claim.length === 1) { + claim = claim[0]; + } else { + return "multiple claims"; + } + } + const type = claim["@type"]; + if (!type) { + return "a claim"; + } else { + let typeExpl = capitalizeAndInsertSpacesBeforeCaps(type); + if (typeExpl === "Person") { + typeExpl += " claim"; + } + return "a " + typeExpl; + } +}; + +/** + return readable description of claim if possible, as a past-tense action + + identifiers is a list of objects with a 'did' field, each representing the user + contacts is a list of objects with a 'did' field for others and a 'name' field for their name + + similar code is also contained in endorser-mobile + **/ +export const claimSpecialDescription = ( + record: GenericServerRecord, + activeDid: string, + identifiers: Array, + contacts: Array, +) => { + let claim = record.claim; + if (claim.claim) { + // it's probably a Verified Credential + claim = claim.claim; + } + + const issuer = didInfo(record.issuer, activeDid, identifiers, contacts); + const type = claim["@type"] || "UnknownType"; + + if (type === "AgreeAction") { + return issuer + " agreed with " + claimSummary(claim.object); + } else if (isAccept(claim)) { + return issuer + " accepted " + claimSummary(claim.object); + } else if (type === "GiveAction") { + // agent.did is for legacy data, before March 2023 + const giver = claim.agent?.identifier || claim.agent?.did; + const giverInfo = didInfo(giver, activeDid, identifiers, contacts); + let gaveAmount = claim.object?.amountOfThisGood + ? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) + : ""; + if (claim.description) { + if (gaveAmount) { + gaveAmount = gaveAmount + ", and also: "; + } + gaveAmount = gaveAmount + claim.description; + } + if (!gaveAmount) { + gaveAmount = "something not described"; + } + // recipient.did is for legacy data, before March 2023 + const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did; + const gaveRecipientInfo = gaveRecipientId + ? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts) + : ""; + return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount; + } else if (type === "JoinAction") { + // agent.did is for legacy data, before March 2023 + const agent = claim.agent?.identifier || claim.agent?.did; + const contactInfo = didInfo(agent, activeDid, identifiers, contacts); + + let eventOrganizer = + claim.event && claim.event.organizer && claim.event.organizer.name; + eventOrganizer = eventOrganizer || ""; + let eventName = claim.event && claim.event.name; + eventName = eventName ? " " + eventName : ""; + let fullEvent = eventOrganizer + eventName; + fullEvent = fullEvent ? " attended the " + fullEvent : ""; + + let eventDate = claim.event && claim.event.startTime; + eventDate = eventDate ? " at " + eventDate : ""; + return contactInfo + fullEvent + eventDate; + } else if (isOffer(claim)) { + const offerer = claim.offeredBy?.identifier; + const contactInfo = didInfo(offerer, activeDid, identifiers, contacts); + let offering = ""; + if (claim.includesObject) { + offering += + " " + + displayAmount( + claim.includesObject.unitCode, + claim.includesObject.amountOfThisGood, + ); + } + if (claim.itemOffered?.description) { + offering += ", saying: " + claim.itemOffered?.description; + } + // recipient.did is for legacy data, before March 2023 + const offerRecipientId = + claim.recipient?.identifier || claim.recipient?.did; + const offerRecipientInfo = offerRecipientId + ? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts) + : ""; + return contactInfo + " offered" + offering + offerRecipientInfo; + } else if (type === "PlanAction") { + const claimer = claim.agent?.identifier || record.issuer; + const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts); + return claimerInfo + " announced a project: " + claim.name; + } else if (type === "Tenure") { + // party.did is for legacy data, before March 2023 + const claimer = claim.party?.identifier || claim.party?.did; + const contactInfo = didInfo(claimer, activeDid, identifiers, contacts); + const polygon = claim.spatialUnit?.geo?.polygon || ""; + return ( + contactInfo + + " possesses [" + + polygon.substring(0, polygon.indexOf(" ")) + + "...]" + ); + } else { + return issuer + " declared " + claimSummary(claim as GenericServerRecord); + } +}; + // from https://stackoverflow.com/a/175787/845494 // export function isNumeric(str: string): boolean { diff --git a/src/router/index.ts b/src/router/index.ts index 68da028fc..b3c078df3 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -165,6 +165,30 @@ const routes: Array = [ import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), beforeEnter: enterOrStart, }, + { + path: "/quick-action-bvc", + name: "quick-action-bvc", + component: () => + import( + /* webpackChunkName: "quick-action-bvc" */ "../views/QuickActionBvcView.vue" + ), + }, + { + path: "/quick-action-bvc-begin", + name: "quick-action-bvc-begin", + component: () => + import( + /* webpackChunkName: "quick-action-bvc-begin" */ "../views/QuickActionBvcBeginView.vue" + ), + }, + { + path: "/quick-action-bvc-end", + name: "quick-action-bvc-end", + component: () => + import( + /* webpackChunkName: "quick-action-bvc-end" */ "../views/QuickActionBvcEndView.vue" + ), + }, { path: "/scan-contact", name: "scan-contact", diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 7f2a44730..7b72d53cb 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -299,7 +299,7 @@