forked from trent_larson/crowd-funder-for-time-pwa
Merging remote master into local
This commit is contained in:
@@ -188,6 +188,9 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
|
|||||||
|
|
||||||
## Kudos
|
## 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)
|
* [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)
|
* [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)
|
||||||
|
|||||||
@@ -4,8 +4,11 @@
|
|||||||
- add infinite scroll assignee:matthew
|
- add infinite scroll assignee:matthew
|
||||||
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
|
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)
|
- 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 :
|
- stats v1 :
|
||||||
- 01 show numeric stats
|
- 01 show numeric stats
|
||||||
@@ -28,21 +31,41 @@
|
|||||||
|
|
||||||
- show pop-up confirming that settings & contacts have been downloaded
|
- 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 customize favicon
|
||||||
- .5 make advanced features harder to access; advanced build?
|
- .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 :
|
- Release Minimum Viable Product :
|
||||||
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
|
||||||
- Add disclaimers.
|
- Add disclaimers.
|
||||||
|
- Rename DB to TimeSafari.
|
||||||
- Switch default server to the public server.
|
- Switch default server to the public server.
|
||||||
- Deploy to a server.
|
- Deploy to a server.
|
||||||
- Ensure public server has limits that work for group adoption.
|
- Ensure public server has limits that work for group adoption.
|
||||||
- Test PWA features on Android and iOS.
|
- Test PWA features on Android and iOS.
|
||||||
|
|
||||||
|
- 40 notifications v+ :
|
||||||
|
- pull, w/ scheduled runs
|
||||||
|
|
||||||
- Stats :
|
- Stats :
|
||||||
- 01 point out user's location on the world
|
- 01 point out user's location on the world
|
||||||
- 01 present a credential selected from the stats
|
- 01 present a credential selected from the stats
|
||||||
@@ -66,6 +89,7 @@
|
|||||||
|
|
||||||
- Peer DID
|
- Peer DID
|
||||||
|
|
||||||
|
|
||||||
- DIDComm
|
- DIDComm
|
||||||
|
|
||||||
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
|
- Write to or read from a different ledger (eg. private ACDC, attest.sh)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 MiB |
BIN
public/img/textures/leafy-autumn-forest-floor.jpg
Normal file
BIN
public/img/textures/leafy-autumn-forest-floor.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 KiB |
104
src/components/GiftedDialog.vue
Normal file
104
src/components/GiftedDialog.vue
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="dialog-overlay">
|
||||||
|
<div class="dialog">
|
||||||
|
<h1 class="text-lg text-center">
|
||||||
|
Received from {{ contact?.name || "nobody in particular" }}
|
||||||
|
</h1>
|
||||||
|
<p class="py-2">{{ message }}</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||||
|
placeholder="What you received"
|
||||||
|
v-model="description"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<span class="py-4">Hours</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="block w-8 rounded border border-slate-400 ml-4 text-center"
|
||||||
|
v-model="hours"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col px-1">
|
||||||
|
<div>
|
||||||
|
<fa icon="square-caret-up" size="2xl" @click="increment()" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<button class="rounded border border-slate-400" @click="confirm">
|
||||||
|
<span class="m-2">Confirm</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="rounded border border-slate-400" @click="cancel">
|
||||||
|
<span class="m-2">Cancel</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["message"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
contact: null,
|
||||||
|
description: "",
|
||||||
|
hours: "0",
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
open(contact) {
|
||||||
|
this.contact = contact;
|
||||||
|
this.visible = true;
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.visible = false;
|
||||||
|
},
|
||||||
|
increment() {
|
||||||
|
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
|
||||||
|
},
|
||||||
|
decrement() {
|
||||||
|
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
this.close();
|
||||||
|
this.$emit("dialog-result", {
|
||||||
|
action: "confirm",
|
||||||
|
contact: this.contact,
|
||||||
|
hours: parseFloat(this.hours),
|
||||||
|
description: this.description,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.close();
|
||||||
|
this.$emit("dialog-result", { action: "cancel" });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
background-color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -31,6 +31,7 @@ export default class InfiniteScroll extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 'beforeUnmount' hook runs before unmounting the component
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
|
|||||||
|
|
||||||
export function createTerrain(props) {
|
export function createTerrain(props) {
|
||||||
const loader = new TextureLoader();
|
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
|
// w h
|
||||||
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
|
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
* 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 =
|
const secret =
|
||||||
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export type Account = {
|
|||||||
dateCreated: string;
|
dateCreated: string;
|
||||||
derivationPath: string;
|
derivationPath: string;
|
||||||
did: 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;
|
identity: string;
|
||||||
publicKeyHex: string;
|
publicKeyHex: string;
|
||||||
mnemonic: string;
|
mnemonic: string;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// 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
|
||||||
|
|
||||||
activeDid?: string;
|
activeDid?: string;
|
||||||
apiServer?: string;
|
apiServer?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
lastViewedClaimId?: string;
|
||||||
showContactGivesInline?: boolean;
|
showContactGivesInline?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,25 @@
|
|||||||
|
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 SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||||
export const SERVICE_ID = "endorser.ch";
|
export const SERVICE_ID = "endorser.ch";
|
||||||
|
|
||||||
|
export interface AgreeVerifiableCredential {
|
||||||
|
"@context": string;
|
||||||
|
"@type": string;
|
||||||
|
// "any" because arbitrary objects can be subject of agreement
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
object: Record<any, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimResult {
|
||||||
|
success: { claimId: string; handleId: string };
|
||||||
|
error: { code: string; message: string };
|
||||||
|
}
|
||||||
|
|
||||||
export interface GenericClaim {
|
export interface GenericClaim {
|
||||||
"@context": string;
|
"@context": string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
@@ -10,14 +29,6 @@ export interface GenericClaim {
|
|||||||
claim: Record<any, any>;
|
claim: Record<any, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgreeVerifiableCredential {
|
|
||||||
"@context": string;
|
|
||||||
"@type": string;
|
|
||||||
// "any" because arbitrary objects can be subject of agreement
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
object: Record<any, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GiveServerRecord {
|
export interface GiveServerRecord {
|
||||||
agentDid: string;
|
agentDid: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -33,10 +44,10 @@ export interface GiveServerRecord {
|
|||||||
export interface GiveVerifiableCredential {
|
export interface GiveVerifiableCredential {
|
||||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||||
"@type": string;
|
"@type": string;
|
||||||
agent: { identifier: string };
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
object: { amountOfThisGood: number; unitCode: string };
|
object?: { amountOfThisGood: number; unitCode: string };
|
||||||
recipient: { identifier: string };
|
recipient: { identifier: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,3 +58,125 @@ export interface RegisterVerifiableCredential {
|
|||||||
object: string;
|
object: string;
|
||||||
recipient: { identifier: 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<AxiosResponse<ClaimResult> | 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<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
|
||||||
|
//
|
||||||
|
export function isNumeric(str: string): boolean {
|
||||||
|
return !isNaN(+str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numberOrZero(str: string): number {
|
||||||
|
return isNumeric(str) ? +str : 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import "./assets/styles/tailwind.css";
|
|||||||
|
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import {
|
import {
|
||||||
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
faCircle,
|
faCircle,
|
||||||
@@ -36,6 +37,8 @@ import {
|
|||||||
faRotate,
|
faRotate,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faSquareCaretUp,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
@@ -43,6 +46,7 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
faBurst,
|
||||||
faCalendar,
|
faCalendar,
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
faCircle,
|
faCircle,
|
||||||
@@ -69,6 +73,8 @@ library.add(
|
|||||||
faRotate,
|
faRotate,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
|
faSquareCaretDown,
|
||||||
|
faSquareCaretUp,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||||
import { accountsDB } from "@/db";
|
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<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "home",
|
name: "home",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"),
|
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
||||||
beforeEnter: async (to, from, next) => {
|
beforeEnter: enterOrStart,
|
||||||
await accountsDB.open();
|
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
|
||||||
if (num_accounts > 0) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
next({ name: "start" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/about",
|
path: "/about",
|
||||||
@@ -28,6 +36,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "account",
|
name: "account",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/confirm-contact",
|
path: "/confirm-contact",
|
||||||
@@ -58,6 +67,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "contacts",
|
name: "contacts",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
|
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/scan-contact",
|
path: "/scan-contact",
|
||||||
@@ -111,6 +121,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
|
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/new-identifier",
|
||||||
|
name: "new-identifier",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/project",
|
path: "/project",
|
||||||
name: "project",
|
name: "project",
|
||||||
@@ -122,6 +140,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "projects",
|
name: "projects",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
|
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
|
||||||
|
beforeEnter: enterOrStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/seed-backup",
|
path: "/seed-backup",
|
||||||
|
|||||||
@@ -66,20 +66,22 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Friend referral requirement notice -->
|
<!-- Registration notice -->
|
||||||
|
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
|
||||||
<div
|
<div
|
||||||
|
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
|
||||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
|
||||||
>
|
>
|
||||||
<p class="mb-4">
|
<p class="mb-4">
|
||||||
<b>Important:</b> before you can create a new project or commit time to
|
<b>Note:</b> Before you can publicly announce a new project or time
|
||||||
one, you need a friend to register you.
|
commitment, a friend needs to register you.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<router-link
|
||||||
id="btnShowQR"
|
:to="{ name: 'contact-qr' }"
|
||||||
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
|
||||||
>
|
>
|
||||||
Share Your ID
|
Share Your Info
|
||||||
</button>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Identity Details -->
|
<!-- Identity Details -->
|
||||||
@@ -181,7 +183,7 @@
|
|||||||
<div class="text-slate-500 text-center">
|
<div class="text-slate-500 text-center">
|
||||||
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
||||||
</div>
|
</div>
|
||||||
<img src="img/sample-qr-code.png" class="w-full mb-3" />
|
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
value="cancel"
|
value="cancel"
|
||||||
@@ -229,6 +231,13 @@
|
|||||||
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
||||||
Check Limits
|
Check Limits
|
||||||
</button>
|
</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">
|
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
|
||||||
<span class="font-bold">Rate Limits</span>
|
<span class="font-bold">Rate Limits</span>
|
||||||
<p>
|
<p>
|
||||||
@@ -254,10 +263,10 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="apiServerInput != apiServer"
|
v-if="apiServerInput != apiServer"
|
||||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
class="px-4 rounded bg-red-500 border border-slate-400"
|
||||||
@click="onClickSaveApiServer()"
|
@click="onClickSaveApiServer()"
|
||||||
>
|
>
|
||||||
<fa icon="floppy-disk" class="fa-fw"></fa>
|
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-4 rounded bg-slate-200 border border-slate-400"
|
class="px-4 rounded bg-slate-200 border border-slate-400"
|
||||||
@@ -280,7 +289,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="numAccounts > 0" class="flex py-2">
|
<div v-if="numAccounts > 0" class="flex py-2">
|
||||||
Switch Account
|
Switch Identifier
|
||||||
<span v-for="accountNum in numAccounts" :key="accountNum">
|
<span v-for="accountNum in numAccounts" :key="accountNum">
|
||||||
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
|
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
|
||||||
#{{ accountNum }}
|
#{{ accountNum }}
|
||||||
@@ -289,10 +298,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button class="text-blue-500 px-2">
|
<button class="text-blue-500">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'statistics' }"
|
:to="{ name: 'statistics' }"
|
||||||
class="block text-center py-3 px-1"
|
class="block text-center py-3"
|
||||||
>
|
>
|
||||||
See Achievements & Statistics
|
See Achievements & Statistics
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -323,12 +332,7 @@ import { useClipboard } from "@vueuse/core";
|
|||||||
import { AppString } from "@/constants/app";
|
import { AppString } from "@/constants/app";
|
||||||
import { db, accountsDB } from "@/db";
|
import { db, accountsDB } from "@/db";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import {
|
import { accessToken } from "@/libs/crypto";
|
||||||
accessToken,
|
|
||||||
deriveAddress,
|
|
||||||
generateSeed,
|
|
||||||
newIdentifier,
|
|
||||||
} from "@/libs/crypto";
|
|
||||||
import { AxiosError } from "axios/index";
|
import { AxiosError } from "axios/index";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
@@ -357,6 +361,8 @@ export default class AccountViewView extends Vue {
|
|||||||
publicHex = "";
|
publicHex = "";
|
||||||
publicBase64 = "";
|
publicBase64 = "";
|
||||||
limits: RateLimits | null = null;
|
limits: RateLimits | null = null;
|
||||||
|
limitsMessage = "";
|
||||||
|
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
|
||||||
showContactGives = false;
|
showContactGives = false;
|
||||||
|
|
||||||
showDidCopy = false;
|
showDidCopy = false;
|
||||||
@@ -383,11 +389,12 @@ export default class AccountViewView extends Vue {
|
|||||||
|
|
||||||
// 'created' hook runs when the Vue instance is first created
|
// 'created' hook runs when the Vue instance is first created
|
||||||
async created() {
|
async created() {
|
||||||
// Uncomment to register this user on the test server.
|
// Uncomment this to register this user on the test server.
|
||||||
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
|
// To manage within the vue devtools browser extension https://devtools.vuejs.org/
|
||||||
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
|
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
|
||||||
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
|
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
|
||||||
//testServerRegisterUser();
|
//testServerRegisterUser();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
@@ -399,34 +406,13 @@ export default class AccountViewView extends Vue {
|
|||||||
this.showContactGives = !!settings?.showContactGivesInline;
|
this.showContactGives = !!settings?.showContactGivesInline;
|
||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
|
||||||
if (this.numAccounts === 0) {
|
|
||||||
let address = ""; // 0x... ETH address, without "did:eth:"
|
|
||||||
let privateHex = "";
|
|
||||||
const mnemonic = generateSeed();
|
|
||||||
[address, privateHex, this.publicHex, this.derivationPath] =
|
|
||||||
deriveAddress(mnemonic);
|
|
||||||
|
|
||||||
const newId = newIdentifier(
|
|
||||||
address,
|
|
||||||
this.publicHex,
|
|
||||||
privateHex,
|
|
||||||
this.derivationPath
|
|
||||||
);
|
|
||||||
await accountsDB.accounts.add({
|
|
||||||
dateCreated: new Date().toISOString(),
|
|
||||||
derivationPath: this.derivationPath,
|
|
||||||
did: newId.did,
|
|
||||||
identity: JSON.stringify(newId),
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
|
||||||
});
|
|
||||||
this.activeDid = newId.did;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
|
this.numAccounts = accounts.length;
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
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.publicHex = identity.keys[0].publicKeyHex;
|
this.publicHex = identity.keys[0].publicKeyHex;
|
||||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||||
this.derivationPath = identity.keys[0].meta.derivationPath;
|
this.derivationPath = identity.keys[0].meta.derivationPath;
|
||||||
@@ -436,11 +422,13 @@ export default class AccountViewView extends Vue {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertMessage =
|
this.alertMessage =
|
||||||
"Clear your cache and start over (after data backup). See console log for more info.";
|
"Clear your cache and start over (after data backup).";
|
||||||
console.error("Telling user to clear cache because:", err);
|
console.error("Telling user to clear cache at page create because:", err);
|
||||||
this.alertTitle = "Error Creating Account";
|
this.alertTitle = "Error Creating Account";
|
||||||
this.isAlertVisible = true;
|
this.isAlertVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.checkLimits();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateShowContactAmounts() {
|
public async updateShowContactAmounts() {
|
||||||
@@ -451,9 +439,12 @@ export default class AccountViewView extends Vue {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.alertMessage =
|
this.alertMessage =
|
||||||
"Clear your cache and start over (after data backup). See console log for more info.";
|
"Clear your cache and start over (after data backup).";
|
||||||
console.error("Telling user to clear cache because:", err);
|
console.error(
|
||||||
this.alertTitle = "Error Creating Account";
|
"Telling user to clear cache after contact setting update because:",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
this.alertTitle = "Error Updating Contact Setting";
|
||||||
this.isAlertVisible = true;
|
this.isAlertVisible = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,11 +473,17 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkLimits() {
|
async checkLimits() {
|
||||||
|
this.loadingLimits = true;
|
||||||
|
this.limitsMessage = "";
|
||||||
|
|
||||||
const url = this.apiServer + "/api/report/rateLimits";
|
const url = this.apiServer + "/api/report/rateLimits";
|
||||||
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 === this.activeDid, accounts);
|
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.");
|
||||||
|
}
|
||||||
const token = await accessToken(identity);
|
const token = await accessToken(identity);
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -502,18 +499,14 @@ export default class AccountViewView extends Vue {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const serverError = error as AxiosError;
|
const serverError = error as AxiosError;
|
||||||
|
|
||||||
this.alertTitle = "Error from Server";
|
|
||||||
console.error("Bad response retrieving limits: ", serverError);
|
console.error("Bad response retrieving limits: ", serverError);
|
||||||
// Anybody know how to access items inside "response.data" without this?
|
// Anybody know how to access items inside "response.data" without this?
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const data: any = serverError.response?.data;
|
const data: any = serverError.response?.data;
|
||||||
if (data.error.message) {
|
this.limitsMessage = data?.error?.message || "Bad server response.";
|
||||||
this.alertMessage = data.error.message;
|
|
||||||
} else {
|
|
||||||
this.alertMessage = "Bad server response. See logs for details.";
|
|
||||||
}
|
|
||||||
this.isAlertVisible = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadingLimits = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchAccount(accountNum: number) {
|
async switchAccount(accountNum: number) {
|
||||||
|
|||||||
@@ -168,7 +168,10 @@ export default class ContactsView extends Vue {
|
|||||||
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);
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error("No identity found.");
|
||||||
|
}
|
||||||
|
|
||||||
// load all the time I have given to them
|
// load all the time I have given to them
|
||||||
try {
|
try {
|
||||||
@@ -267,7 +270,10 @@ export default class ContactsView extends Vue {
|
|||||||
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 === this.activeDid, accounts);
|
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.");
|
||||||
|
}
|
||||||
if (identity.keys[0].privateKeyHex !== null) {
|
if (identity.keys[0].privateKeyHex !== null) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<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 mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
Contact Info
|
Your Contact Info
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -113,7 +113,10 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
if (!account) {
|
if (!account) {
|
||||||
this.alertMessage = "You have no identity yet.";
|
this.alertMessage = "You have no identity yet.";
|
||||||
} else {
|
} else {
|
||||||
const identity = JSON.parse(account.identity);
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error("No identity found.");
|
||||||
|
}
|
||||||
|
|
||||||
const publicKeyHex = identity.keys[0].publicKeyHex;
|
const publicKeyHex = identity.keys[0].publicKeyHex;
|
||||||
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ export default class ContactsView extends Vue {
|
|||||||
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 === this.activeDid, accounts);
|
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) {
|
if (!identity) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -487,7 +487,10 @@ export default class ContactsView extends Vue {
|
|||||||
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 === this.activeDid, accounts);
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim: RegisterVerifiableCredential = {
|
const vcClaim: RegisterVerifiableCredential = {
|
||||||
@@ -495,7 +498,7 @@ export default class ContactsView extends Vue {
|
|||||||
"@type": "RegisterAction",
|
"@type": "RegisterAction",
|
||||||
agent: { identifier: identity.did },
|
agent: { identifier: identity.did },
|
||||||
object: SERVICE_ID,
|
object: SERVICE_ID,
|
||||||
recipient: { identifier: contact.did },
|
participant: { identifier: contact.did },
|
||||||
};
|
};
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
@@ -530,7 +533,15 @@ export default class ContactsView extends Vue {
|
|||||||
try {
|
try {
|
||||||
const resp = await this.axios.post(url, payload, { headers });
|
const resp = await this.axios.post(url, payload, { headers });
|
||||||
//console.log("Got resp data:", resp.data);
|
//console.log("Got resp data:", resp.data);
|
||||||
if (resp.data?.success?.handleId) {
|
if (resp.data?.success?.embeddedRecordError) {
|
||||||
|
this.alertTitle = "Registration Still Unknown";
|
||||||
|
let message = "There was some problem with the registration.";
|
||||||
|
if (typeof resp.data.success.embeddedRecordError == "string") {
|
||||||
|
message += " " + resp.data.success.embeddedRecordError;
|
||||||
|
}
|
||||||
|
this.alertMessage = message;
|
||||||
|
this.isAlertVisible = true;
|
||||||
|
} else if (resp.data?.success?.handleId) {
|
||||||
contact.registered = true;
|
contact.registered = true;
|
||||||
db.contacts.update(contact.did, { registered: true });
|
db.contacts.update(contact.did, { registered: true });
|
||||||
|
|
||||||
@@ -567,7 +578,10 @@ export default class ContactsView extends Vue {
|
|||||||
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 === this.activeDid, accounts);
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
const token = await accessToken(identity);
|
const token = await accessToken(identity);
|
||||||
const headers = {
|
const headers = {
|
||||||
@@ -606,7 +620,10 @@ export default class ContactsView extends Vue {
|
|||||||
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 === this.activeDid, accounts);
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
const token = await accessToken(identity);
|
const token = await accessToken(identity);
|
||||||
const headers = {
|
const headers = {
|
||||||
@@ -663,7 +680,10 @@ export default class ContactsView extends Vue {
|
|||||||
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 === this.activeDid, accounts);
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|||||||
@@ -60,14 +60,14 @@
|
|||||||
gifts and collaboration.
|
gifts and collaboration.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How is this app useful?</h2>
|
<h2 class="text-xl font-semibold">What is the philosophy here?</h2>
|
||||||
<p>
|
<p>
|
||||||
We are building networks of people who want to grow a gifting society.
|
We are building networks of people who want to grow a gifting society.
|
||||||
First of all, you can record ways you've seen people give, and that
|
First of all, you can record ways you've seen people give, and that
|
||||||
leaves a permanent record... one that they show came from you. This is
|
leaves a permanent record -- one that came from you, and the recipient
|
||||||
personally gratifying, but it extends to broader work: volunteers can
|
can prove it was for them. This is personally gratifying, but it extends
|
||||||
get confirmation of activity and selectively show off their
|
to broader work: volunteers can get confirmation of activity and
|
||||||
contributions and network.
|
selectively show off their contributions and network.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
You can also record projects and plans and invite others to collaborate.
|
You can also record projects and plans and invite others to collaborate.
|
||||||
@@ -83,10 +83,25 @@
|
|||||||
the control; this app gives you the control.
|
the control; this app gives you the control.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
|
||||||
|
<p>
|
||||||
|
You need someone to register you -- usually the person who told you
|
||||||
|
about this app, on the Contacts
|
||||||
|
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
|
||||||
|
and after you have contacts, you can select any contact on the home page
|
||||||
|
and record your appreciation for... whatever. That is a claim recorded
|
||||||
|
on a custom ledger. The day after being registered, you'll be able to
|
||||||
|
register others; later, you can create projects, too.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Note that there are limits to how many each person can register, so you
|
||||||
|
may have to wait.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
|
<h2 class="text-xl font-semibold">How do I backup all my data?</h2>
|
||||||
<p>
|
<p>
|
||||||
There are two parts to backup your data: the identifier secrets and the
|
There are two sets of data to backup: the identifier secrets and the
|
||||||
other data such as settings, contacts, etc.
|
other data that isn't quite a secret such as settings, contacts, etc.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
@@ -100,6 +115,10 @@
|
|||||||
<li>
|
<li>
|
||||||
Click on "Backup Identifier Seed" and follow the instructions.
|
Click on "Backup Identifier Seed" and follow the instructions.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
If you have other identifiers, switch to each one and repeat those
|
||||||
|
steps.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
@@ -156,28 +175,6 @@
|
|||||||
key.
|
key.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
|
||||||
How do I get permission to store claims on the server?
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Get registered by someone else with the app; they can register you on
|
|
||||||
the Contacts <fa icon="circle-user" class="fa-fw" /> page. There are
|
|
||||||
limits to how many each person can register, so you may have to wait.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What do you mean by "claims"?</h2>
|
|
||||||
<p>
|
|
||||||
Certain actions in this app are signed by your private keys, and these
|
|
||||||
are often called "claims". For example, when you give time to a person
|
|
||||||
or project, you sign a claim declaring that you gave that time. When you
|
|
||||||
declare a project, you sign a claim declaring it to the world. When you
|
|
||||||
confirm someone else's claim, you sign a claim of agreement.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Some of the data in this app does not involve claims, such as your
|
|
||||||
contact list and your identifier.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
I know there is a record from someone, so why can't I see that info?
|
I know there is a record from someone, so why can't I see that info?
|
||||||
</h2>
|
</h2>
|
||||||
@@ -199,8 +196,8 @@
|
|||||||
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
<h2 class="text-xl font-semibold">How do I create another identity?</h2>
|
||||||
<p>
|
<p>
|
||||||
Go
|
Go
|
||||||
<router-link to="import-account" class="text-blue-500">
|
<router-link to="start" class="text-blue-500">
|
||||||
import another mnemonic here.
|
create another identity here.
|
||||||
</router-link>
|
</router-link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -216,6 +213,14 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ package.version }}
|
{{ package.version }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
For any other questions, including remove your data:
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Contact us through
|
||||||
|
<a href="https://communitycred.org">CommunityCred.org</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,15 +1,360 @@
|
|||||||
<template>
|
<template>
|
||||||
<section></section>
|
<!-- QUICK NAV -->
|
||||||
|
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
||||||
|
<ul class="flex text-2xl p-2 gap-2">
|
||||||
|
<!-- Home Feed -->
|
||||||
|
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
||||||
|
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
|
||||||
|
><fa icon="house-chimney" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Search -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'discover' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
><fa icon="magnifying-glass" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Projects -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'projects' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
><fa icon="folder-open" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Contacts -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'contacts' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
><fa icon="users" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Profile -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'account' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
><fa icon="circle-user" class="fa-fw"></fa
|
||||||
|
></router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Time Safari
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl">Quick Action</h1>
|
||||||
|
<p>Choose a contact to whom to show appreciation:</p>
|
||||||
|
<div class="px-4">
|
||||||
|
<button
|
||||||
|
v-for="contact in allContacts"
|
||||||
|
:key="contact.did"
|
||||||
|
@click="openDialog(contact)"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
{{ contact.name }},
|
||||||
|
</button>
|
||||||
|
or
|
||||||
|
<button @click="openDialog()" class="text-blue-500">
|
||||||
|
nobody in particular
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GiftedDialog
|
||||||
|
ref="customDialog"
|
||||||
|
@dialog-result="handleDialogResult"
|
||||||
|
message="Confirm to publish to the world."
|
||||||
|
>
|
||||||
|
</GiftedDialog>
|
||||||
|
|
||||||
|
<div class="py-4">
|
||||||
|
<h1 class="text-2xl">Latest Activity</h1>
|
||||||
|
<span :class="{ hidden: isHiddenSpinner }">
|
||||||
|
<fa icon="spinner" class="fa-fw"></fa>
|
||||||
|
Loading…
|
||||||
|
</span>
|
||||||
|
<ul class="">
|
||||||
|
<li
|
||||||
|
class="border-b border-slate-300"
|
||||||
|
v-for="record in feedData"
|
||||||
|
:key="record.jwtId"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="border-b text-orange-400 px-8 py-4"
|
||||||
|
v-if="record.jwtId == feedLastViewedId"
|
||||||
|
>
|
||||||
|
You've seen all claims below.
|
||||||
|
</div>
|
||||||
|
{{ this.giveDescription(record) }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- This same popup code is in many files. -->
|
||||||
|
<div v-bind:class="computedAlertClassNames()">
|
||||||
|
<button
|
||||||
|
class="close-button bg-slate-200 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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import * as R from "ramda";
|
||||||
import { Options, Vue } from "vue-class-component";
|
import { Options, Vue } from "vue-class-component";
|
||||||
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src
|
|
||||||
|
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||||
|
import { db, accountsDB } from "@/db";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
@Options({
|
@Options({
|
||||||
components: {
|
components: { GiftedDialog },
|
||||||
HelloWorld,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
export default class HomeView extends Vue {}
|
export default class HomeView extends Vue {
|
||||||
|
activeDid = "";
|
||||||
|
allAccounts: Array<Account> = [];
|
||||||
|
allContacts: Array<Contact> = [];
|
||||||
|
apiServer = "";
|
||||||
|
feedAllLoaded = false;
|
||||||
|
feedData = [];
|
||||||
|
feedPreviousOldestId = null;
|
||||||
|
feedLastViewedId = null;
|
||||||
|
isHiddenSpinner = true;
|
||||||
|
|
||||||
|
// 'created' hook runs when the Vue instance is first created
|
||||||
|
async created() {
|
||||||
|
await accountsDB.open();
|
||||||
|
this.allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.activeDid = settings?.activeDid || "";
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'mounted' hook runs after initial render
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.apiServer = settings?.apiServer || "";
|
||||||
|
|
||||||
|
this.updateAllFeed();
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Error in mounted():", err);
|
||||||
|
this.alertTitle = "Error";
|
||||||
|
this.alertMessage =
|
||||||
|
err.userMessage ||
|
||||||
|
"There was an error retrieving the latest sweet, sweet action.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllFeed = async () => {
|
||||||
|
this.isHiddenSpinner = false;
|
||||||
|
|
||||||
|
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
|
||||||
|
.then(async (results) => {
|
||||||
|
if (results.data.length > 0) {
|
||||||
|
this.feedData = this.feedData.concat(results.data);
|
||||||
|
//console.log("Feed data:", this.feedData);
|
||||||
|
this.feedAllLoaded = results.hitLimit;
|
||||||
|
this.feedPreviousOldestId =
|
||||||
|
results.data[results.data.length - 1].jwtId;
|
||||||
|
if (
|
||||||
|
this.feedLastViewedId == null ||
|
||||||
|
this.feedLastViewedId < results.data[0].jwtId
|
||||||
|
) {
|
||||||
|
// save it to storage
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
lastViewedClaimId: results.data[0].jwtId,
|
||||||
|
});
|
||||||
|
// but not for this page because we need to remember what it was before
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("Error with feed load:", e);
|
||||||
|
this.alertMessage =
|
||||||
|
e.userMessage || "There was an error retrieving feed data.";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.isHiddenSpinner = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
retrieveClaims = async (endorserApiServer, identifier, beforeId) => {
|
||||||
|
//const afterQuery = afterId == null ? "" : "&afterId=" + afterId;
|
||||||
|
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
|
||||||
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
if (this.activeDid) {
|
||||||
|
const account = R.find(
|
||||||
|
(acc) => acc.did === this.activeDid,
|
||||||
|
this.allAccounts
|
||||||
|
);
|
||||||
|
//console.log("about to parse from", this.activeDid, account?.identity);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error("No identity found.");
|
||||||
|
}
|
||||||
|
const token = await accessToken(identity);
|
||||||
|
headers["Authorization"] = "Bearer " + token;
|
||||||
|
} else {
|
||||||
|
// it's OK without auth... we just won't get any identifiers
|
||||||
|
}
|
||||||
|
return fetch(this.apiServer + "/api/v2/report/gives?" + beforeQuery, {
|
||||||
|
method: "GET",
|
||||||
|
headers: headers,
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const details = await response.text();
|
||||||
|
throw details;
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
|
if (results.data) {
|
||||||
|
return results;
|
||||||
|
} else {
|
||||||
|
throw JSON.stringify(results);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
giveDescription(giveRecord) {
|
||||||
|
let claim = giveRecord.fullClaim;
|
||||||
|
if (claim.claim) {
|
||||||
|
// it's probably a Verified Credential
|
||||||
|
claim = claim.claim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// agent.did is for legacy data, before March 2023
|
||||||
|
const giver =
|
||||||
|
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer;
|
||||||
|
const giverInfo = didInfo(giver, this.allAccounts, this.allContacts);
|
||||||
|
const gaveAmount = claim.object?.amountOfThisGood
|
||||||
|
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||||
|
: claim.description || "something unknown";
|
||||||
|
// recipient.did is for legacy data, before March 2023
|
||||||
|
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
||||||
|
const gaveRecipientInfo = gaveRecipientId
|
||||||
|
? " to " + didInfo(gaveRecipientId, this.allAccounts, this.allContacts)
|
||||||
|
: "";
|
||||||
|
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayAmount(code, amt) {
|
||||||
|
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
currencyShortWordForCode(unitCode, single) {
|
||||||
|
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
openDialog(contact) {
|
||||||
|
this.$refs.customDialog.open(contact);
|
||||||
|
}
|
||||||
|
handleDialogResult(result) {
|
||||||
|
if (result.action === "confirm") {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.recordGive(result.contact, result.description, result.hours);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// action was "cancel" so do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param contact may be null
|
||||||
|
* @param description may be an empty string
|
||||||
|
* @param hours may be 0
|
||||||
|
*/
|
||||||
|
recordGive(contact, description, hours) {
|
||||||
|
if (this.activeDid == null) {
|
||||||
|
this.alertTitle = "Error";
|
||||||
|
this.alertMessage =
|
||||||
|
"You must select an identity before you can record a give.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const account = R.find(
|
||||||
|
(acc) => acc.did === this.activeDid,
|
||||||
|
this.allAccounts
|
||||||
|
);
|
||||||
|
//console.log("about to parse from", this.activeDid, account?.identity);
|
||||||
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
|
if (!identity) {
|
||||||
|
throw new Error("No identity found.");
|
||||||
|
}
|
||||||
|
createAndSubmitGive(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
identity,
|
||||||
|
contact?.did,
|
||||||
|
this.activeDid,
|
||||||
|
description,
|
||||||
|
hours
|
||||||
|
)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.status != 201 || result.data?.error) {
|
||||||
|
console.log("Error with give result:", result);
|
||||||
|
this.alertTitle = "Error";
|
||||||
|
this.alertMessage =
|
||||||
|
result.data?.message || "There was an error recording the give.";
|
||||||
|
} else {
|
||||||
|
this.alertTitle = "Success";
|
||||||
|
this.alertMessage = "That gift was recorded.";
|
||||||
|
//this.updateAllFeed(); // full update is overkill but we should show something
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("Error with give caught:", e);
|
||||||
|
this.alertTitle = "Error";
|
||||||
|
this.alertMessage =
|
||||||
|
e.userMessage || "There was an error recording the give.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This same popup code is in many files.
|
||||||
|
alertMessage = "";
|
||||||
|
alertTitle = "";
|
||||||
|
public onClickClose() {
|
||||||
|
this.alertTitle = "";
|
||||||
|
this.alertMessage = "";
|
||||||
|
}
|
||||||
|
public computedAlertClassNames() {
|
||||||
|
return {
|
||||||
|
hidden: !this.alertMessage,
|
||||||
|
"dismissable-alert": true,
|
||||||
|
"bg-slate-100": 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>
|
</script>
|
||||||
|
|||||||
@@ -49,8 +49,9 @@
|
|||||||
<span :class="{ hidden: isHiddenSave }">Save Project</span>
|
<span :class="{ hidden: isHiddenSave }">Save Project</span>
|
||||||
|
|
||||||
<!-- SHOW if in saving state; DISABLE button while in saving state -->
|
<!-- SHOW if in saving state; DISABLE button while in saving state -->
|
||||||
<span :class="{ hidden: isHiddenSpinner }"
|
<span :class="{ hidden: isHiddenSpinner }">
|
||||||
><i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
<!-- icon no worky? -->
|
||||||
|
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
||||||
Saving…</span
|
Saving…</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
@@ -122,11 +123,14 @@ export default class NewEditProjectView extends Vue {
|
|||||||
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) {
|
||||||
console.error("Problem! Should have a profile!");
|
console.error("Error: no account was found.");
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
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);
|
this.LoadProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +226,7 @@ export default class NewEditProjectView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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;
|
const serverError = error as AxiosError;
|
||||||
if (serverError) {
|
if (serverError) {
|
||||||
this.isAlertVisible = true;
|
this.isAlertVisible = true;
|
||||||
@@ -255,11 +259,14 @@ export default class NewEditProjectView extends Vue {
|
|||||||
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) {
|
||||||
console.error("Problem! Should have a profile!");
|
console.error("Error: there is no account.");
|
||||||
} else {
|
} else {
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
const account = R.find((acc) => acc.did === this.activeDid, accounts);
|
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);
|
this.SaveProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/views/NewIdentifierView.vue
Normal file
126
src/views/NewIdentifierView.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<!-- QUICK NAV -->
|
||||||
|
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
|
||||||
|
<ul class="flex text-2xl p-2 gap-2">
|
||||||
|
<!-- Home Feed -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
|
||||||
|
<fa icon="house-chimney" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Search -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'discover' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Projects -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'projects' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="folder-open" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Contacts -->
|
||||||
|
<li class="basis-1/5 rounded-md text-slate-500">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'contacts' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="users" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<!-- Profile -->
|
||||||
|
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'account' }"
|
||||||
|
class="block text-center py-3 px-1"
|
||||||
|
>
|
||||||
|
<fa icon="circle-user" class="fa-fw"></fa>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Your Identity
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="flex justify-center py-12">
|
||||||
|
<span />
|
||||||
|
<span v-if="loading">
|
||||||
|
<span class="text-xl">Creating... </span>
|
||||||
|
<fa
|
||||||
|
icon="spinner"
|
||||||
|
class="fa-spin fa-spin-pulse"
|
||||||
|
color="green"
|
||||||
|
size="128"
|
||||||
|
></fa>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<span class="text-xl">Created!</span>
|
||||||
|
<fa
|
||||||
|
icon="burst"
|
||||||
|
class="fa-beat px-12"
|
||||||
|
color="green"
|
||||||
|
style="
|
||||||
|
--fa-animation-duration: 1s;
|
||||||
|
--fa-animation-direction: reverse;
|
||||||
|
--fa-animation-iteration-count: 1;
|
||||||
|
--fa-beat-scale: 6;
|
||||||
|
"
|
||||||
|
></fa>
|
||||||
|
</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import "dexie-export-import";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { accountsDB, db } from "@/db";
|
||||||
|
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
|
||||||
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class AccountViewView extends Vue {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await accountsDB.open();
|
||||||
|
const mnemonic = generateSeed();
|
||||||
|
// address is 0x... ETH address, without "did:eth:"
|
||||||
|
const [address, privateHex, publicHex, derivationPath] =
|
||||||
|
deriveAddress(mnemonic);
|
||||||
|
|
||||||
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||||
|
const identity = JSON.stringify(newId);
|
||||||
|
await accountsDB.accounts.add({
|
||||||
|
dateCreated: new Date().toISOString(),
|
||||||
|
derivationPath: derivationPath,
|
||||||
|
did: newId.did,
|
||||||
|
identity: identity,
|
||||||
|
mnemonic: mnemonic,
|
||||||
|
publicKeyHex: newId.keys[0].publicKeyHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: newId.did,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push({ name: "account" });
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -240,7 +240,10 @@ export default class ProjectViewView extends Vue {
|
|||||||
} else {
|
} else {
|
||||||
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);
|
||||||
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);
|
this.LoadProject(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,8 +222,10 @@ export default class ProjectsView extends Vue {
|
|||||||
} else {
|
} else {
|
||||||
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);
|
||||||
const identity = JSON.parse(account?.identity || "undefined");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
this.current = identity;
|
if (!identity) {
|
||||||
|
throw new Error("No identity found.");
|
||||||
|
}
|
||||||
this.LoadProjects(identity);
|
this.LoadProjects(identity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { Options, Vue } from "vue-class-component";
|
|||||||
})
|
})
|
||||||
export default class StartView extends Vue {
|
export default class StartView extends Vue {
|
||||||
public onClickYes() {
|
public onClickYes() {
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "new-identifier" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onClickNo() {
|
public onClickNo() {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
|
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
|
||||||
<li>Each will show at their time of appearance relative to all others.</li>
|
<li>Each will show at their time of appearance relative to all others.</li>
|
||||||
<li>Note that the ones on the left and right edges are randomized
|
<li>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.
|
||||||
</li>
|
</li>
|
||||||
<!-- eslint-enable -->
|
<!-- eslint-enable -->
|
||||||
</ul>
|
</ul>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
{{ worldProperties.endTime }}
|
{{ worldProperties.endTime }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="worldProperties.animationDurationSeconds">
|
<div v-if="worldProperties.animationDurationSeconds">
|
||||||
<label>Animation duration: </label>
|
<label>Animation Time: </label>
|
||||||
{{ worldProperties.animationDurationSeconds }} seconds
|
{{ worldProperties.animationDurationSeconds }} seconds
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,6 +113,7 @@ export default class StatisticsView extends Vue {
|
|||||||
world: World;
|
world: World;
|
||||||
worldProperties: WorldProperties = {};
|
worldProperties: WorldProperties = {};
|
||||||
|
|
||||||
|
// 'mounted' hook runs after initial render
|
||||||
mounted() {
|
mounted() {
|
||||||
const container = document.querySelector("#scene-container");
|
const container = document.querySelector("#scene-container");
|
||||||
const newWorld = new World(container, this);
|
const newWorld = new World(container, this);
|
||||||
|
|||||||
Reference in New Issue
Block a user