diff --git a/README.md b/README.md
index 7648ce3..da752e5 100644
--- a/README.md
+++ b/README.md
@@ -188,6 +188,9 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
## Kudos
+Gifts make the world go 'round!
+
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
-* [Many libraries]() such as Veramo.io, Vuejs.org, threejs
+* [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
+* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)
diff --git a/project.task.yaml b/project.task.yaml
index 54205d9..9c96dcd 100644
--- a/project.task.yaml
+++ b/project.task.yaml
@@ -4,8 +4,9 @@
- add infinite scroll assignee:matthew
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
-- allow type annotations in World.js & landmarks.js (since we get this error: "Types are not supported by current JavaScript version")
+- allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- replace user-affecting console.log & console.error with error messages (eg. catches)
+- if there's no identity, handle it on pages which expect an identity (eg. project -- look for JSON.parse identity calls)
- 8 Move to vue-facing-decorator
@@ -30,9 +31,11 @@
- show pop-up confirming that settings & contacts have been downloaded
-- Ensure each action sent to the server has a confirmation.
+- Ensure each action sent to the server has a confirmation - registration
-- discover screen
+- Home Feed & Quick Give screen :
+ - 01 save the feed-viewed status in settings storage ("afterQuery")
+ - 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
- .5 customize favicon
- .5 make advanced features harder to access; advanced build?
@@ -40,14 +43,29 @@
- 24 Move to Vite
+- 40 notifications :
+ - push
+
+- stats v1 :
+ - 01 show numeric stats
+ - 01 link to world for specific stats
+ - .5 don't load another instance of a bush if it already exists
+
+- Do we want split first name & last name?
+- remove 'about' page
+
- Release Minimum Viable Product :
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers.
+ - Rename DB to TimeSafari.
- Switch default server to the public server.
- Deploy to a server.
- Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS.
+- 40 notifications v+ :
+ - pull, w/ scheduled runs
+
- Stats :
- 01 point out user's location on the world
- 01 present a credential selected from the stats
diff --git a/public/img/textures/forest-floor.png b/public/img/textures/forest-floor.png
deleted file mode 100644
index 5a7a58e..0000000
Binary files a/public/img/textures/forest-floor.png and /dev/null differ
diff --git a/public/img/textures/leafy-autumn-forest-floor.jpg b/public/img/textures/leafy-autumn-forest-floor.jpg
new file mode 100644
index 0000000..cadd5a6
Binary files /dev/null and b/public/img/textures/leafy-autumn-forest-floor.jpg differ
diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue
new file mode 100644
index 0000000..c48ce3c
--- /dev/null
+++ b/src/components/GiftedDialog.vue
@@ -0,0 +1,104 @@
+
+
+
+
+ Received from {{ contact?.name || "nobody in particular" }}
+
+
{{ message }}
+
+
+ Hours
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/InfiniteScroll.vue b/src/components/InfiniteScroll.vue
index 2afdb9e..d973694 100644
--- a/src/components/InfiniteScroll.vue
+++ b/src/components/InfiniteScroll.vue
@@ -14,6 +14,7 @@ export default class InfiniteScroll extends Vue {
readonly distance!: number;
private observer!: IntersectionObserver;
+ // 'mounted' hook runs after initial render
mounted() {
const options = {
root: this.$refs.scrollContainer as HTMLElement,
@@ -24,6 +25,7 @@ export default class InfiniteScroll extends Vue {
this.observer.observe(this.$refs.sentinel as HTMLElement);
}
+ // 'beforeUnmount' hook runs before unmounting the component
beforeUnmount() {
this.observer.disconnect();
}
diff --git a/src/components/World/components/objects/terrain.js b/src/components/World/components/objects/terrain.js
index b7d1f54..0abb80d 100644
--- a/src/components/World/components/objects/terrain.js
+++ b/src/components/World/components/objects/terrain.js
@@ -2,7 +2,7 @@ import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
export function createTerrain(props) {
const loader = new TextureLoader();
- const height = loader.load("img/textures/forest-floor.png");
+ const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
// w h
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
diff --git a/src/db/index.ts b/src/db/index.ts
index bcb3806..58bd231 100644
--- a/src/db/index.ts
+++ b/src/db/index.ts
@@ -41,7 +41,12 @@ const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/
-// create password and place password in localStorage
+/**
+ * Create password and place password in localStorage.
+ *
+ * It's good practice to keep the data encrypted at rest, so we'll do that even
+ * if the secret is stored right next to the app.
+ */
const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
diff --git a/src/db/tables/accounts.ts b/src/db/tables/accounts.ts
index b42e55a..d31f160 100644
--- a/src/db/tables/accounts.ts
+++ b/src/db/tables/accounts.ts
@@ -3,6 +3,8 @@ export type Account = {
dateCreated: string;
derivationPath: string;
did: string;
+ // stringified JSON containing underlying key material of type IIdentifier
+ // https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
identity: string;
publicKeyHex: string;
mnemonic: string;
diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts
index a7671c2..02f7ba7 100644
--- a/src/db/tables/settings.ts
+++ b/src/db/tables/settings.ts
@@ -1,10 +1,12 @@
// a singleton
export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY
+
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
+ lastViewedClaimId?: string;
showContactGivesInline?: boolean;
};
diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts
index 227577f..6766ecd 100644
--- a/src/libs/endorserServer.ts
+++ b/src/libs/endorserServer.ts
@@ -1,21 +1,32 @@
+import * as R from "ramda";
+import { IIdentifier } from "@veramo/core";
+import { accessToken, SimpleSigner } from "@/libs/crypto";
+import * as didJwt from "did-jwt";
+import { Axios, AxiosResponse } from "axios";
+
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch";
-export interface GenericClaim {
+export interface AgreeVerifiableCredential {
"@context": string;
"@type": string;
- issuedAt: string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- claim: Record;
+ object: Record;
}
-export interface AgreeVerifiableCredential {
+export interface ClaimResult {
+ success: { claimId: string; handleId: string };
+ error: { code: string; message: string };
+}
+
+export interface GenericClaim {
"@context": string;
"@type": string;
+ issuedAt: string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- object: Record;
+ claim: Record;
}
export interface GiveServerRecord {
@@ -33,10 +44,10 @@ export interface GiveServerRecord {
export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
- agent: { identifier: string };
+ agent?: { identifier: string };
description?: string;
identifier?: string;
- object: { amountOfThisGood: number; unitCode: string };
+ object?: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
}
@@ -47,3 +58,125 @@ export interface RegisterVerifiableCredential {
object: string;
recipient: { identifier: string };
}
+
+export interface InternalError {
+ error: string; // for system logging
+ userMessage?: string; // for user display
+}
+
+// This is used to check for hidden info.
+// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
+const HIDDEN_DID = "did:none:HIDDEN";
+
+export function isHiddenDid(did) {
+ return did === HIDDEN_DID;
+}
+
+/**
+ always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
+ **/
+export function didInfo(did, identifiers, contacts) {
+ const myId = R.find((i) => i.did === did, identifiers);
+ if (myId) {
+ return "You";
+ } else {
+ const contact = R.find((c) => c.did === did, contacts);
+ if (contact) {
+ return contact.name || "Someone Unnamed in Contacts";
+ } else if (!did) {
+ return "Unpecified Person";
+ } else if (isHiddenDid(did)) {
+ return "Someone Not In Network";
+ } else {
+ return "Someone Not In Contacts";
+ }
+ }
+}
+
+/**
+ * For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
+
+ * @param identity
+ * @param fromDid may be null
+ * @param toDid
+ * @param description may be null; should have this or hours
+ * @param hours may be null; should have this or description
+ */
+export async function createAndSubmitGive(
+ axios: Axios,
+ apiServer: string,
+ identity: IIdentifier,
+ fromDid: string,
+ toDid: string,
+ description: string,
+ hours: number
+): Promise | InternalError> {
+ // Make a claim
+ const vcClaim: GiveVerifiableCredential = {
+ "@context": "https://schema.org",
+ "@type": "GiveAction",
+ recipient: { identifier: toDid },
+ };
+ if (fromDid) {
+ vcClaim.agent = { identifier: fromDid };
+ }
+ if (description) {
+ vcClaim.description = description;
+ }
+ if (hours) {
+ vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
+ }
+ // 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((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
+//
+export function isNumeric(str: string): boolean {
+ return !isNaN(+str);
+}
+
+export function numberOrZero(str: string): number {
+ return isNumeric(str) ? +str : 0;
+}
diff --git a/src/main.ts b/src/main.ts
index 9595c5b..860f91b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -37,6 +37,8 @@ import {
faRotate,
faShareNodes,
faSpinner,
+ faSquareCaretDown,
+ faSquareCaretUp,
faTrashCan,
faUser,
faUsers,
@@ -71,6 +73,8 @@ library.add(
faRotate,
faShareNodes,
faSpinner,
+ faSquareCaretDown,
+ faSquareCaretUp,
faTrashCan,
faUser,
faUsers,
diff --git a/src/router/index.ts b/src/router/index.ts
index 302f00b..5ae7a16 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,21 +1,29 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db";
+/**
+ *
+ * @param to :RouteLocationNormalized
+ * @param from :RouteLocationNormalized
+ * @param next :NavigationGuardNext
+ */
+const enterOrStart = async (to, from, next) => {
+ await accountsDB.open();
+ const num_accounts = await accountsDB.accounts.count();
+ if (num_accounts > 0) {
+ next();
+ } else {
+ next({ name: "start" });
+ }
+};
+
const routes: Array = [
{
path: "/",
name: "home",
component: () =>
- import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
- beforeEnter: async (to, from, next) => {
- await accountsDB.open();
- const num_accounts = await accountsDB.accounts.count();
- if (num_accounts > 0) {
- next();
- } else {
- next({ name: "start" });
- }
- },
+ import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
+ beforeEnter: enterOrStart,
},
{
path: "/about",
@@ -28,6 +36,7 @@ const routes: Array = [
name: "account",
component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
+ beforeEnter: enterOrStart,
},
{
path: "/confirm-contact",
@@ -58,6 +67,7 @@ const routes: Array = [
name: "contacts",
component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
+ beforeEnter: enterOrStart,
},
{
path: "/scan-contact",
@@ -130,6 +140,7 @@ const routes: Array = [
name: "projects",
component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
+ beforeEnter: enterOrStart,
},
{
path: "/seed-backup",
diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue
index b86e3d2..17e6e2d 100644
--- a/src/views/AccountViewView.vue
+++ b/src/views/AccountViewView.vue
@@ -298,10 +298,10 @@
+
+ {{ contact.name }},
+
+ or
+
+ nobody in particular
+
+
+
+
+
+
+
+
+
Latest Activity
+
+
+ Loading…
+
+
+
+
+ You've seen all claims below.
+
+ {{ this.giveDescription(record) }}
+
+
+
+
+
+
+
+
+
+
+
{{ alertTitle }}
+
{{ alertMessage }}
+
diff --git a/src/views/NewEditProjectView.vue b/src/views/NewEditProjectView.vue
index 0644c9a..72079a8 100644
--- a/src/views/NewEditProjectView.vue
+++ b/src/views/NewEditProjectView.vue
@@ -49,8 +49,9 @@
Save Project
-
+
+
+
Saving…
@@ -122,11 +123,14 @@ export default class NewEditProjectView extends Vue {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
- console.error("Problem! Should have a profile!");
+ console.error("Error: no account was found.");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
- const identity = JSON.parse(account?.identity || "undefined");
+ const identity = JSON.parse(account?.identity || "null");
+ if (!identity) {
+ throw new Error("No identity found.");
+ }
this.LoadProject(identity);
}
}
@@ -222,7 +226,7 @@ export default class NewEditProjectView extends Vue {
);
}
} catch (error) {
- let userMessage = "There was an error. See logs for more info.";
+ let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError;
if (serverError) {
this.isAlertVisible = true;
@@ -254,11 +258,14 @@ export default class NewEditProjectView extends Vue {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts === 0) {
- console.error("Problem! Should have a profile!");
+ console.error("Error: there is no account.");
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
- const identity = JSON.parse(account?.identity || "undefined");
+ const identity = JSON.parse(account?.identity || "null");
+ if (!identity) {
+ throw new Error("No identity found.");
+ }
this.SaveProject(identity);
}
}
diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue
index 5af2fdf..37fd059 100644
--- a/src/views/ProjectViewView.vue
+++ b/src/views/ProjectViewView.vue
@@ -240,7 +240,10 @@ export default class ProjectViewView extends Vue {
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
- const identity = JSON.parse(account?.identity || "undefined");
+ const identity = JSON.parse(account?.identity || "null");
+ if (!identity) {
+ throw new Error("No identity found.");
+ }
this.LoadProject(identity);
}
}
diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue
index f9dee84..b497eb2 100644
--- a/src/views/ProjectsView.vue
+++ b/src/views/ProjectsView.vue
@@ -167,7 +167,10 @@ export default class ProjectsView extends Vue {
} else {
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
- const identity = JSON.parse(account?.identity || "undefined");
+ const identity = JSON.parse(account?.identity || "null");
+ if (!identity) {
+ throw new Error("No identity found.");
+ }
this.LoadProjects(identity);
}
}
diff --git a/src/views/StatisticsView.vue b/src/views/StatisticsView.vue
index 1b32a14..2913b20 100644
--- a/src/views/StatisticsView.vue
+++ b/src/views/StatisticsView.vue
@@ -61,7 +61,7 @@
Each will show at their time of appearance relative to all others.
Note that the ones on the left and right edges are randomized
- because not all their positional data is visible to you.
+ because their data isn't all visible to you.