Compare commits

..

2 Commits

Author SHA1 Message Date
Matthew Raymer
e25e5d5ff6 WIPgit add . Updates to package.json ... much breakage 2023-08-30 19:43:26 +08:00
Matthew Raymer
8452af7abc Removing old alerts 2023-08-30 19:17:29 +08:00
35 changed files with 13175 additions and 14751 deletions

View File

@@ -99,9 +99,6 @@ See https://tea.xyz
### Reference Material ### Reference Material
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
``` ```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83 // reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83

26032
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,59 +9,57 @@
}, },
"dependencies": { "dependencies": {
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.3", "@fortawesome/vue-fontawesome": "^3.0.3",
"@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",
"@veramo/core": "^5.2.0", "@veramo/core": "^5.4.1",
"@veramo/credential-w3c": "^5.2.0", "@veramo/credential-w3c": "^5.4.1",
"@veramo/data-store": "^5.2.0", "@veramo/data-store": "^5.4.1",
"@veramo/did-manager": "^5.1.2", "@veramo/did-manager": "^5.4.1",
"@veramo/did-provider-ethr": "^5.1.2", "@veramo/did-provider-ethr": "^5.4.1",
"@veramo/did-resolver": "^5.2.0", "@veramo/did-resolver": "^5.4.1",
"@veramo/key-manager": "^5.1.2", "@veramo/key-manager": "^5.4.1",
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.4.1",
"@zxing/text-encoding": "^0.9.0", "@zxing/text-encoding": "^0.9.0",
"axios": "^1.4.0", "axios": "^1.5.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"core-js": "^3.31.1", "core-js": "^3.32.1",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"dexie-export-import": "^4.0.7", "dexie-export-import": "^4.0.7",
"did-jwt": "^7.2.4", "did-jwt": "^7.2.6",
"ethereum-cryptography": "^2.0.0", "ethereum-cryptography": "^2.1.2",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0", "ethr-did-resolver": "^8.1.2",
"jdenticon": "^3.2.0", "jdenticon": "^3.2.0",
"js-generate-password": "^0.1.9", "js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0", "localstorage-slim": "^2.4.0",
"luxon": "^3.3.0", "luxon": "^3.4.2",
"merkletreejs": "^0.3.10", "merkletreejs": "^0.3.10",
"moment": "^2.29.4", "moment": "^2.29.4",
"notiwind": "^2.0.2", "notiwind": "^2.0.2",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.2.0",
"qr-code-generator-vue3": "^1.4.21", "qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.29.0", "ramda": "^0.29.0",
"readable-stream": "^4.4.2", "readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"three": "^0.154.0", "three": "^0.155.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^2.1.20", "vue-facing-decorator": "^3.0.2",
"vue-qrcode-reader": "^5.3.4", "vue-router": "^4.2.4",
"vue-router": "^4.2.3",
"web-did-resolver": "^2.0.27" "web-did-resolver": "^2.0.27"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.4",
"@types/ramda": "^0.29.3", "@types/ramda": "^0.29.3",
"@types/three": "^0.152.1", "@types/three": "^0.155.1",
"@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^5.61.0", "@typescript-eslint/parser": "^6.5.0",
"@vue-leaflet/vue-leaflet": "^0.10.1", "@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.8",
@@ -71,15 +69,15 @@
"@vue/cli-plugin-vuex": "~5.0.8", "@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.3", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.15",
"eslint": "^8.44.0", "eslint": "^8.48.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0-alpha.1", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.15.1", "eslint-plugin-vue": "^9.17.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"postcss": "^8.4.24", "postcss": "^8.4.29",
"prettier": "^3.0.0", "prettier": "^3.0.3",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.3",
"typescript": "~5.1.6" "typescript": "~5.2.2"
} }
} }

View File

@@ -1,18 +1,14 @@
tasks: tasks:
- fix "any" warnings
- fix missing updateAllFeed in ContactGiftingView page
- check that Anonymous users jdenticon are nulls (not "Anonymous")
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView) - test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
- .2 bug - on contacts view, click on "to" & "from" and nothing happens - .2 bug - on contacts view, click on "to" & "from" and nothing happens
- 40 notifications : - 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew - push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
- .2 Rename repo to crowd-sourcing... - 01 add my bounding box(es) of interest for searches on Nearby part of Discovery page
- .5 search by a bounding box(s) of interest for local projects (see API by clicking on "Nearby")
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew - 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
- 08 Scan QR code to import into contacts assignee:matthew - 08 Scan QR code to import into contacts assignee:matthew
- SEE: https://github.com/gruhn/vue-qrcode-reader - SEE: https://github.com/gruhn/vue-qrcode-reader
@@ -28,8 +24,6 @@ tasks:
- 24 Move to Vite assignee:matthew - 24 Move to Vite assignee:matthew
- .5 Allow edit of a contact name (but not the DID)
- .2 Edit Plan does not have icons across the bottom assignee-group:ui
- .5 include the hash of the latest commit, and maybe a version - .5 include the hash of the latest commit, and maybe a version
- .5 add link to further project / people when a project pays ahead - .5 add link to further project / people when a project pays ahead
- .5 add project ID to the URL, to make a project publicly-accessible - .5 add project ID to the URL, to make a project publicly-accessible
@@ -40,7 +34,6 @@ tasks:
- .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent - .2 fix rate limit verbiage (with the new one-per-day allowance) assignee:trent
- .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164 - .1 remove the logic to exclude beforeId in list of plans after server has commit 26b25af605e715600d4f12b6416ed9fd7142d164
- .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show" - .2 in SeedBackupView, don't load the mnemonic and keep it in memory; only load it when they click "show"
- .1 change default server in app.ts
- Discuss whether the remaining tasks are worthwhile before MVP release. - Discuss whether the remaining tasks are worthwhile before MVP release.
@@ -109,8 +102,6 @@ tasks:
- 40 notifications v+ : - 40 notifications v+ :
- pull, w/ scheduled runs - pull, w/ scheduled runs
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
log: log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29 - videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27 - project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

View File

@@ -0,0 +1,47 @@
<template>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-amber-400 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
@Component
export default class AlertMessage extends Vue {
@Prop alertTitle = "";
@Prop alertMessage = "";
isAlertVisible = this.alertMessage;
public onClickClose() {
this.isAlertVisible = false;
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-amber-200": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

@@ -8,7 +8,7 @@ import { toSvg } from "jdenticon";
@Component @Component
export default class EntityIcon extends Vue { export default class EntityIcon extends Vue {
@Prop entityId = ""; @Prop entityId = "";
@Prop iconSize = 0; @Prop iconSize = "";
generateIdenticon() { generateIdenticon() {
const svgString = toSvg(this.entityId, this.iconSize); const svgString = toSvg(this.entityId, this.iconSize);

View File

@@ -52,18 +52,18 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator"; import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
@Component @Component
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
@Prop message = ""; @Prop message = "";
giver?: GiverInputInfo; giver = null;
description = ""; description = "";
hours = "0"; hours = "0";
visible = false; visible = false;
open(giver: GiverInputInfo) { open(giver) {
// giver: GiverInputInfo
this.giver = giver; this.giver = giver;
this.visible = true; this.visible = true;
} }
@@ -81,7 +81,7 @@ export default class GiftedDialog extends Vue {
} }
@Emit("dialog-result") @Emit("dialog-result")
confirm(): GiverOutputInfo { confirm() {
const result = { const result = {
action: "confirm", action: "confirm",
giver: this.giver, giver: this.giver,
@@ -90,14 +90,14 @@ export default class GiftedDialog extends Vue {
}; };
this.close(); this.close();
this.description = ""; this.description = "";
this.giver = undefined; this.giver = null;
this.hours = "0"; this.hours = "0";
return result; return result;
} }
@Emit("dialog-result") @Emit("dialog-result")
cancel(): GiverOutputInfo { cancel() {
const result = { action: "cancel" }; const result = { action: "cancel" };
this.close(); this.close();
return result; return result;

View File

@@ -1,10 +1,8 @@
/** /**
* Generic strings that could be used throughout the app. * Generic strings that could be used throughout the app.
*
* See also ../libs/veramo/setup.ts
*/ */
export enum AppString { export enum AppString {
APP_NAME = "Time Safari", APP_NAME = "Kick-Start with Time",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch", PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch", TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
@@ -12,13 +10,3 @@ export enum AppString {
DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER, DEFAULT_ENDORSER_API_SERVER = TEST_ENDORSER_API_SERVER,
} }
/**
* See notiwind package
*/
export interface NotificationIface {
group: string;
type: string; // "toast" | "info" | "success" | "warning" | "danger"
title: string;
text: string;
}

View File

@@ -7,5 +7,5 @@ export interface Contact {
} }
export const ContactsSchema = { export const ContactsSchema = {
contacts: "&did, name, publicKeyBase64, registered, seesMe", contacts: "++did, name, publicKeyBase64, registered, seesMe",
}; };

View File

@@ -1,10 +1,3 @@
export type BoundingBox = {
eastLong: number;
maxLat: number;
minLat: number;
westLong: number;
};
// a singleton // a singleton
export type Settings = { export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY id: number; // there's only one entry: MASTER_SETTINGS_KEY
@@ -14,10 +7,6 @@ export type Settings = {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
lastViewedClaimId?: string; lastViewedClaimId?: string;
searchBoxes?: Array<{
name: string;
bbox: BoundingBox;
}>;
showContactGivesInline?: boolean; showContactGivesInline?: boolean;
}; };

View File

@@ -1,4 +1,5 @@
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
import { getRandomBytesSync } from "ethereum-cryptography/random"; import { getRandomBytesSync } from "ethereum-cryptography/random";
import { entropyToMnemonic } from "ethereum-cryptography/bip39"; import { entropyToMnemonic } from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english"; import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
@@ -6,9 +7,6 @@ import { HDNode } from "@ethersproject/hdnode";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as u8a from "uint8arrays"; import * as u8a from "uint8arrays";
import { ENDORSER_JWT_URL_LOCATION } from "@/libs/endorserServer";
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'"; export const DEFAULT_ROOT_DERIVATION_PATH = "m/76798669'/0'/0'/0'";
/** /**
@@ -152,24 +150,3 @@ export function fromJose(signature: string): {
export function bytesToHex(b: Uint8Array): string { export function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, "base16"); return u8a.toString(b, "base16");
} }
/**
@return results of uportJwtPayload:
{ iat: number, iss: string (DID), own: { name, publicEncKey (base64-encoded key) } }
Note that similar code is also contained in time-safari
*/
export const getContactPayloadFromJwtUrl = (jwtUrlText: string) => {
let jwtText = jwtUrlText;
const endorserContextLoc = jwtText.indexOf(ENDORSER_JWT_URL_LOCATION);
if (endorserContextLoc > -1) {
jwtText = jwtText.substring(
endorserContextLoc + ENDORSER_JWT_URL_LOCATION.length,
);
}
// JWT format: { header, payload, signature, data }
const jwt = didJwt.decodeJWT(jwtText);
return jwt.payload;
};

View File

@@ -6,12 +6,7 @@ import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch"; export const SERVICE_ID = "endorser.ch";
// the prefix for the contact URL
export const CONTACT_URL_PREFIX = "https://endorser.ch";
// the suffix for the contact URL
export const ENDORSER_JWT_URL_LOCATION = "/contact?jwt=";
export interface AgreeVerifiableCredential { export interface AgreeVerifiableCredential {
"@context": string; "@context": string;
@@ -26,13 +21,6 @@ export interface GiverInputInfo {
name?: string; name?: string;
} }
export interface GiverOutputInfo {
action: string;
giver?: GiverInputInfo;
description?: string;
hours?: number;
}
export interface ClaimResult { export interface ClaimResult {
success: { claimId: string; handleId: string }; success: { claimId: string; handleId: string };
error: { code: string; message: string }; error: { code: string; message: string };
@@ -67,30 +55,7 @@ export interface GiveVerifiableCredential {
fulfills?: { "@type": string; identifier: string }; fulfills?: { "@type": string; identifier: string };
identifier?: string; identifier?: string;
object?: { amountOfThisGood: number; unitCode: string }; object?: { amountOfThisGood: number; unitCode: string };
recipient?: { identifier: string }; recipient: { identifier: string };
}
export interface PlanVerifiableCredential {
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
description: string;
identifier?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
}
export interface PlanServerRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string;
endTime?: string;
issuerDid: string;
handleId: string;
locLat?: number;
locLon?: number;
startTime?: string;
url?: string;
} }
export interface RegisterVerifiableCredential { export interface RegisterVerifiableCredential {
@@ -98,7 +63,7 @@ export interface RegisterVerifiableCredential {
"@type": string; "@type": string;
agent: { identifier: string }; agent: { identifier: string };
object: string; object: string;
participant: { identifier: string }; recipient: { identifier: string };
} }
export interface InternalError { export interface InternalError {
@@ -110,7 +75,7 @@ export interface InternalError {
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN"; const HIDDEN_DID = "did:none:HIDDEN";
export function isHiddenDid(did: string) { export function isHiddenDid(did) {
return did === HIDDEN_DID; return did === HIDDEN_DID;
} }
@@ -123,7 +88,7 @@ export function didInfo(
allMyDids: Array<string>, allMyDids: Array<string>,
contacts: Array<Contact>, contacts: Array<Contact>,
): string { ): string {
const myId: string | undefined = R.find(R.equals(did), allMyDids); const myId: string | undefined = R.find(R.equals(did), allMyDids, did);
if (myId) { if (myId) {
return "You" + (myId !== activeDid ? " (Alt ID)" : ""); return "You" + (myId !== activeDid ? " (Alt ID)" : "");
} else { } else {
@@ -140,22 +105,6 @@ export function didInfo(
} }
} }
export interface ResultWithType {
type: string;
}
export interface SuccessResult extends ResultWithType {
type: "success";
response: AxiosResponse<ClaimResult>;
}
export interface ErrorResult {
type: "error";
error: InternalError;
}
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
/** /**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
* *
@@ -169,115 +118,76 @@ export async function createAndSubmitGive(
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
identity: IIdentifier, identity: IIdentifier,
fromDid?: string, fromDid: string,
toDid?: string, toDid: string,
description?: string, description: string,
hours?: number, hours: number,
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
): Promise<CreateAndSubmitGiveResult> { ): Promise<AxiosResponse<ClaimResult> | InternalError> {
try { // Make a claim
// Make a claim const vcClaim: GiveVerifiableCredential = {
const vcClaim: GiveVerifiableCredential = { "@context": "https://schema.org",
"@context": "https://schema.org", "@type": "GiveAction",
"@type": "GiveAction", recipient: { identifier: toDid },
}; };
if (toDid) { if (fromDid) {
vcClaim.recipient = { identifier: toDid }; vcClaim.agent = { identifier: fromDid };
} }
if (fromDid) { if (description) {
vcClaim.agent = { identifier: fromDid }; vcClaim.description = description;
} }
if (description) { if (hours) {
vcClaim.description = description; vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
} }
if (hours) { if (fulfillsProjectHandleId) {
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" }; vcClaim.fulfills = {
} "@type": "PlanAction",
if (fulfillsProjectHandleId) { identifier: fulfillsProjectHandleId,
vcClaim.fulfills = {
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
};
}
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
const firstKey = identity.keys[0];
if (!firstKey || !firstKey.privateKeyHex) {
throw {
error: "No private key",
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
};
}
const privateKeyHex = firstKey.privateKeyHex;
if (!privateKeyHex) {
throw {
error: "No private key",
message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
};
}
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const response = await axios.post(url, payload, { headers });
return {
type: "success",
response,
};
} catch (error: unknown) {
let errorMessage: string;
if (error instanceof Error) {
// If it's a JavaScript Error object
errorMessage = error.message;
} else if (
typeof error === "object" &&
error !== null &&
"message" in error
) {
// If it's an object that has a 'message' property
errorMessage = (error as { message: string }).message;
} else {
// Unknown error shape, default message
errorMessage = "Unknown error";
}
return {
type: "error",
error: {
error: errorMessage,
userMessage: "Failed to create and submit the claim.",
},
}; };
} }
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) {
return new Promise<InternalError>((resolve, reject) => {
reject({
error: "No private key",
message:
"Your identifier " +
identity.did +
" is not configured correctly. Use a different identifier.",
});
});
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return axios.post(url, payload, { headers });
} }
// from https://stackoverflow.com/a/175787/845494 // from https://stackoverflow.com/a/175787/845494

View File

@@ -1,7 +1,151 @@
// see also ../constants/app.ts and // Created from the setup in https://veramo.io/docs/guides/react_native
// Core interfaces
/* import {
createAgent,
IDIDManager,
IResolver,
IDataStore,
IKeyManager,
} from "@veramo/core";
*/
// Core identity manager plugin
//import { DIDManager } from "@veramo/did-manager";
// Ethr did identity provider
//import { EthrDIDProvider } from "@veramo/did-provider-ethr";
// Core key manager plugin
//import { KeyManager } from "@veramo/key-manager";
// Custom key management system for RN
//import { KeyManagementSystem } from '@veramo/kms-local-react-native'
// Custom resolver
// Custom resolvers
//import { DIDResolverPlugin } from "@veramo/did-resolver";
/* import { Resolver } from "did-resolver";
import { getResolver as ethrDidResolver } from "ethr-did-resolver";
import { getResolver as webDidResolver } from "web-did-resolver";
*/
// for VCs and VPs https://veramo.io/docs/api/credential-w3c
//import { CredentialIssuer } from '@veramo/credential-w3c'
// Storage plugin using TypeOrm
/* import {
Entities,
KeyStore,
DIDStore,
IDataStoreORM,
} from "@veramo/data-store";
*/
// TypeORM is installed with @veramo/typeorm
//import { createConnection } from 'typeorm'
//import * as R from "ramda";
/*
import { Contact } from '../entity/contact'
import { Settings } from '../entity/settings'
import { PrivateData } from '../entity/privateData'
import { Initial1616938713828 } from '../migration/1616938713828-initial'
import { SettingsContacts1616967972293 } from '../migration/1616967972293-settings-contacts'
import { EncryptedSeed1637856484788 } from '../migration/1637856484788-EncryptedSeed'
import { HomeScreenConfig1639947962124 } from '../migration/1639947962124-HomeScreenConfig'
import { HandlePublicKeys1652142819353 } from '../migration/1652142819353-HandlePublicKeys'
import { LastClaimsSeen1656811846836 } from '../migration/1656811846836-LastClaimsSeen'
import { ContactRegistered1662256903367 }from '../migration/1662256903367-ContactRegistered'
import { PrivateData1663080623479 } from '../migration/1663080623479-PrivateData'
const ALL_ENTITIES = Entities.concat([Contact, Settings, PrivateData])
// Create react native DB connection configured by ormconfig.js
export const dbConnection = createConnection({
database: 'endorser-mobile.sqlite',
entities: ALL_ENTITIES,
location: 'default',
logging: ['error', 'info', 'warn'],
migrations: [ Initial1616938713828, SettingsContacts1616967972293, EncryptedSeed1637856484788, HomeScreenConfig1639947962124, HandlePublicKeys1652142819353, LastClaimsSeen1656811846836, ContactRegistered1662256903367, PrivateData1663080623479 ],
migrationsRun: true,
type: 'react-native',
})
*/
function didProviderName(netName: string) { function didProviderName(netName: string) {
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName); return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName);
} }
export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet"); //const NETWORK_NAMES = ["mainnet", "rinkeby"];
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
DEFAULT_DID_PROVIDER_NETWORK_NAME,
);
export const HANDY_APP = false;
// this is used as the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch";
//const INFURA_PROJECT_ID = "INFURA_PROJECT_ID";
/*
const providers = {}
NETWORK_NAMES.forEach((networkName) => {
providers[didProviderName(networkName)] = new EthrDIDProvider({
defaultKms: 'local',
network: networkName,
rpcUrl: 'https://' + networkName + '.infura.io/v3/' + INFURA_PROJECT_ID,
gas: 1000001,
ttl: 60 * 60 * 24 * 30 * 12 + 1,
})
})
const didManager = new DIDManager({
store: new DIDStore(dbConnection),
defaultProvider: DEFAULT_DID_PROVIDER_NAME,
providers: providers,
})
*/
/* const basicDidResolvers = NETWORK_NAMES.map((networkName) => [
networkName,
new Resolver({
ethr: ethrDidResolver({
networks: [
{
name: networkName,
rpcUrl:
"https://" + networkName + ".infura.io/v3/" + INFURA_PROJECT_ID,
},
],
}).ethr,
web: webDidResolver().web,
}),
]);
const basicResolverMap = R.fromPairs(basicDidResolvers)
export const DEFAULT_BASIC_RESOLVER = basicResolverMap[DEFAULT_DID_PROVIDER_NETWORK_NAME]
const agentDidResolvers = NETWORK_NAMES.map((networkName) => {
return new DIDResolverPlugin({
resolver: basicResolverMap[networkName],
})
})
let allPlugins = [
new CredentialIssuer(),
new KeyManager({
store: new KeyStore(dbConnection),
kms: {
local: new KeyManagementSystem(),
},
}),
didManager,
].concat(agentDidResolvers)
*/
//export const agent = createAgent<IDIDManager & IKeyManager & IDataStore & IDataStoreORM & IResolver>({ plugins: allPlugins })

View File

@@ -1,11 +1,5 @@
import { import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
createRouter, import { accountsDB } from "@/db";
createWebHistory,
NavigationGuardNext,
RouteLocationNormalized,
RouteRecordRaw,
} from "vue-router";
import { accountsDB } from "@/db/index";
/** /**
* *
@@ -13,11 +7,7 @@ import { accountsDB } from "@/db/index";
* @param from :RouteLocationNormalized * @param from :RouteLocationNormalized
* @param next :NavigationGuardNext * @param next :NavigationGuardNext
*/ */
const enterOrStart = async ( const enterOrStart = async (to, from, next) => {
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
await accountsDB.open(); await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) { if (num_accounts > 0) {
@@ -200,12 +190,7 @@ const router = createRouter({
routes, routes,
}); });
const errorHandler = ( const errorHandler = (error, to, from) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
to: RouteLocationNormalized,
from: RouteLocationNormalized,
) => {
// Handle the error here // Handle the error here
console.error("Caught in top level error handler:", error, to, from); console.error("Caught in top level error handler:", error, to, from);

View File

@@ -2,7 +2,7 @@ import axios from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db } from "../db"; import { db } from "../db";
import { SERVICE_ID } from "../libs/endorserServer"; import { SERVICE_ID } from "../libs/veramo/setup";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";

View File

@@ -170,33 +170,6 @@
</button> </button>
</dialog> </dialog>
<div class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<h3 <h3
class="text-sm uppercase font-semibold mb-3" class="text-sm uppercase font-semibold mb-3"
@click="showAdvanced = !showAdvanced" @click="showAdvanced = !showAdvanced"
@@ -229,6 +202,36 @@
<div class="ml-2">Show amounts given with contacts</div> <div class="ml-2">Show amounts given with contacts</div>
</label> </label>
<div class="flex py-2">
<button
class="text-center text-md text-blue-500"
@click="checkLimits()"
>
Check Limits
</button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<div class="flex py-2"> <div class="flex py-2">
<router-link <router-link
:to="{ name: 'identity-switcher' }" :to="{ name: 'identity-switcher' }"
@@ -287,33 +290,23 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios/index";
import "dexie-export-import"; import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import QuickNav from "@/components/QuickNav.vue"; import { AxiosError } from "axios/index";
import QuickNav from "@/components/QuickNav";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
// 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;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString; Constants = AppString;
activeDid = ""; activeDid = "";
@@ -336,10 +329,8 @@ export default class AccountViewView extends Vue {
showPubCopy = false; showPubCopy = false;
showAdvanced = false; showAdvanced = false;
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@@ -349,7 +340,7 @@ export default class AccountViewView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -359,7 +350,7 @@ export default class AccountViewView extends Vue {
} }
// call fn, copy text to the clipboard, then redo fn after 2 seconds // call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) { doCopyTwoSecRedo(text, fn) {
fn(); fn();
useClipboard() useClipboard()
.copy(text) .copy(text)
@@ -411,8 +402,7 @@ export default class AccountViewView extends Vue {
}); });
this.checkLimitsFor(identity); this.checkLimitsFor(identity);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err) {
} catch (err: any) {
if ( if (
err.message === err.message ===
"Attempted to load account records with no identity available." "Attempted to load account records with no identity available."
@@ -515,8 +505,7 @@ export default class AccountViewView extends Vue {
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; this.limits = resp.data;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: unknown) {
} catch (error: any) {
if ( if (
error.message === error.message ===
"Attempted to load Give records with no identity available." "Attempted to load Give records with no identity available."
@@ -527,9 +516,13 @@ export default class AccountViewView extends Vue {
const serverError = error as AxiosError; const serverError = error as AxiosError;
console.error("Bad response retrieving limits: ", serverError); console.error("Bad response retrieving limits: ", serverError);
const data = (serverError.response && const data: ErrorResponse | undefined =
serverError.response.data) as ErrorResponse; serverError.response && serverError.response.data;
this.limitsMessage = data?.error?.message || "Bad server response."; if (data && data.error && data.error.message) {
this.limitsMessage = data.error.message;
} else {
this.limitsMessage = "Bad server response.";
}
} }
} }
@@ -579,7 +572,7 @@ export default class AccountViewView extends Vue {
this.apiServer = this.apiServerInput; this.apiServer = this.apiServerInput;
} }
setApiServerInput(value: string) { setApiServerInput(value) {
this.apiServerInput = value; this.apiServerInput = value;
} }
} }

View File

@@ -97,7 +97,7 @@
import * as R from "ramda"; import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
@@ -109,20 +109,10 @@ import {
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contact: Contact | null = null; contact: Contact | null = null;
@@ -134,7 +124,7 @@ export default class ContactsView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@@ -150,7 +140,7 @@ export default class ContactsView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -172,8 +162,7 @@ export default class ContactsView extends Vue {
if (this.activeDid && this.contact) { if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact); this.loadGives(this.activeDid, this.contact);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err) {
} catch (err: any) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -191,7 +180,7 @@ export default class ContactsView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
let result: Array<GiveServerRecord> = []; let result = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +

View File

@@ -82,47 +82,35 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts"; import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import { createAndSubmitGive } from "@/libs/endorserServer";
createAndSubmitGive, import { Account } from "@/db/tables/accounts";
CreateAndSubmitGiveResult,
ErrorResult,
GiverInputInfo,
GiverOutputInfo,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
apiServer = ""; apiServer = "";
accounts: typeof AccountsSchema; isHiddenSpinner = true;
accounts: AccountsSchema;
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
accountsDB.open(); accountsDB.open();
this.numAccounts = await accountsDB.accounts.count(); this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@@ -138,7 +126,7 @@ export default class HomeView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -149,20 +137,23 @@ export default class HomeView extends Vue {
async created() { async created() {
try { try {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
// eslint-disable-next-line @typescript-eslint/no-explicit-any this.feedLastViewedId = settings?.lastViewedClaimId;
} catch (err: any) { this.updateAllFeed();
} catch (err) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
err.message || err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
@@ -170,20 +161,37 @@ export default class HomeView extends Vue {
} }
} }
openDialog(giver: GiverInputInfo) { public async buildHeaders() {
(this.$refs.customDialog as GiftedDialog).open(giver); const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
} }
handleDialogResult(result: GiverOutputInfo) { openDialog(giver) {
this.$refs.customDialog.open(giver);
}
handleDialogResult(result) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
this.recordGive( this.recordGive(result.contact?.did, result.description, result.hours);
result.giver?.did, resolve();
result.description,
result.hours,
).then(() => {
resolve(null);
});
}); });
} else { } else {
// action was "cancel" so do nothing // action was "cancel" so do nothing
@@ -196,11 +204,7 @@ export default class HomeView extends Vue {
* @param description may be an empty string * @param description may be an empty string
* @param hours may be 0 * @param hours may be 0
*/ */
public async recordGive( public async recordGive(giverDid, description, hours) {
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) { if (!this.activeDid) {
this.$notify( this.$notify(
{ {
@@ -239,8 +243,8 @@ export default class HomeView extends Vue {
hours, hours,
); );
if (this.isGiveCreationError(result)) { if (isGiveCreationError(result)) {
const errorMessage = this.getGiveCreationErrorMessage(result); const errorMessage = getGiveCreationErrorMessage(result);
console.log("Error with give result:", result); console.log("Error with give result:", result);
this.$notify( this.$notify(
{ {
@@ -262,20 +266,16 @@ export default class HomeView extends Vue {
-1, -1,
); );
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) {
} catch (error: any) {
console.log("Error with give caught:", error); console.log("Error with give caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the Give.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: message, text:
getGiveErrorMessage(error) ||
"There was an error recording the give.",
}, },
-1, -1,
); );
@@ -284,12 +284,16 @@ export default class HomeView extends Vue {
// Helper functions for readability // Helper functions for readability
isGiveCreationError(result: CreateAndSubmitGiveResult) { isGiveCreationError(result) {
return result.type == "error"; return result.status !== 201 || result.data?.error;
} }
getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) { getGiveCreationErrorMessage(result) {
return (result as ErrorResult).error?.userMessage; return result.data?.error?.message;
}
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
} }
} }
</script> </script>

View File

@@ -3,7 +3,7 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Contact Info Your Contact Info
</h1> </h1>
@@ -17,54 +17,35 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<h1 class="text-4xl text-center font-light pt-4">Scan Contact Info</h1>
<qrcode-stream @detect="onScanDetect" @error="onScanError" />
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader"; import { accountsDB, db } from "@/db";
import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda"; import * as R from "ramda";
import { SimpleSigner } from "@/libs/crypto"; import { SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import {
CONTACT_URL_PREFIX,
ENDORSER_JWT_URL_LOCATION,
} from "@/libs/endorserServer";
// 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;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
QrcodeStream,
QRCodeVue3, QRCodeVue3,
QuickNav, QuickNav,
}, },
}) })
export default class ContactQRScanShow extends Vue { export default class ContactQRScanShow extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
qrValue = ""; qrValue = "";
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find( const account: Account | undefined = R.find(
@@ -122,47 +103,9 @@ export default class ContactQRScanShow extends Vue {
issuer: identity.did, issuer: identity.did,
signer: signer, signer: signer,
}); });
const viewPrefix = CONTACT_URL_PREFIX + ENDORSER_JWT_URL_LOCATION; const viewPrefix = "https://endorser.ch/contact?jwt=";
this.qrValue = viewPrefix + vcJwt; this.qrValue = viewPrefix + vcJwt;
} }
} }
/**
*
* @param content is the result of a QR scan, an array with one item with a rawValue property
*/
// Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) {
if (content[0]?.rawValue) {
console.log("onDetect", content[0].rawValue);
localStorage.setItem("contactEndorserUrl", content[0].rawValue);
this.$router.push({ name: "contacts" });
} else {
this.$notify(
{
group: "alert",
type: "warning",
title: "Invalid Contact QR Code",
text: "No QR code detected with contact information.",
},
-1,
);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanError(error: any) {
console.log("Scan was invalid:", error);
this.$notify(
{
group: "alert",
type: "warning",
title: "Invalid Scan",
text: "The scan was invalid.",
},
-1,
);
}
} }
</script> </script>

View File

@@ -20,11 +20,6 @@
<!-- New Contact --> <!-- New Contact -->
<div class="mb-4 flex"> <div class="mb-4 flex">
<span class="self-center bg-slate-500 text-white px-1.5 py-1 rounded-md">
<router-link :to="{ name: 'contact-qr' }">
<fa icon="qrcode" class="fa-fw" />
</router-link>
</span>
<input <input
type="text" type="text"
placeholder="DID, Name, Public Key" placeholder="DID, Name, Public Key"
@@ -126,21 +121,19 @@
</button> </button>
<button <button
v-if="contact.registered"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="Registered"
>
<fa icon="person-circle-check" class="fa-fw" />
</button>
<button
v-else
@click="register(contact)" @click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="Registration unknown"
> >
<fa <fa icon="person-circle-question" class="fa-fw" />
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else
icon="person-circle-question"
class="fa-fw"
title="Registration Unknown"
/>
</button> </button>
<button <button
@@ -216,26 +209,19 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { NotificationIface } from "@/constants/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { import {
accessToken,
getContactPayloadFromJwtUrl,
SimpleSigner,
} from "@/libs/crypto";
import {
GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
RegisterVerifiableCredential, RegisterVerifiableCredential,
SERVICE_ID, SERVICE_ID,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon";
// 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;
@@ -244,12 +230,9 @@ const Buffer = require("buffer/").Buffer;
components: { QuickNav, EntityIcon }, components: { QuickNav, EntityIcon },
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || "";
contactInput = ""; contactInput = "";
// { "did:...": concatenated-descriptions } entry for each contact // { "did:...": concatenated-descriptions } entry for each contact
givenByMeDescriptions: Record<string, string> = {}; givenByMeDescriptions: Record<string, string> = {};
@@ -284,15 +267,9 @@ export default class ContactsView extends Vue {
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts, allContacts,
); );
if (this.contactEndorserUrl) {
await this.newContactFromScan(this.contactEndorserUrl);
localStorage.removeItem("contactEndorserUrl");
this.contactEndorserUrl = "";
}
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
@@ -306,7 +283,7 @@ export default class ContactsView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -315,7 +292,7 @@ export default class ContactsView extends Vue {
return headers; return headers;
} }
public async getHeadersAndIdentity(activeDid: string) { public async getHeadersAndIdentity(activeDid) {
const identity = await this.getIdentity(activeDid); const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity); const headers = await this.getHeaders(identity);
@@ -324,11 +301,11 @@ export default class ContactsView extends Vue {
async loadGives() { async loadGives() {
const handleResponse = ( const handleResponse = (
resp: { status: number; data: { data: GiveServerRecord[] } }, resp,
descriptions: Record<string, string>, descriptions,
confirmed: Record<string, number>, confirmed,
unconfirmed: Record<string, number>, unconfirmed,
useRecipient: boolean, useRecipient,
) => { ) => {
if (resp.status === 200) { if (resp.status === 200) {
const allData = resp.data.data; const allData = resp.data.data;
@@ -360,8 +337,9 @@ export default class ContactsView extends Vue {
title: "Error With Server", title: "Error With Server",
text: text:
"Got an error retrieving your " + "Got an error retrieving your " +
(useRecipient ? "given" : "received") + resp.config.url.includes("recipientDid")
" time from the server.", ? "received"
: "given" + " time from the server.",
}, },
-1, -1,
); );
@@ -425,18 +403,6 @@ export default class ContactsView extends Vue {
} }
async onClickNewContact(): Promise<void> { async onClickNewContact(): Promise<void> {
if (!this.contactInput) {
this.$notify(
{
group: "alert",
type: "warning",
title: "No Contact",
text: "There was no contact info to add.",
},
-1,
);
return;
}
let did = this.contactInput; let did = this.contactInput;
let name, publicKeyBase64; let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(","); const commaPos1 = this.contactInput.indexOf(",");
@@ -455,74 +421,12 @@ export default class ContactsView extends Vue {
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64"); publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
} }
const newContact = { did, name, publicKeyBase64 }; const newContact = { did, name, publicKeyBase64 };
return this.addContact(newContact); await db.contacts.add(newContact);
} const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
async newContactFromScan(url: string): Promise<void> { (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
const payload = getContactPayloadFromJwtUrl(url); allContacts,
if (!payload) { );
this.$notify(
{
group: "alert",
type: "danger",
title: "No Contact Info",
text: "The contact info could not be parsed.",
},
-1,
);
return;
} else {
return this.addContact({
did: payload.iss,
name: payload.own.name,
publicKeyBase64: payload.own.publicEncKey,
} as Contact);
}
}
async addContact(newContact: Contact) {
if (!newContact.did) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Incomplete Contact",
text: "Cannot add a contact without a DID.",
},
-1,
);
return;
}
return db.contacts
.add(newContact)
.then(() => {
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts,
);
this.$notify(
{
group: "alert",
type: "success",
title: "Contact added",
text: newContact.name + " was added.",
},
-1,
);
})
.catch((err) => {
console.error("Error when adding contact to storage:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Contact Not Added",
text: "An error prevented importing.",
},
-1,
);
});
} }
async deleteContact(contact: Contact) { async deleteContact(contact: Contact) {
@@ -546,9 +450,6 @@ export default class ContactsView extends Vue {
confirm( confirm(
"Are you sure you want to use one of your registrations for " + "Are you sure you want to use one of your registrations for " +
this.nameForDid(this.contacts, contact.did) + this.nameForDid(this.contacts, contact.did) +
(contact.registered
? " -- especially since they are already marked as registered"
: "") +
"?", "?",
) )
) { ) {
@@ -700,8 +601,6 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" + "/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const identity = await this.getIdentity(this.activeDid);
const headers = await this.getHeaders(identity);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -779,17 +678,11 @@ export default class ContactsView extends Vue {
// if they have unconfirmed amounts, ask to confirm those first // if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) { if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are";
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours";
if ( if (
confirm( confirm(
"There " + "There are " +
isare +
" " +
this.givenToMeUnconfirmed[fromDid] + this.givenToMeUnconfirmed[fromDid] +
" unconfirmed " + " unconfirmed hours from them." +
hours +
" from them." +
" Would you like to confirm some of those hours?", " Would you like to confirm some of those hours?",
) )
) { ) {
@@ -797,7 +690,6 @@ export default class ContactsView extends Vue {
name: "contact-amounts", name: "contact-amounts",
query: { contactDid: fromDid }, query: { contactDid: fromDid },
}); });
return;
} }
} }
if (!this.isNumeric(this.hourInput)) { if (!this.isNumeric(this.hourInput)) {
@@ -848,9 +740,7 @@ export default class ContactsView extends Vue {
confirm( confirm(
"Are you sure you want to record " + "Are you sure you want to record " +
this.hourInput + this.hourInput +
" hour" + " hours " +
(this.hourInput == "1" ? "" : "s") +
" " +
toFrom + toFrom +
description + description +
"?", "?",

View File

@@ -9,7 +9,7 @@
</h1> </h1>
<!-- Quick Search --> <!-- Quick Search -->
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()"> <div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()">
<input <input
type="text" type="text"
v-model="searchTerms" v-model="searchTerms"
@@ -17,7 +17,7 @@
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/> />
<button <button
@click="searchAll()" @click="search()"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
> >
<fa icon="magnifying-glass" class="fa-fw"></fa> <fa icon="magnifying-glass" class="fa-fw"></fa>
@@ -32,8 +32,6 @@
href="#" href="#"
@click=" @click="
projects = []; projects = [];
isLocalActive = true;
isRemoteActive = false;
searchLocal(); searchLocal();
" "
v-bind:class="computedLocalTabClassNames()" v-bind:class="computedLocalTabClassNames()"
@@ -51,9 +49,7 @@
v-bind:class="computedRemoteTabClassNames()" v-bind:class="computedRemoteTabClassNames()"
@click=" @click="
projects = []; projects = [];
isRemoteActive = true; search();
isLocalActive = false;
searchAll();
" "
> >
Remote Remote
@@ -66,49 +62,6 @@
</ul> </ul>
</div> </div>
<div v-if="isLocalActive">
<div v-if="!isChoosingSearchBox">
<button
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="isChoosingSearchBox = true"
>
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
</button>
</div>
<div v-else>
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
Choose Location Below for Nearby Search
</button>
<button
v-if="isNewMarkerSet"
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="storeSearchBox"
>
Store This Location for Nearby Search
</button>
<button
v-if="searchBox"
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="forgetSearchBox"
>
Delete Stored Location
</button>
<button
v-if="isNewMarkerSet"
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="resetLatLong"
>
Reset Marker
</button>
<button
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="cancelSearchBoxSelect"
>
Cancel
</button>
</div>
</div>
<!-- Loading Animation --> <!-- Loading Animation -->
<div <div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full" class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
@@ -118,7 +71,7 @@
</div> </div>
<!-- Results List --> <!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox"> <InfiniteScroll @reached-bottom="loadMoreData">
<ul> <ul>
<li <li
class="border-b border-slate-300" class="border-b border-slate-300"
@@ -150,103 +103,35 @@
</li> </li>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
<div
v-if="isLocalActive && isChoosingSearchBox"
style="height: 600px; width: 800px"
>
<l-map
ref="map"
:center="[localCenterLat, localCenterLong]"
v-model:zoom="localZoom"
@click="setMapPoint"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
/>
<l-marker
v-if="isNewMarkerSet"
:lat-lng="[localCenterLat, localCenterLong]"
@click="isNewMarkerSet = false"
/>
<l-rectangle
v-if="isNewMarkerSet"
:bounds="[
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
]"
:weight="1"
/>
</l-map>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { LeafletMouseEvent } from "leaflet";
import "leaflet/dist/leaflet.css";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import {
LMap,
LMarker,
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { didInfo, ProjectData } from "@/libs/endorserServer"; import { didInfo } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon";
const DEFAULT_LAT_LONG_DIFF = 0.01;
const WORLD_ZOOM = 2;
const DEFAULT_ZOOM = 2;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: { QuickNav, InfiniteScroll, EntityIcon },
LRectangle,
QuickNav,
InfiniteScroll,
EntityIcon,
LMap,
LMarker,
LTileLayer,
},
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
searchTerms = ""; searchTerms = "";
projects: ProjectData[] = []; projects: ProjectData[] = [];
isChoosingSearchBox = false;
isLocalActive = true; isLocalActive = true;
isRemoteActive = false; isRemoteActive = false;
isNewMarkerSet = false;
localCenterLat = 0;
localCenterLong = 0;
localLatDiff = DEFAULT_LAT_LONG_DIFF;
localLongDiff = DEFAULT_LAT_LONG_DIFF;
localCount = 0; localCount = 0;
localZoom = DEFAULT_ZOOM;
remoteCount = 0; remoteCount = 0;
searchBox: { name: string; bbox: BoundingBox } | null = null;
isLoading = false; isLoading = false;
// make this function available to the Vue template // make this function available to the Vue template
@@ -257,9 +142,6 @@ export default class DiscoverView extends Vue {
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.searchBox = settings?.searchBoxes?.[0] || null;
this.resetLatLong();
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
await accountsDB.open(); await accountsDB.open();
@@ -269,10 +151,8 @@ export default class DiscoverView extends Vue {
this.searchLocal(); this.searchLocal();
} }
public async buildHeaders(): Promise<HeadersInit> { public async buildHeaders() {
const headers: HeadersInit = { const headers = { "Content-Type": "application/json" };
"Content-Type": "application/json",
};
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); await accountsDB.open();
@@ -293,13 +173,16 @@ export default class DiscoverView extends Vue {
return headers; return headers;
} }
public async searchAll(beforeId?: string) { public async search(beforeId?: string) {
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms); let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
if (beforeId) { if (beforeId) {
queryParams = queryParams + `&beforeId=${beforeId}`; queryParams = queryParams + `&beforeId=${beforeId}`;
} }
this.isRemoteActive = true;
this.isLocalActive = false;
try { try {
this.isLoading = true; this.isLoading = true;
const response = await fetch( const response = await fetch(
@@ -312,13 +195,12 @@ 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);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: `There was a problem accessing the server. Try again later.`, text: `There was a problem accessing the server. Please try again later. (${details})`,
}, },
-1, -1,
); );
@@ -331,15 +213,14 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data; const plans: ProjectData[] = results.data;
if (plans) { if (plans) {
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, rowid } = plan; const { name, description, handleId, rowid, issuerDid } = plan;
this.projects.push({ name, description, handleId, rowid }); this.projects.push({ name, description, handleId, rowid, issuerDid });
} }
this.remoteCount = this.projects.length; this.remoteCount = this.projects.length;
} else { } else {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) {
} catch (e: any) {
console.log("Error with feed load:", e); console.log("Error with feed load:", e);
this.$notify( this.$notify(
{ {
@@ -356,19 +237,14 @@ export default class DiscoverView extends Vue {
} }
public async searchLocal(beforeId?: string) { public async searchLocal(beforeId?: string) {
if (!this.searchBox) {
this.projects = [];
return;
}
const claimContents = const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms); "claimContents=" + encodeURIComponent(this.searchTerms);
let queryParams = [ let queryParams = [
claimContents, claimContents,
"minLocLat=" + this.searchBox.bbox.minLat, "minLocLat=40.901000",
"maxLocLat=" + this.searchBox.bbox.maxLat, "maxLocLat=40.904000",
"westLocLon=" + this.searchBox.bbox.westLong, "westLocLon=-111.914000",
"eastLocLon=" + this.searchBox.bbox.eastLong, "eastLocLon=-111.909000",
].join("&"); ].join("&");
if (beforeId) { if (beforeId) {
@@ -377,6 +253,8 @@ export default class DiscoverView extends Vue {
try { try {
this.isLoading = true; this.isLoading = true;
this.isLocalActive = true;
this.isRemoteActive = false;
const response = await fetch( const response = await fetch(
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{ {
@@ -387,13 +265,12 @@ 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);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: "There was a problem accessing the server. Try again later.", text: `There was a problem accessing the server. Please try again later. (${details})`,
}, },
-1, -1,
); );
@@ -418,8 +295,7 @@ export default class DiscoverView extends Vue {
} else { } else {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) {
} catch (e: any) {
console.log("Error with feed load:", e); console.log("Error with feed load:", e);
this.$notify( this.$notify(
{ {
@@ -445,7 +321,7 @@ export default class DiscoverView extends Vue {
if (this.isLocalActive) { if (this.isLocalActive) {
this.searchLocal(latestProject["rowid"]); this.searchLocal(latestProject["rowid"]);
} else if (this.isRemoteActive) { } else if (this.isRemoteActive) {
this.searchAll(latestProject["rowid"]); this.search(latestProject["rowid"]);
} }
} }
} }
@@ -462,128 +338,6 @@ export default class DiscoverView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
setMapPoint(event: LeafletMouseEvent) {
if (this.isNewMarkerSet) {
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
} else {
// marker is not set
this.localCenterLat = event.latlng.lat;
this.localCenterLong = event.latlng.lng;
let latDiff = DEFAULT_LAT_LONG_DIFF;
let longDiff = DEFAULT_LAT_LONG_DIFF;
// Guess at a size for the bounding box.
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
const bounds = event.target.boxZoom?._map?.getBounds();
if (bounds) {
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
}
this.localLatDiff = latDiff;
this.localLongDiff = longDiff;
this.isNewMarkerSet = true;
}
}
public resetLatLong() {
if (this.searchBox?.bbox) {
const bbox = this.searchBox.bbox;
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
this.localZoom = WORLD_ZOOM;
this.isNewMarkerSet = true;
} else {
this.isNewMarkerSet = false;
}
}
public async storeSearchBox() {
if (this.localCenterLong || this.localCenterLat) {
try {
const newSearchBox = {
name: "Local",
bbox: {
eastLong: this.localCenterLong + this.localLongDiff,
maxLat: this.localCenterLat + this.localLatDiff,
minLat: this.localCenterLat - this.localLatDiff,
westLong: this.localCenterLong - this.localLongDiff,
},
};
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [newSearchBox],
});
this.searchBox = newSearchBox;
this.isChoosingSearchBox = false;
this.searchLocal();
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Search Settings",
text: "Try going to a different page and then coming back.",
},
-1,
);
console.error(
"Telling user to retry the location search setting because:",
err,
);
}
} else {
this.$notify(
{
group: "alert",
type: "warning",
title: "No Location Selected",
text: "Select a location on the map.",
},
-1,
);
}
}
public async forgetSearchBox() {
try {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: [],
});
this.searchBox = null;
this.localCenterLat = 0;
this.localCenterLong = 0;
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
this.localZoom = DEFAULT_ZOOM;
this.isChoosingSearchBox = false;
this.isNewMarkerSet = false;
this.searchLocal();
} catch (err) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Updating Search Settings",
text: "Try going to a different page and then coming back.",
},
-1,
);
console.error(
"Telling user to retry the location search setting because:",
err,
);
}
}
public cancelSearchBoxSelect() {
this.isChoosingSearchBox = false;
this.localZoom = WORLD_ZOOM;
}
public computedLocalTabClassNames() { public computedLocalTabClassNames() {
return { return {
"inline-block": true, "inline-block": true,

View File

@@ -181,7 +181,7 @@
<script lang="ts"> <script lang="ts">
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {

View File

@@ -202,42 +202,26 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
createAndSubmitGive,
didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon";
import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
feedAllLoaded = false; feedAllLoaded = false;
feedData = []; feedData = [];
feedPreviousOldestId?: string; feedPreviousOldestId = null;
feedLastViewedId?: string; feedLastViewedId = null;
isHiddenSpinner = true; isHiddenSpinner = true;
numAccounts = 0; numAccounts = 0;
@@ -246,7 +230,7 @@ export default class HomeView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@@ -262,7 +246,7 @@ export default class HomeView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -284,8 +268,7 @@ export default class HomeView extends Vue {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId; this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed(); this.updateAllFeed();
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err) {
} catch (err: any) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -301,9 +284,7 @@ export default class HomeView extends Vue {
} }
public async buildHeaders() { public async buildHeaders() {
const headers: HeadersInit = { const headers = { "Content-Type": "application/json" };
"Content-Type": "application/json",
};
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); await accountsDB.open();
@@ -326,7 +307,7 @@ export default class HomeView extends Vue {
public async updateAllFeed() { public async updateAllFeed() {
this.isHiddenSpinner = false; this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId) await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
.then(async (results) => { .then(async (results) => {
if (results.data.length > 0) { if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data); this.feedData = this.feedData.concat(results.data);
@@ -361,7 +342,7 @@ export default class HomeView extends Vue {
this.isHiddenSpinner = true; this.isHiddenSpinner = true;
} }
public async retrieveClaims(endorserApiServer: string, beforeId?: string) { public async retrieveClaims(endorserApiServer, identifier, beforeId) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch( const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery, endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
@@ -384,13 +365,13 @@ export default class HomeView extends Vue {
} }
} }
giveDescription(giveRecord: GiveServerRecord) { giveDescription(giveRecord) {
// claim.claim happen for some claims wrapped in a Verifiable Credential let claim = giveRecord.fullClaim;
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (claim.claim) {
const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim; claim = claim.claim;
}
// agent.did is for legacy data, before March 2023 // agent.did is for legacy data, before March 2023
// eslint-disable-next-line @typescript-eslint/no-explicit-any const giverDid = claim.agent?.identifier || claim.agent?.did;
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
const giverInfo = didInfo( const giverInfo = didInfo(
giverDid, giverDid,
this.activeDid, this.activeDid,
@@ -401,9 +382,7 @@ export default class HomeView extends Vue {
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown"; : claim.description || "something unknown";
// recipient.did is for legacy data, before March 2023 // recipient.did is for legacy data, before March 2023
const gaveRecipientId = const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim.recipient?.identifier || (claim.recipient as any)?.did;
const gaveRecipientInfo = gaveRecipientId const gaveRecipientInfo = gaveRecipientId
? " to " + ? " to " +
didInfo( didInfo(
@@ -416,28 +395,23 @@ export default class HomeView extends Vue {
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo; return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
} }
displayAmount(code: string, amt: number) { displayAmount(code, amt) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1); return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
} }
currencyShortWordForCode(unitCode: string, single: boolean) { currencyShortWordForCode(unitCode, single) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
openDialog(giver: GiverInputInfo) { openDialog(giver) {
(this.$refs.customDialog as GiftedDialog).open(giver); this.$refs.customDialog.open(giver);
} }
handleDialogResult(result: GiverOutputInfo) { handleDialogResult(result) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
this.recordGive( this.recordGive(result.giver?.did, result.description, result.hours);
result.giver?.did, resolve();
result.description,
result.hours,
).then(() => {
resolve(null);
});
}); });
} else { } else {
// action was "cancel" so do nothing // action was "cancel" so do nothing
@@ -450,11 +424,7 @@ export default class HomeView extends Vue {
* @param description may be an empty string * @param description may be an empty string
* @param hours may be 0 * @param hours may be 0
*/ */
public async recordGive( public async recordGive(giverDid, description, hours) {
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) { if (!this.activeDid) {
this.$notify( this.$notify(
{ {
@@ -516,19 +486,16 @@ export default class HomeView extends Vue {
-1, -1,
); );
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) {
} catch (error: any) {
console.log("Error with give caught:", error); console.log("Error with give caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the give.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: message, text:
this.getGiveErrorMessage(error) ||
"There was an error recording the give.",
}, },
-1, -1,
); );
@@ -537,14 +504,16 @@ export default class HomeView extends Vue {
// Helper functions for readability // Helper functions for readability
// eslint-disable-next-line @typescript-eslint/no-explicit-any isGiveCreationError(result) {
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error; return result.status !== 201 || result.data?.error;
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any getGiveCreationErrorMessage(result) {
getGiveCreationErrorMessage(result: any) {
return result.data?.error?.message; return result.data?.error?.message;
} }
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
}
} }
</script> </script>

View File

@@ -67,33 +67,21 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts"; import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue { export default class IdentitySwitcherView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString; Constants = AppString;
public accounts: typeof AccountsSchema; public accounts: AccountsSchema;
public activeDid = ""; public activeDid;
public apiServer = ""; public firstName;
public apiServerInput = ""; public lastName;
public firstName = ""; public otherIdentities = [];
public lastName = "";
public otherIdentities: Array<{ did: string }> = [];
public showContactGives = false;
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@@ -130,20 +118,31 @@ export default class IdentitySwitcherView extends Vue {
} }
} }
} catch (err) { } catch (err) {
this.$notify( if (
{ err.message ===
group: "alert", "Attempted to load account records with no identity available."
type: "danger", ) {
title: "Error Loading Accounts", this.limitsMessage = "No identity.";
text: "Clear your cache and start over (after data backup).", this.loadingLimits = false;
}, } else {
-1, this.$notify(
); {
console.error("Telling user to clear cache at page create because:", err); group: "alert",
type: "danger",
title: "Error Creating Account",
text: "Clear your cache and start over (after data backup).",
},
-1,
);
console.error(
"Telling user to clear cache at page create because:",
err,
);
}
} }
} }
async switchAccount(did?: string) { async switchAccount(did: string) {
// 0 means none // 0 means none
if (did === "0") { if (did === "0") {
did = undefined; did = undefined;
@@ -152,7 +151,7 @@ export default class IdentitySwitcherView extends Vue {
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did, activeDid: did,
}); });
this.activeDid = did || ""; this.activeDid = did;
this.otherIdentities = []; this.otherIdentities = [];
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();

View File

@@ -67,7 +67,7 @@ import {
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
} from "../libs/crypto"; } from "../libs/crypto";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ @Component({

View File

@@ -73,7 +73,7 @@ import {
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
} from "../libs/crypto"; } from "../libs/crypto";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ @Component({
@@ -87,9 +87,9 @@ export default class ImportAccountView extends Vue {
async mounted() { async mounted() {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const seedDids: Record<string, Array<string>> = {}; const seedDids = {};
accounts.forEach((account) => { accounts.forEach((account) => {
const prevDids: Array<string> = seedDids[account.mnemonic] || []; const prevDids = seedDids[account.mnemonic] || [];
seedDids[account.mnemonic] = prevDids.concat([account.did]); seedDids[account.mnemonic] = prevDids.concat([account.did]);
}); });
this.didArrays = Object.values(seedDids); this.didArrays = Object.values(seedDids);
@@ -107,9 +107,9 @@ export default class ImportAccountView extends Vue {
public async incrementDerivation() { public async incrementDerivation() {
await accountsDB.open(); await accountsDB.open();
// find the maximum derivation path for the selected DIDs // find the maximum derivation path for the selected DIDs
const selectedArray: Array<string> = const selectedArray: Array<string> = this.didArrays.find(
this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) || (dids) => dids[0] === this.selectedArrayFirstDid,
[]; );
const allMatchingAccounts = await accountsDB.accounts const allMatchingAccounts = await accountsDB.accounts
.where("did") .where("did")
.anyOf(...selectedArray) .anyOf(...selectedArray)

View File

@@ -49,7 +49,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { db } from "@/db/index"; import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ @Component({

View File

@@ -107,26 +107,16 @@ import * as didJwt from "did-jwt";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { PlanVerifiableCredential } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { LMap, LMarker, LTileLayer }, components: { LMap, LMarker, LTileLayer },
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
description = ""; description = "";
@@ -143,7 +133,7 @@ export default class NewEditProjectView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@@ -159,7 +149,7 @@ export default class NewEditProjectView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -218,7 +208,7 @@ export default class NewEditProjectView extends Vue {
private async SaveProject(identity: IIdentifier) { private async SaveProject(identity: IIdentifier) {
// Make a claim // Make a claim
const vcClaim: PlanVerifiableCredential = { const vcClaim: VerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "PlanAction", "@type": "PlanAction",
name: this.projectName, name: this.projectName,
@@ -273,15 +263,17 @@ export default class NewEditProjectView extends Vue {
// version shows up here: https://api.endorser.ch/api-docs/ // version shows up here: https://api.endorser.ch/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) { if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
this.errorMessage = ""; this.errorMessage = "";
// handleId is new in server v release-1.6.0; remove fullIri when that // handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://api.endorser.ch/api-docs/ // version shows up here: https://api.endorser.ch/api-docs/
useAppStore().setProjectId( useAppStore().setProjectId(
resp.data.success.handleId || resp.data.success.fullIri, resp.data.success.handleId || resp.data.success.fullIri,
); );
setTimeout( setTimeout(
function (that: NewEditProjectView) { function (that: Vue) {
that.$router.push({ name: "project" }); const route = {
name: "project",
};
that.$router.push(route);
}, },
2000, 2000,
this, this,
@@ -289,13 +281,11 @@ export default class NewEditProjectView extends Vue {
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error saving the project."; let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError<{ const serverError = error as AxiosError;
error?: { message?: string };
}>;
if (serverError) { if (serverError) {
if (Object.prototype.hasOwnProperty.call(serverError, "message")) { if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError); console.log(serverError);
userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user. userMessage = serverError.response.data.error.message; // This is info for the user.
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -40,10 +40,10 @@
<script lang="ts"> <script lang="ts">
import "dexie-export-import"; import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {

View File

@@ -204,40 +204,28 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError, RawAxiosRequestHeaders } from "axios"; import { AxiosError } from "axios";
import * as moment from "moment"; import * as moment from "moment";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord, GiveServerRecord,
ResultWithType,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -271,7 +259,7 @@ export default class ProjectViewView extends Vue {
this.LoadProject(identity); this.LoadProject(identity);
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@@ -287,7 +275,7 @@ export default class ProjectViewView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity: IIdentifier) { public async getHeaders(identity) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -305,12 +293,7 @@ export default class ProjectViewView extends Vue {
} }
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo( didInfo(did, activeDid, dids, contacts) {
did: string,
activeDid: string,
dids: Array<string>,
contacts: Array<Contact>,
) {
return didInfo(did, activeDid, dids, contacts); return didInfo(did, activeDid, dids, contacts);
} }
@@ -327,7 +310,7 @@ export default class ProjectViewView extends Vue {
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const headers: RawAxiosRequestHeaders = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
if (identity) { if (identity) {
@@ -461,9 +444,8 @@ export default class ProjectViewView extends Vue {
} }
} }
openDialog(contact: GiverInputInfo) { openDialog(contact) {
const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog; this.$refs.customDialog.open(contact);
dialog.open(contact);
} }
getOpenStreetMapUrl() { getOpenStreetMapUrl() {
@@ -480,16 +462,11 @@ export default class ProjectViewView extends Vue {
); );
} }
handleDialogResult(result: GiverOutputInfo) { handleDialogResult(result) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
this.recordGive( this.recordGive(result.contact?.did, result.description, result.hours);
result.giver?.did, resolve();
result.description,
result.hours,
).then(() => {
resolve(null);
});
}); });
} else { } else {
// action was not "confirm" so do nothing // action was not "confirm" so do nothing
@@ -502,7 +479,7 @@ export default class ProjectViewView extends Vue {
* @param description may be an empty string * @param description may be an empty string
* @param hours may be 0 * @param hours may be 0
*/ */
async recordGive(giverDid?: string, description?: string, hours?: number) { async recordGive(giverDid, description, hours) {
if (!this.activeDid) { if (!this.activeDid) {
this.$notify( this.$notify(
{ {
@@ -526,7 +503,10 @@ export default class ProjectViewView extends Vue {
}, },
-1, -1,
); );
} else { return;
}
try {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
@@ -538,7 +518,21 @@ export default class ProjectViewView extends Vue {
hours, hours,
this.projectId, this.projectId,
); );
if (result.type == "success") {
if (result.status !== 201 || result.data?.error) {
console.log("Error with give result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
result.data?.error?.message ||
"There was an error recording the give.",
},
-1,
);
} else {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -548,27 +542,21 @@ export default class ProjectViewView extends Vue {
}, },
-1, -1,
); );
} else {
console.log("Error with give creation:", result);
if (result.type != "error") {
console.log(
"... and it has an unexpected result type of",
(result as ResultWithType).type,
);
}
const message =
result?.error?.userMessage ||
"There was an error recording the Give.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: message,
},
-1,
);
} }
} catch (e) {
console.log("Error with give caught:", e);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.",
},
-1,
);
} }
} }
} }

View File

@@ -72,34 +72,22 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon";
import { ProjectData } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { InfiniteScroll, QuickNav, EntityIcon }, components: { InfiniteScroll, QuickNav, EntityIcon },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
apiServer = ""; apiServer = "";
projects: ProjectData[] = []; projects: ProjectData[] = [];
current: IIdentifier; current: IIdentifier;
isLoading = false; isLoading = false;
alertTitle = "";
alertMessage = "";
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@@ -124,30 +112,21 @@ export default class ProjectsView extends Vue {
if (resp.status === 200 || !resp.data.data) { if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data; const plans: ProjectData[] = resp.data.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, rowid } = plan; const { name, description, handleId = plan.fullIri, rowid } = plan;
this.projects.push({ name, description, handleId, rowid }); this.projects.push({ name, description, handleId, rowid });
} }
} else { } else {
console.log("Bad server response & data:", resp.status, resp.data); console.log("Bad server response & data:", resp.status, resp.data);
this.$notify( throw Error("Failed to get projects from the server.");
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to get projects from the server. Try again later.",
},
-1,
);
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error) {
} catch (error: any) { console.error("Got error loading projects:", error.message);
console.error("Got error loading projects:", error.message || error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: "Got an error loading projects.", text: "Got an error loading projects: " + error.message,
}, },
-1, -1,
); );
@@ -191,7 +170,7 @@ export default class ProjectsView extends Vue {
await this.dataLoader(url, token); await this.dataLoader(url, token);
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")

View File

@@ -55,27 +55,14 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db";
import * as R from "ramda"; import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
interface Account {
mnemonic: string;
}
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue { export default class SeedBackupView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; activeAccount = null;
activeAccount: Account | null | undefined = null;
numAccounts = 0; numAccounts = 0;
showSeed = false; showSeed = false;
@@ -90,7 +77,7 @@ export default class SeedBackupView extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
this.numAccounts = accounts.length; this.numAccounts = accounts.length;
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts); this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
} catch (err: unknown) { } catch (err) {
console.error("Got an error loading an identity:", err); console.error("Got an error loading an identity:", err);
this.$notify( this.$notify(
{ {

View File

@@ -37,7 +37,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB } from "@/db/index"; import { accountsDB } from "@/db";
@Component({ @Component({
components: {}, components: {},

View File

@@ -37,32 +37,15 @@
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js"; import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js"; import { World } from "@/components/World/World.js";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav";
interface RendererSVGType {
domElement: Element;
}
interface Dictionary<T> {
[key: string]: T;
}
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { World, QuickNav } }) @Component({ components: { World, QuickNav } })
export default class StatisticsView extends Vue { export default class StatisticsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
world: World; world: World;
worldProperties: Dictionary<number> = {}; worldProperties: WorldProperties = {};
mounted() { mounted() {
try { try {
@@ -70,14 +53,14 @@ export default class StatisticsView extends Vue {
const newWorld = new World(container, this); const newWorld = new World(container, this);
newWorld.start(); newWorld.start();
this.world = newWorld; this.world = newWorld;
} catch (err: unknown) { } catch (err) {
const error = err as Error; console.log(err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Mounting Error", title: "Mounting Error",
text: error.message, text: err.message,
}, },
-1, -1,
); );
@@ -95,12 +78,12 @@ export default class StatisticsView extends Vue {
ExportToSVG(rendererSVG, "test.svg"); ExportToSVG(rendererSVG, "test.svg");
} }
public setWorldProperty(propertyName: string, propertyValue: number) { public setWorldProperty(propertyName, propertyValue) {
this.worldProperties[propertyName] = propertyValue; this.worldProperties[propertyName] = propertyValue;
} }
} }
function ExportToSVG(rendererSVG: RendererSVGType, filename: string) { function ExportToSVG(rendererSVG, filename) {
const XMLS = new XMLSerializer(); const XMLS = new XMLSerializer();
const svgfile = XMLS.serializeToString(rendererSVG.domElement); const svgfile = XMLS.serializeToString(rendererSVG.domElement);
const svgData = svgfile; const svgData = svgfile;

View File

@@ -1,47 +1,41 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowJs": true, "target": "esnext",
"resolveJsonModule": true, "module": "esnext",
"target": "esnext", "strict": true,
"module": "esnext", "jsx": "preserve",
"strict": true, "moduleResolution": "node",
"strictPropertyInitialization": false, "experimentalDecorators": true,
"jsx": "preserve", "skipLibCheck": true,
"moduleResolution": "node", "esModuleInterop": true,
"experimentalDecorators": true, "allowSyntheticDefaultImports": true,
"skipLibCheck": true, "forceConsistentCasingInFileNames": true,
"esModuleInterop": true, "useDefineForClassFields": true,
"allowSyntheticDefaultImports": true, "sourceMap": true,
"forceConsistentCasingInFileNames": true, "baseUrl": ".",
"useDefineForClassFields": true, "types": [
"sourceMap": true, "webpack-env"
"baseUrl": "./src",
"types": [
"webpack-env"
],
"paths": {
"@/components/*": ["components/*"],
"@/views/*": ["views/*"],
"@/db/*": ["db/*"],
"@/libs/*": ["libs/*"],
"@/constants/*": ["constants/*"],
"@/store/*": ["store/*"],
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
], ],
"exclude": [ "paths": {
"node_modules" "@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
] ]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
} }