Browse Source

Merge pull request 'Shortcut page for BVC assertions & confirmations' (#103) from bvc-shortcut into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/103
button-visual-enhancement
trentlarson 9 months ago
parent
commit
a488a36bc0
  1. 14
      package-lock.json
  2. 3
      package.json
  3. 41
      project.task.yaml
  4. 4
      src/App.vue
  5. 19
      src/components/GiftedDialog.vue
  6. 27
      src/components/OfferDialog.vue
  7. 1
      src/db/tables/settings.ts
  8. 249
      src/libs/endorserServer.ts
  9. 30
      src/libs/util.ts
  10. 24
      src/router/index.ts
  11. 62
      src/views/AccountViewView.vue
  12. 5
      src/views/ClaimView.vue
  13. 2
      src/views/ContactAmountsView.vue
  14. 2
      src/views/ContactGiftingView.vue
  15. 3
      src/views/ContactQRScanShowView.vue
  16. 18
      src/views/ContactsView.vue
  17. 8
      src/views/DiscoverView.vue
  18. 21
      src/views/HomeView.vue
  19. 5
      src/views/ProjectViewView.vue
  20. 220
      src/views/QuickActionBvcBeginView.vue
  21. 368
      src/views/QuickActionBvcEndView.vue
  22. 52
      src/views/QuickActionBvcView.vue

14
package-lock.json

@ -17,6 +17,7 @@
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.4.2",
"@veramo/core": "^5.4.1", "@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1", "@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1", "@veramo/data-store": "^5.4.1",
@ -41,7 +42,7 @@
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"localstorage-slim": "^2.5.0", "localstorage-slim": "^2.5.0",
"luxon": "^3.4.3", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"moment": "^2.29.4", "moment": "^2.29.4",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
@ -9170,6 +9171,11 @@
"@types/geojson": "*" "@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": { "node_modules/@types/mime": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
@ -20251,9 +20257,9 @@
} }
}, },
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.4.3", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
"integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }

3
package.json

@ -17,6 +17,7 @@
"@pvermeer/dexie-encrypted-addon": "^3.0.0", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^21.0.0", "@tweenjs/tween.js": "^21.0.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/luxon": "^3.4.2",
"@veramo/core": "^5.4.1", "@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.4.1", "@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.4.1", "@veramo/data-store": "^5.4.1",
@ -41,7 +42,7 @@
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"localstorage-slim": "^2.5.0", "localstorage-slim": "^2.5.0",
"luxon": "^3.4.3", "luxon": "^3.4.4",
"merkletreejs": "^0.3.11", "merkletreejs": "^0.3.11",
"moment": "^2.29.4", "moment": "^2.29.4",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",

41
project.task.yaml

@ -2,43 +2,40 @@
tasks : tasks :
- .2 fix give dialog from "more contacts" off home page to allow giving to this user - .2 fix give dialog from "more contacts" off home page to allow giving to this user
- 01 in the feed, group by project or contact or topic or time/$ (via BC)
- .2 anchor hash into BTC
- .1 when gave to a project, say "gave to project"
- .2 fix bottom of project selection map, where the icons are hidden but a tap goes to the icon's page - .2 fix bottom of project selection map, where the icons are hidden but a tap goes to the icon's page
- 01 separate not-on-platform vs totally anonymous; terminology "unidentified"? - .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window)
- .2 don't show a warning on a totally new project when the authorized agent is set
- .2 anchor hash into BTC
- .2 list the "show more" contacts alphabetically
- 01 page for BVC - 32 image on give :
- Show a camera to take a picture
- Scale the image to a reasonable size
- Upload to a public readable place
- check the rate limits
- use CID (hash?)
- put the image URL in the claim
- Rates - images erased?
- image not associated with JWT ULID since that's assigned later
- 24 compelling UI for credential presentations - 24 compelling UI for credential presentations
- discover who in my network has activity on a project - discover who in my network has activity on a project
- 24 compelling UI for statistics (eg. World?) - 24 compelling UI for statistics (eg. World?)
- .5 stop from seeing an error on the first page when browser doesn't support service workers (which I've seen on iPhone; visible in Firefox private window) - 01 in the feed, group by project or contact or topic or time/$ (via BC)
- 01 separate not-on-platform vs totally anonymous; terminology "unidentified"?
- .2 add links between projects - .2 add links between projects
- 32 image on give :
- Show a camera to take a picture
- Scale the image to a reasonable size
- Upload to a public readable place
- check the rate limits
- use CID (hash?)
- put the image URL in the claim
- Rates - images erased?
- image not associated with JWT ULID since that's assigned later
- 24 make the contact browsing on the front page something that invites more action - 24 make the contact browsing on the front page something that invites more action
- .2 list the "show more" contacts alphabetically
- .5 change server plan & project endpoints to use jwtId as identifier rather than rowid - .5 change server plan & project endpoints to use jwtId as identifier rather than rowid
- 16 edit offers & gives, or revoke allowing re-creation - 16 edit offers & gives, or revoke allowing re-creation
- .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page. - .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page.
- .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.) - .1 Add help instructions for "Encryption key has changed" error. (It is a problem if localStorage is cleared, but the contacts & settings remain and they have to restore their seeds.)
- .5 add more detail on TimeSafari.org
- .1 show better error when user with no ID goes to the "My Project" page - .1 show better error when user with no ID goes to the "My Project" page
- 08 add button to front page to prompt for ideas for gratitude : - 01 in front page prompt for ideas for gratitude :
- show previous on "Your" screen - randomize (not show in order)
- checkboxes - randomize vs show in order, show non-person-oriented messages, show only contacts, show only projects - checkboxes - show non-person-oriented messages, show only contacts, show only projects
- 08 allow user to add a time when they want their daily notification - 08 allow user to add a time when they want their daily notification

4
src/App.vue

@ -582,7 +582,7 @@ export default class App extends Vue {
} }
}) })
.catch((error) => { .catch((error) => {
console.log("Push provider server communication failed:", error); console.error("Push provider server communication failed:", error);
return false; return false;
}); });
@ -597,7 +597,7 @@ export default class App extends Vue {
return response.ok; return response.ok;
}) })
.catch((error) => { .catch((error) => {
console.log("Push server communication failed:", error); console.error("Push server communication failed:", error);
return false; return false;
}); });

19
src/components/GiftedDialog.vue

@ -77,7 +77,6 @@ import {
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
@Component @Component
@ -206,22 +205,6 @@ export default class GiftedDialog extends Vue {
}); });
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records for DID ${activeDid} but no identifier was found",
);
}
return identity;
}
/** /**
* *
* @param giverDid may be null * @param giverDid may be null
@ -262,7 +245,7 @@ export default class GiftedDialog extends Vue {
} }
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,

27
src/components/OfferDialog.vue

@ -72,9 +72,8 @@ import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { createAndSubmitOffer } from "@/libs/endorserServer"; import { createAndSubmitOffer } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import { accountsDB, db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
@Component @Component
export default class OfferDialog extends Vue { export default class OfferDialog extends Vue {
@ -102,7 +101,7 @@ export default class OfferDialog extends Vue {
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings from database:", err); console.error("Error retrieving settings from database:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -173,22 +172,6 @@ export default class OfferDialog extends Vue {
}); });
} }
public async getIdentity(activeDid: string) {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
);
}
return identity;
}
/** /**
* *
* @param description may be an empty string * @param description may be an empty string
@ -228,7 +211,7 @@ export default class OfferDialog extends Vue {
} }
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await libsUtil.getIdentity(this.activeDid);
const result = await createAndSubmitOffer( const result = await createAndSubmitOffer(
this.axios, this.axios,
this.apiServer, this.apiServer,
@ -245,7 +228,7 @@ export default class OfferDialog extends Vue {
this.isOfferCreationError(result.response) this.isOfferCreationError(result.response)
) { ) {
const errorMessage = this.getOfferCreationErrorMessage(result); const errorMessage = this.getOfferCreationErrorMessage(result);
console.log("Error with offer creation result:", result); console.error("Error with offer creation result:", result);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -268,7 +251,7 @@ export default class OfferDialog extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) { } catch (error: any) {
console.log("Error with offer recordation caught:", error); console.error("Error with offer recordation caught:", error);
const message = const message =
error.userMessage || error.userMessage ||
error.response?.data?.error?.message || error.response?.data?.error?.message ||

1
src/db/tables/settings.ts

@ -31,6 +31,7 @@ export type Settings = {
}>; }>;
showContactGivesInline?: boolean; // Display contact inline or not 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 vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
warnIfProdServer?: boolean; // Warn if using a production server warnIfProdServer?: boolean; // Warn if using a production server
warnIfTestServer?: boolean; // Warn if using a testing server warnIfTestServer?: boolean; // Warn if using a testing server

249
src/libs/endorserServer.ts

@ -22,7 +22,7 @@ export interface AgreeVerifiableCredential {
"@type": string; "@type": string;
// "any" because arbitrary objects can be subject of agreement // "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<any, any>; object: Record<string, any>;
} }
export interface GiverInputInfo { export interface GiverInputInfo {
@ -46,21 +46,25 @@ export interface ClaimResult {
export interface GenericVerifiableCredential { export interface GenericVerifiableCredential {
"@context": string; "@context": string;
"@type": string; "@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }
export interface GenericServerRecord extends GenericVerifiableCredential { export interface GenericServerRecord extends GenericVerifiableCredential {
handleId?: string; handleId?: string;
id?: string; id: string;
issuedAt?: string; issuedAt: string;
issuer?: string; issuer: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim: Record<any, any>; claim: Record<string, any>;
claimType?: string; claimType?: string;
} }
export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = { export const BLANK_GENERIC_SERVER_RECORD: GenericServerRecord = {
"@context": SCHEMA_ORG_CONTEXT, "@context": SCHEMA_ORG_CONTEXT,
"@type": "", "@type": "",
claim: {}, claim: {},
id: "",
issuedAt: "",
issuer: "",
}; };
export interface GiveServerRecord { export interface GiveServerRecord {
@ -226,16 +230,16 @@ export interface ErrorResponse {
}; };
} }
export interface ErrorResult {
type: "error";
error: InternalError;
}
export interface InternalError { export interface InternalError {
error: string; // for system logging error: string; // for system logging
userMessage?: string; // for user display userMessage?: string; // for user display
} }
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult; export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
// This is used to check for hidden info. // This is used to check for hidden info.
@ -327,7 +331,7 @@ export function addLastClaimOrHandleAsIdIfMissing(
} }
// return clone of object without any nested *VisibleToDids keys // return clone of object without any nested *VisibleToDids keys
// similar logic is found in endorser-mobile // similar code is also contained in endorser-mobile
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function removeVisibleToDids(input: any): any { export function removeVisibleToDids(input: any): any {
if (input instanceof Object) { if (input instanceof Object) {
@ -337,7 +341,6 @@ export function removeVisibleToDids(input: any): any {
const result: Record<string, any> = {}; const result: Record<string, any> = {};
for (const key in input) { for (const key in input) {
if (!key.endsWith("VisibleToDids")) { if (!key.endsWith("VisibleToDids")) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result[key] = removeVisibleToDids(R.clone(input[key])); result[key] = removeVisibleToDids(R.clone(input[key]));
} }
} }
@ -346,7 +349,6 @@ export function removeVisibleToDids(input: any): any {
// it's an array // it's an array
return R.map(removeVisibleToDids, input); return R.map(removeVisibleToDids, input);
} }
return false;
} else { } else {
return input; return input;
} }
@ -515,6 +517,28 @@ export async function createAndSubmitOffer(
); );
} }
// similar logic is found in endorser-mobile
export const createAndSubmitConfirmation = async (
identifier: IIdentifier,
claim: GenericVerifiableCredential,
lastClaimId: string, // used to set the lastClaimId
handleId: string | undefined,
apiServer: string,
axios: Axios,
) => {
const goodClaim = removeSchemaContext(
removeVisibleToDids(
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
),
);
const confirmationClaim: GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
return createAndSubmitClaim(confirmationClaim, identifier, apiServer, axios);
};
export async function createAndSubmitClaim( export async function createAndSubmitClaim(
vcClaim: GenericVerifiableCredential, vcClaim: GenericVerifiableCredential,
identity: IIdentifier, identity: IIdentifier,
@ -579,12 +603,199 @@ export async function createAndSubmitClaim(
} }
} }
// from https://stackoverflow.com/a/175787/845494 // eslint-disable-next-line @typescript-eslint/no-explicit-any
// export const isAccept = (claim: Record<string, any>) => {
export function isNumeric(str: string): boolean { return (
return !isNaN(+str); claim &&
claim["@context"] === SCHEMA_ORG_CONTEXT &&
claim["@type"] === "AcceptAction"
);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isOffer = (claim: Record<string, any>) => {
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 numberOrZero(str: string): number { export function displayAmount(code: string, amt: number) {
return isNumeric(str) ? +str : 0; 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<string, any>) => {
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<string, any>;
}
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<string>,
contacts: Array<Contact>,
) => {
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);
}
};
export const BVC_MEETUPS_PROJECT_CLAIM_ID =
//"https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H";
"https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK";
export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
return {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "JoinAction",
agent: {
identifier: did,
},
event: {
organizer: {
name: "Bountiful Voluntaryist Community",
},
name: "Saturday Morning Meeting",
startTime: startTime,
},
};
};

30
src/libs/util.ts

@ -1,14 +1,16 @@
// many of these are also found in endorser-mobile utility.ts // many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { IIdentifier } from "@veramo/core";
import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer"; import { GenericServerRecord, containsHiddenDid } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { useClipboard } from "@vueuse/core";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@ -55,6 +57,16 @@ export function iconForUnitCode(unitCode: string) {
return UNIT_CODES[unitCode]?.faIcon || "question"; return UNIT_CODES[unitCode]?.faIcon || "question";
} }
// from https://stackoverflow.com/a/175787/845494
//
export function isNumeric(str: string): boolean {
return !isNaN(+str);
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}
export const isGlobalUri = (uri: string) => { export const isGlobalUri = (uri: string) => {
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
}; };
@ -180,6 +192,22 @@ export function findAllVisibleToDids(
* *
**/ **/
export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
await accountsDB.open();
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`,
);
}
return identity;
};
/** /**
* Generates a new identity, saves it to the database, and sets it as the active identity. * Generates a new identity, saves it to the database, and sets it as the active identity.
* @return {Promise<string>} with the DID of the new identity * @return {Promise<string>} with the DID of the new identity

24
src/router/index.ts

@ -165,6 +165,30 @@ const routes: Array<RouteRecordRaw> = [
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart, 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", path: "/scan-contact",
name: "scan-contact", name: "scan-contact",

62
src/views/AccountViewView.vue

@ -299,7 +299,7 @@
<label <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center justify-between cursor-pointer my-4" class="flex items-center justify-between cursor-pointer my-4"
@click="handleChange" @click="toggleShowContactAmounts"
> >
<!-- label --> <!-- label -->
<span class="text-slate-500 text-sm font-bold">Contacts Display</span> <span class="text-slate-500 text-sm font-bold">Contacts Display</span>
@ -439,6 +439,28 @@
{{ DEFAULT_PUSH_SERVER }} {{ DEFAULT_PUSH_SERVER }}
</span> </span>
<label
for="toggleShowShortcutBvc"
class="flex items-center justify-between cursor-pointer my-4"
@click="toggleShowShortcutBvc"
>
<!-- label -->
<span class="text-slate-500 text-sm font-bold"
>Show BVC Shortcut on Home Page</span
>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="showShortcutBvc" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div>
</div>
</label>
<div class="mt-4"> <div class="mt-4">
<h2 class="text-slate-500 text-sm font-bold"> <h2 class="text-slate-500 text-sm font-bold">
Contacts & Settings Database Contacts & Settings Database
@ -537,6 +559,7 @@ export default class AccountViewView extends Vue {
showB64Copy = false; showB64Copy = false;
showPubCopy = false; showPubCopy = false;
showAdvanced = false; showAdvanced = false;
showShortcutBvc = false;
subscription: PushSubscription | null = null; subscription: PushSubscription | null = null;
warnIfProdServer = false; warnIfProdServer = false;
warnIfTestServer = false; warnIfTestServer = false;
@ -596,6 +619,7 @@ export default class AccountViewView extends Vue {
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3 (settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
this.showShortcutBvc = !!settings?.showShortcutBvc;
this.warnIfProdServer = !!settings?.warnIfProdServer; this.warnIfProdServer = !!settings?.warnIfProdServer;
this.warnIfTestServer = !!settings?.warnIfTestServer; this.warnIfTestServer = !!settings?.warnIfTestServer;
this.webPushServer = (settings?.webPushServer as string) || ""; this.webPushServer = (settings?.webPushServer as string) || "";
@ -653,7 +677,7 @@ export default class AccountViewView extends Vue {
.then(() => setTimeout(fn, 2000)); .then(() => setTimeout(fn, 2000));
} }
handleChange() { toggleShowContactAmounts() {
this.showContactGives = !this.showContactGives; this.showContactGives = !this.showContactGives;
this.updateShowContactAmounts(); this.updateShowContactAmounts();
} }
@ -668,6 +692,11 @@ export default class AccountViewView extends Vue {
this.updateWarnIfTestServer(this.warnIfTestServer); this.updateWarnIfTestServer(this.warnIfTestServer);
} }
toggleShowShortcutBvc() {
this.showShortcutBvc = !this.showShortcutBvc;
this.updateShowShortcutBvc(this.showShortcutBvc);
}
readableTime(timeStr: string) { readableTime(timeStr: string) {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
@ -763,7 +792,7 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after contact setting update because:", "Telling user to try again after contact-amounts setting update because:",
err, err,
); );
} }
@ -786,7 +815,7 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after setting update because:", "Telling user to try again after prod-server-warning setting update because:",
err, err,
); );
} }
@ -809,7 +838,30 @@ export default class AccountViewView extends Vue {
-1, -1,
); );
console.error( console.error(
"Telling user to try again after setting update because:", "Telling user to try again after test-server-warning setting update because:",
err,
);
}
}
public async updateShowShortcutBvc(newSetting: boolean) {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: newSetting,
});
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating BVC Shortcut Setting",
text: "The setting may not have saved. Try again, maybe after restarting the app.",
},
-1,
);
console.error(
"Telling user to try again after BVC-shortcut setting update because:",
err, err,
); );
} }

5
src/views/ClaimView.vue

@ -730,10 +730,7 @@ export default class ClaimView extends Vue {
), ),
), ),
); );
const confirmationClaim: serverUtil.GenericVerifiableCredential & { const confirmationClaim: serverUtil.GenericVerifiableCredential = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any;
} = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,

2
src/views/ContactAmountsView.vue

@ -179,7 +179,7 @@ export default class ContactAmountssView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings or gives.", err); console.error("Error retrieving settings or gives.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

2
src/views/ContactGiftingView.vue

@ -119,7 +119,7 @@ export default class ContactGiftingView extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings & contacts:", err); console.error("Error retrieving settings & contacts:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

3
src/views/ContactQRScanShowView.vue

@ -180,7 +180,6 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) { onScanDetect(content: any) {
if (content[0]?.rawValue) { if (content[0]?.rawValue) {
//console.log("onDetect", content[0].rawValue);
localStorage.setItem("contactEndorserUrl", content[0].rawValue); localStorage.setItem("contactEndorserUrl", content[0].rawValue);
this.$router.push({ name: "contacts" }); this.$router.push({ name: "contacts" });
} else { } else {
@ -198,7 +197,7 @@ export default class ContactQRScanShow extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanError(error: any) { onScanError(error: any) {
console.log("Scan was invalid:", error); console.error("Scan was invalid:", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

18
src/views/ContactsView.vue

@ -284,6 +284,7 @@
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { IndexableType } from "dexie";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
@ -311,7 +312,6 @@ import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { IndexableType } from "dexie";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@ -497,7 +497,7 @@ export default class ContactsView extends Vue {
this.givenToMeConfirmed = givenToMeConfirmed; this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = givenToMeUnconfirmed; this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} catch (error) { } catch (error) {
console.log("Error loading gives", error); console.error("Error loading gives", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -1000,7 +1000,7 @@ export default class ContactsView extends Vue {
-1, -1,
); );
} else { } else {
console.log("Got bad server response when checking visibility: ", resp); console.error("Got bad server response checking visibility:", resp);
const message = resp.data.error?.message || "Got bad server response."; const message = resp.data.error?.message || "Got bad server response.";
this.$notify( this.$notify(
{ {
@ -1013,7 +1013,7 @@ export default class ContactsView extends Vue {
); );
} }
} catch (err) { } catch (err) {
console.log("Caught error from request to check visibility:", err); console.error("Caught error from request to check visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -1026,12 +1026,6 @@ export default class ContactsView extends Vue {
} }
} }
// from https://stackoverflow.com/a/175787/845494
//
private isNumeric(str: string): boolean {
return !isNaN(+str);
}
private nameForDid(contacts: Array<Contact>, did: string): string { private nameForDid(contacts: Array<Contact>, did: string): string {
const contact = R.find((con) => con.did == did, contacts); const contact = R.find((con) => con.did == did, contacts);
return this.nameForContact(contact); return this.nameForContact(contact);
@ -1067,7 +1061,7 @@ export default class ContactsView extends Vue {
return; return;
} }
} }
if (!this.isNumeric(this.hourInput)) { if (!libsUtil.isNumeric(this.hourInput)) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -1204,7 +1198,7 @@ export default class ContactsView extends Vue {
} }
} }
} catch (error) { } catch (error) {
console.log("Error in createAndSubmitContactGive: ", error); console.error("Error in createAndSubmitContactGive: ", error);
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError) { if (serverError) {

8
src/views/DiscoverView.vue

@ -254,7 +254,7 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.log("Problem with full search:", details); console.error("Problem with full search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -282,7 +282,7 @@ export default class DiscoverView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.log("Error with feed load:", e); console.error("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -337,7 +337,7 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.log("Problem with nearby search:", details); console.error("Problem with nearby search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -374,7 +374,7 @@ export default class DiscoverView extends Vue {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
console.log("Error with feed load:", e); console.error("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

21
src/views/HomeView.vue

@ -3,8 +3,8 @@
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-2 pb-24 max-w-3xl mx-auto">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-8">
Time Safari Time Safari
</h1> </h1>
@ -59,6 +59,15 @@
</div> </div>
</div> </div>
<div v-if="showShortcutBvc" class="mb-4">
<router-link
:to="{ name: 'quick-action-bvc' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
Bountiful Voluntaryist Community Actions</router-link
>
</div>
<!-- show the actions for recognizing a give --> <!-- show the actions for recognizing a give -->
<div class="mb-8"> <div class="mb-8">
<div v-if="isCreatingIdentifier"> <div v-if="isCreatingIdentifier">
@ -287,6 +296,7 @@ export default class HomeView extends Vue {
isCreatingIdentifier = false; isCreatingIdentifier = false;
isFeedLoading = true; isFeedLoading = true;
isRegistered = false; isRegistered = false;
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
@ -321,6 +331,7 @@ export default class HomeView extends Vue {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedClaimId = settings?.lastViewedClaimId; this.feedLastViewedClaimId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
this.showShortcutBvc = !!settings?.showShortcutBvc;
if (this.allMyDids.length === 0) { if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true; this.isCreatingIdentifier = true;
@ -335,7 +346,7 @@ export default class HomeView extends Vue {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.log("Error retrieving settings or feed.", err); console.error("Error retrieving settings or feed.", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -441,7 +452,7 @@ export default class HomeView extends Vue {
} }
}) })
.catch((e) => { .catch((e) => {
console.log("Error with feed load:", e); console.error("Error with feed load:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -473,7 +484,7 @@ export default class HomeView extends Vue {
}, },
); );
if (response.status !== 200) { if (!response.ok) {
throw await response.text(); throw await response.text();
} }

5
src/views/ProjectViewView.vue

@ -800,10 +800,7 @@ export default class ProjectViewView extends Vue {
), ),
), ),
); );
const confirmationClaim: serverUtil.GenericVerifiableCredential & { const confirmationClaim: serverUtil.GenericVerifiableCredential = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any;
} = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "AgreeAction", "@type": "AgreeAction",
object: goodClaim, object: goodClaim,

220
src/views/QuickActionBvcBeginView.vue

@ -0,0 +1,220 @@
<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="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
Beginning of BVC Saturday Meeting
</h1>
<div>
<h2 class="text-2xl m-2">You're Here</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="attended" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Attended</span>
</div>
<div class="m-2 flex">
<input type="checkbox" v-model="gaveTime" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Spent Time</span>
<span v-if="gaveTime">
<input
type="text"
placeholder="How much time"
v-model="hoursStr"
size="1"
class="border border-slate-400 h-6 px-2"
/>
hour(s)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6" />
</div>
</div>
<div
v-if="attended || (gaveTime && hoursStr && hoursStr != '0')"
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center text-md font-bold bg-blue-500 text-white px-2 py-3 rounded-md w-56"
>
Sign & Send
</button>
</div>
<div v-else class="flex justify-center mt-4">
<button
class="block text-center text-md font-bold bg-slate-500 text-white px-2 py-3 rounded-md w-56"
>
Select Your Actions
</button>
</div>
</section>
</template>
<script lang="ts">
import axios from "axios";
import { DateTime } from "luxon";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { db } from "@/db/index";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
bvcMeetingJoinClaim,
createAndSubmitClaim,
createAndSubmitGive,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
attended = true;
gaveTime = true;
hoursStr = "1";
todayOrPreviousStartDate = "";
async mounted() {
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!
this.todayOrPreviousStartDate =
eventStartDateObj.toISO({
suppressMilliseconds: true,
}) || "";
}
async record() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
const activeDid = settings?.activeDid || "";
const apiServer = settings?.apiServer || "";
try {
const hoursNum = libsUtil.numberOrZero(this.hoursStr);
const identity = await libsUtil.getIdentity(activeDid);
// first send the claim for time given
let timeSuccess = false;
if (this.gaveTime && hoursNum > 0) {
const timeResult = await createAndSubmitGive(
axios,
apiServer,
identity,
activeDid,
undefined,
undefined,
hoursNum,
"HUR",
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
if (timeResult.type === "success") {
timeSuccess = true;
} else {
console.error("Error sending give:", timeResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
timeResult?.error?.userMessage ||
"There was an error sending the time.",
},
-1,
);
}
}
// now send the claim for attendance
let attendedSuccess = false;
if (this.attended) {
const attendResult = await createAndSubmitClaim(
bvcMeetingJoinClaim(activeDid, this.todayOrPreviousStartDate),
identity,
apiServer,
axios,
);
if (attendResult.type === "success") {
attendedSuccess = true;
} else {
console.error("Error sending give:", attendResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
attendResult?.error?.userMessage ||
"There was an error sending the attendance.",
},
-1,
);
}
}
if (timeSuccess || attendedSuccess) {
const actions =
timeSuccess && attendedSuccess
? "attendance and time have been"
: timeSuccess
? "time has been"
: "attendance has been";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: `Your ${actions} recorded.`,
},
-1,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error sending claims.", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: error.userMessage || "There was an error sending claims.",
},
-1,
);
}
}
}
</script>

368
src/views/QuickActionBvcEndView.vue

@ -0,0 +1,368 @@
<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="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</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">
<fa icon="spinner" class="animate-spin" />
</div>
<div v-else-if="claimsToConfirm.length === 0">
There are no claims yet today for you to confirm.
<span v-if="claimCountWithHidden > 0">
{{
claimCountWithHidden === 1
? "(There is 1 claim with hidden details.)"
: `(There are ${claimCountWithHidden} claims with hidden details.)`
}}
</span>
</div>
<ul class="border-t border-slate-300 m-2">
<li
class="border-b border-slate-300 py-2"
v-for="record in claimsToConfirm"
:key="record.id"
>
<div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start">
<span>
<input
type="checkbox"
:checked="claimsToConfirmSelected.includes(record.id)"
@click="
claimsToConfirmSelected.includes(record.id)
? claimsToConfirmSelected.splice(
claimsToConfirmSelected.indexOf(record.id),
1,
)
: claimsToConfirmSelected.push(record.id)
"
class="mr-2 h-6 w-6"
/>
</span>
{{
claimSpecialDescription(
record,
activeDid,
allMyDids,
allContacts,
)
}}
<a @click="onClickLoadClaim(record.id)">
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
/>
</a>
</span>
</div>
</li>
</ul>
</div>
<div>
<h2 class="text-2xl m-2">Anything else?</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Someone else gave</span>
<span v-if="someoneGave">
<input
type="text"
v-model="description"
size="20"
class="border border-slate-400 h-6 px-2"
/>
</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="claimsToConfirmSelected.length || (someoneGave && description)"
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center text-md font-bold bg-blue-500 text-white px-2 py-3 rounded-md w-56"
>
Sign & Send
</button>
</div>
<div v-else class="flex justify-center mt-4">
<button
class="block text-center text-md font-bold bg-slate-500 text-white px-2 py-3 rounded-md w-56"
>
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 { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription,
containsHiddenDid,
createAndSubmitConfirmation,
createAndSubmitGive,
ErrorResult,
GenericServerRecord,
GenericVerifiableCredential,
} from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util";
@Component({
methods: { claimSpecialDescription },
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: Array<string> = [];
apiServer = "";
claimCountWithHidden = 0;
claimsToConfirm: GenericServerRecord[] = [];
claimsToConfirmSelected: string[] = [];
description = "breakfast";
loadingConfirms = true;
someoneGave = false;
async created() {
await db.open();
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
}
async mounted() {
this.loadingConfirms = true;
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,
}) || "";
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
const account: Account | undefined = await accountsDB.accounts
.where("did")
.equals(this.activeDid)
.first();
const identity: IIdentifier = JSON.parse(
(account?.identity as string) || "null",
);
const headers = {
Authorization: "Bearer " + (await accessToken(identity)),
};
try {
const response = await fetch(
this.apiServer +
"/api/claim/?" +
"issuedAt_greaterThanOrEqualTo=" +
encodeURIComponent(todayOrPreviousStartDate) +
"&excludeConfirmations=true",
{ headers },
);
if (!response.ok) {
console.log("Bad response", response);
throw new Error("Bad response when retrieving claims.");
}
await response.json().then((data) => {
const dataByOthers = R.reject(
(claim: GenericServerRecord) => claim.issuer === this.activeDid,
data,
);
const dataByOthersWithoutHidden = R.reject(
containsHiddenDid,
dataByOthers,
);
this.claimsToConfirm = dataByOthersWithoutHidden;
this.claimCountWithHidden =
dataByOthers.length - dataByOthersWithoutHidden.length;
});
} catch (error) {
console.error("Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error retrieving today's claims to confirm.",
},
-1,
);
}
this.loadingConfirms = false;
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
}
async record() {
try {
const identity = await libsUtil.getIdentity(this.activeDid);
// in parallel, make a confirmation for each selected claim and send them all to the server
const confirmResults = await Promise.allSettled(
this.claimsToConfirmSelected.map(async (jwtId) => {
const record = this.claimsToConfirm.find(
(claim) => claim.id === jwtId,
);
if (!record) {
return { type: "error", error: "Record not found." };
}
const identity = await libsUtil.getIdentity(this.activeDid);
return createAndSubmitConfirmation(
identity,
record.claim as GenericVerifiableCredential,
record.id,
record.handleId,
this.apiServer,
axios,
);
}),
);
// check for any rejected confirmations
const confirmsSucceeded = confirmResults.filter(
(result) =>
result.status === "fulfilled" && result.value.type === "success",
);
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
console.error("Error sending confirmations:", confirmResults);
const howMany = confirmsSucceeded.length === 0 ? "all" : "some";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: `There was an error sending ${howMany} of the confirmations.`,
},
-1,
);
}
// now send the give for the description
let giveSucceeded = false;
if (this.someoneGave) {
const giveResult = await createAndSubmitGive(
axios,
this.apiServer,
identity,
undefined,
this.activeDid,
this.description,
undefined,
undefined,
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
giveSucceeded = giveResult.type === "success";
if (!giveSucceeded) {
console.error("Error sending give:", giveResult);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
(giveResult as ErrorResult)?.error?.userMessage ||
"There was an error sending the give.",
},
-1,
);
}
}
if (confirmsSucceeded.length > 0 || giveSucceeded) {
const confirms =
confirmsSucceeded.length === 1 ? "confirmation" : "confirmations";
const actions =
confirmsSucceeded.length > 0 && giveSucceeded
? `${confirms} and give have been`
: giveSucceeded
? "give has been"
: confirms +
" " +
(confirmsSucceeded.length === 1 ? "has" : "have") +
" been";
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: `Your ${actions} recorded.`,
},
-1,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error("Error sending claims.", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: error.userMessage || "There was an error sending claims.",
},
-1,
);
}
}
}
</script>

52
src/views/QuickActionBvcView.vue

@ -0,0 +1,52 @@
<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="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light px-4 mb-4">
Bountiful Voluntaryist Community Actions
</h1>
<div>
<router-link
:to="{ name: 'quick-action-bvc-begin' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
Beginning of Meeting
</router-link>
<router-link
:to="{ name: 'quick-action-bvc-end' }"
class="block text-center text-md font-bold uppercase bg-blue-500 text-white mt-2 px-2 py-3 rounded-md"
>
End of Meeting
</router-link>
</div>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue";
import TopMessage from "@/components/TopMessage.vue";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcView extends Vue {}
</script>
Loading…
Cancel
Save