diff --git a/README.md b/README.md
index 7648ce3c..da752e58 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 5e8936f3..9c96dcdc 100644
--- a/project.task.yaml
+++ b/project.task.yaml
@@ -4,8 +4,11 @@
- 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
- stats v1 :
- 01 show numeric stats
@@ -28,21 +31,41 @@
- 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?
+- 04 allow user to download claims, mine + ones I can see about me from others
+
+- 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
@@ -66,6 +89,7 @@
- Peer DID
+
- DIDComm
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
diff --git a/public/img/textures/forest-floor.png b/public/img/textures/forest-floor.png
deleted file mode 100644
index 5a7a58e6..00000000
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 00000000..cadd5a65
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 00000000..c48ce3cc
--- /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 5a497ea8..3baf32a6 100644
--- a/src/components/InfiniteScroll.vue
+++ b/src/components/InfiniteScroll.vue
@@ -31,6 +31,7 @@ export default class InfiniteScroll extends Vue {
}
}
+ // 'beforeUnmount' hook runs before unmounting the component
beforeUnmount() {
if (this.observer) {
this.observer.disconnect();
diff --git a/src/components/World/components/objects/terrain.js b/src/components/World/components/objects/terrain.js
index b7d1f54e..0abb80d7 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 bcb38066..58bd2317 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 b42e55ac..d31f1608 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 a7671c22..02f7ba7f 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 227577fe..6766ecd0 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 c3bec57c..860f91b5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -10,6 +10,7 @@ import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
+ faBurst,
faCalendar,
faChevronLeft,
faCircle,
@@ -36,6 +37,8 @@ import {
faRotate,
faShareNodes,
faSpinner,
+ faSquareCaretDown,
+ faSquareCaretUp,
faTrashCan,
faUser,
faUsers,
@@ -43,6 +46,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
library.add(
+ faBurst,
faCalendar,
faChevronLeft,
faCircle,
@@ -69,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 d7fd253f..5ae7a166 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",
@@ -111,6 +121,14 @@ const routes: Array = [
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
),
},
+ {
+ path: "/new-identifier",
+ name: "new-identifier",
+ component: () =>
+ import(
+ /* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
+ ),
+ },
{
path: "/project",
name: "project",
@@ -122,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 dd4c412b..17e6e2de 100644
--- a/src/views/AccountViewView.vue
+++ b/src/views/AccountViewView.vue
@@ -66,20 +66,22 @@
-
+
+
- Important: before you can create a new project or commit time to
- one, you need a friend to register you.
+ Note: Before you can publicly announce a new project or time
+ commitment, a friend needs to register you.